doc/gopath.txt — rendered from the plugin's own vimdoc
*gopath.txt* Intelligent file navigation for Neovim *gopath.nvim* Author: Stefan Bartl Version: 0.3.0
CONTENTS
1. Introduction ............................ |gopath-intro| 2. Requirements ............................ |gopath-requirements| 3. Installation ............................ |gopath-installation| 4. Keymaps ................................. |gopath-keymaps| 5. Commands ................................ |gopath-commands| 6. Resolution Pipeline ..................... |gopath-pipeline| 7. Probe ................................... |gopath-probe| 8. Configuration ........................... |gopath-config| 9. Language Support ........................ |gopath-languages| 10. Health Check ............................ |gopath-health| 11. Troubleshooting ......................... |gopath-troubleshooting|
1. INTRODUCTION
gopath.nvim is a modular file-navigation plugin that resolves symbols, Lua
module paths, and arbitrary file references in any buffer. It combines a
multi-phase resolution pipeline with whole-line path extraction, suffix-based
filesystem search, fuzzy alternate matching, and external file opening.
Key capabilities:
- LSP / Treesitter / Builtin resolution chain
- Whole-line stacktrace and path extraction (Phase 3.5)
- Suffix-based search: resolves partial / truncated paths
- Visual-selection probe: select a path token, resolve + open
- Create-on-missing dialog; offers to open the nearest existing ancestor
directory in filetree.nvim when installed (never automatic)
- Env var expansion: $VAR/path/file.md
- Fuzzy alternate (Levenshtein) when file does not exist
- External opener for images, PDFs, media
- Reveal resolved targets in the OS file manager (gM)
- Reveal resolved targets in filetree.nvim's own tree (gT, soft dep)
2. REQUIREMENTS
- Neovim >= 0.10 - Optional: fd / fdfind (fast file search) - Optional: rg (ripgrep) (fallback search) - Optional: git (git-root detection) - Optional: nvim-treesitter
3. INSTALLATION
lazy.nvim:
{
"StefanBartl/gopath.nvim",
event = "VeryLazy",
dependencies = {
"StefanBartl/lib.nvim", -- required
"nvim-treesitter/nvim-treesitter", -- optional but recommended
},
opts = { mode = "hybrid" },
}
A lazy-load trigger (event/cmd/keys/ft, or lazy = false) is REQUIRED, not
optional. With an opts = {...} table and no trigger, lazy.nvim never
sources the plugin: setup() is never called, no keymaps or commands are
registered, and no error is shown — gP simply does nothing and even
:checkhealth gopath is "not found". See |gopath-troubleshooting|.
4. KEYMAPS
Default keymaps (all configurable, see |gopath-config-mappings|): gP Open target in current window g| Open target in horizontal split g\ Open target in vertical split g} Open target in new tab gM Reveal target in the system file manager (Explorer/Finder/…) gT Reveal target in filetree.nvim's own tree (soft dependency) gY Copy path:line:col to clipboard g? Debug resolution under cursor gC Check path under cursor exists; offer to create if missing <leader>pp (n) Probe path under cursor via suffix search (vsplit) <leader>pp (v) Probe visual selection via suffix search (vsplit) Disable all keymaps:
opts = { mappings = false }
Disable a single keymap:
opts = { mappings = { probe = false } }
Multiple keys for one action:
opts = { mappings = { open_here = { "gP", "<2-LeftMouse>" } } }
5. COMMANDS
Unified command with tab-completion at every level: *:Gopath* :Gopath open [edit|split|vsplit|tab|explorer] Resolve and open target with the given mode. "explorer" reveals the target in the system file manager (Explorer/Finder/…) instead of opening a buffer for it. Default mode: edit (current window). :Gopath copy Copy resolved path:line:col to the system clipboard. :Gopath debug Print full resolution chain to :messages. :Gopath check Check whether the path under cursor exists; offer to create it if missing (does not open on a hit). :Gopath probe [edit|split|vsplit] Suffix-based search using path under cursor or visual selection. Default: vsplit. See |gopath-probe|. :Gopath cache build Rebuild the in-memory filesystem cache asynchronously. :Gopath cache info Show cache statistics (file count, age, staleness). :Gopath cache add-root {dir} Add a directory to the cache search roots and rebuild. Individual alias commands (backward compatibility): :GopathOpen [mode] -> :Gopath open [mode] *:GopathOpen* :GopathCopy -> :Gopath copy *:GopathCopy* :GopathDebug -> :Gopath debug *:GopathDebug* :GopathCheck -> :Gopath check *:GopathCheck* :GopathResolve -> :Gopath debug *:GopathResolve* :GopathProbe[!] -> :Gopath probe (! = split) *:GopathProbe* :GopathCacheBuild -> :Gopath cache build *:GopathCacheBuild* :GopathCacheInfo -> :Gopath cache info *:GopathCacheInfo* :GopathCacheAddRoot {dir} -> :Gopath cache add-root *:GopathCacheAddRoot*
6. RESOLUTION PIPELINE
gopath.nvim tries each phase in order and returns the first success.
Phase 1 - :help subject
When the token under cursor looks like a Vim help tag (vim.api.*,
vim.fn.*, etc.) it is opened with :help.
Phase 2 - Environment variable path
$VAR/path/file.md and ${VAR}/path/file.md are expanded using the
current process environment before file lookup.
Phase 3 - filetoken (generic <cfile> resolver)
Expands <cfile>, strips error-message prefixes, parses :line:col,
searches rtp and vim &path (honouring 'suffixesadd', same as gf).
Falls back to tailsearch before building a speculative absolute path.
'includeexpr' is intentionally NOT consulted: gopath's own multi-phase
pipeline (LSP/Treesitter/language resolvers/linepath/tailsearch)
already covers what includeexpr is typically set up to do per-
filetype, more completely and without per-filetype configuration.
Phase 3.5 - linepath (whole-line extraction) *gopath-linepath*
Scans vim.api.nvim_get_current_line() using three heuristics:
a) Stacktrace patterns: path:line:col and path:line via gmatch
b) Extension-driven: find known extensions, expand around them
(150+ extensions in common_extensions.lua)
c) Absolute paths: /unix/path, C:\win\path, \\unc\path
For each candidate: absolute check -> cwd-relative -> tailsearch.
Runs only when Phase 3 returned nil or a low-confidence non-existent
result, so it never overshadows a good filetoken hit.
Phase 4 - Language-specific resolvers
Lua: require_path, alias_index, binding_index, chain, identifier_locator,
symbol_locator, table_locator, value_origin; backed by LSP and/or
Treesitter according to mode and order. Despite the "treesitter
provider" name, symbol_locator.via_treesitter and table_locator
still locate the target line/table region with line-oriented Lua
patterns (not treesitter queries) — chosen originally for
tolerance of newlines-after-=, bracketed keys, and tables
nested inside function calls. A full migration to treesitter
queries is tracked as future work; known bugs in the pattern
matching are fixed as found (see CHANGELOG-equivalent commit
history for table_locator.find_child_table).
With the default order = { "lsp", "treesitter", "builtin" }, a chained
reference (config.setup()) tries symbol_locator.via_lsp first, landing
on the exact definition line/column; the treesitter chain walk (via
identifier_locator / symbol_locator.via_treesitter / value_origin) only
runs when LSP has no client or times out (lsp_timeout_ms). A bare
identifier with no .field chain (local resolver = require("a.b");
cursor on resolver alone) is resolved by identifier_locator, which
runs before the chain-based resolvers in the treesitter pass.
Lua module names resolve through a three-step chain:
1. runtimepath
2. package.path
3. the lua/ tree of every installed plugin
Step 3 covers plugins a manager knows about but has not loaded yet:
until such a plugin loads, its directory is on neither the runtimepath
nor package.path, so require("x.y") pointing into a lazily-loaded
plugin would otherwise not resolve. lazy.nvim and |vim.pack| are both
supported; each plugin's lua/ directory is indexed once by top-level
module name, so a token that is not a module costs a single lookup.
Path lookup caching *gopath-path-cache*
Every search root (runtimepath entry, plugin dir) is read once with a
single scandir and indexed by the names directly inside it. A candidate
is only stat'ed in a root whose index contains its first path segment,
so a token that resolves to nothing is rejected by hash lookup alone.
Search order is unchanged: the index only skips probes that could not
have matched.
This matters because a miss is the common case - any dotted token under
the cursor enters the chain. Uncached, a miss against a 50-entry
runtimepath costs ~200 stat calls (~9.7 ms on Windows); indexed, ~0.09 ms.
Only the FIRST path segment is indexed, so a new file inside an already
known directory needs no invalidation. A brand-new top-level entry is
picked up when the runtimepath changes, when gopath itself creates the
file, on |BufWritePost| (see docs/BINDINGS.md), or after a 30s TTL.
value_origin (table-chain root inference) similarly caches, per target
file, the list of candidate root identifiers ("M", locally-declared
tables, the return-identifier) it tries when locating a chain like
cfg.highlight inside that file. The cache is keyed by the file's
mtime, so repeated lookups against the same unedited file skip the
read + line-scan; it is rebuilt automatically once the file's mtime
changes.
Phase 5 - Fallback
Returns the Phase 3 filetoken result (even if non-existent) so the
alternate fallback / create-on-missing dialog can handle it; then
raw <cfile>.
After pipeline - Alternate + Create-on-missing
If the resolved file does not exist:
1. Fuzzy alternate: Levenshtein similarity (with a prefix bonus for
truncated/abbreviated names) in the same directory. Each
candidate shown lists its size and modification recency. The
selection UI defers to your configured vim.ui.select backend
(telescope-ui-select, dressing.nvim, ...) when one is installed.
2. Create-on-missing: offers to create the file. A directory can't
be opened in a buffer the way a file can, so there is no
automatic "open nearest folder" fallback -- if an existing
ancestor directory exists and filetree.nvim is installed and set
up, the dialog offers a second choice, "Open in filetree", instead.
7. PROBE
The probe command / keymap implements the pathprobe strategy integrated into
gopath. It resolves partial, truncated, or unknown paths using suffix-based
search across multiple roots.
Input (in priority order):
1. Visual selection (in v / V / CTRL-V mode)
2. <cfile> under cursor
3. <cword> under cursor
Resolution:
- Strips :line:col, ellipsis ("..."), quotes
- Generates suffix candidates (last 1-6 path components, longest-first)
- Searches via vim.fs.find with a path_ends_with predicate
- Roots: bufdir -> cwd -> git root -> stdpath config / data / cache
- Single hit: opens directly
- Multiple hits: vim.ui.select if ask_on_ambiguous = true, else shortest
Keymaps:
<leader>pp (normal) probe path under cursor, open in vsplit
<leader>pp (visual) probe selected text, open in vsplit
Commands:
:Gopath probe [mode] (mode: edit | split | vsplit)
:GopathProbe[!] (! = split)
8. CONFIGURATION
Pass a table to setup():
require("gopath").setup({ ... })
Or use lazy.nvim opts = { ... }.
All keys are optional. Unset keys keep their defaults.
*gopath-config-mode*
mode = "hybrid"
Resolution mode: "hybrid" | "lsp" | "treesitter" | "builtin"
order = { "lsp", "treesitter", "builtin" }
Provider order for hybrid mode.
lsp_timeout_ms = 200
Milliseconds to wait for an LSP response.
*gopath-config-linepath*
linepath = {
enable = true,
}
Whole-line path extraction (Phase 3.5).
Disable if you do not want the full line scanned.
*gopath-config-tailsearch*
tailsearch = {
enable = true,
max_components = 6,
ask_on_ambiguous = true,
roots = nil, -- nil = auto-detect
limit = 100,
}
Suffix-based filesystem search.
roots: explicit string[] overrides the auto-detected set.
*gopath-config-alternate*
alternate = {
enable = true,
similarity_threshold = 75,
frecency = {
enable = true,
max_bonus = 10,
dir = nil,
},
}
Levenshtein fuzzy alternate resolution.
Higher threshold = stricter matching.
frecency: candidates you have picked from this dialog before rise
within their similarity band, so the one you meant last time is on
top the next time three near-identical names come up together.
max_bonus is the size of that band, in similarity points. The bonus
saturates and is capped there, so history breaks near-ties and can
never push a 95% match below a 76% one. Set it to 0 to keep
recording without reordering; set enable = false for neither.
dir overrides the storage directory; nil uses lib.nvim.frecency's
own, under stdpath("data").
*gopath-config-external*
external = {
enable = true,
extensions = nil,
}
External opener for images, PDFs, media.
extensions: string[] of additional extensions (without the dot, e.g.
"heic") that EXTEND the built-in list — it is not replaced.
enable = false disables the external opener entirely; matching files
are then opened as normal buffers instead.
*gopath-config-url*
url = {
enable = true,
bare_hosts = true,
schemes = nil,
tlds = nil,
}
URL recognition. A URL under the cursor is handed to the external
opener (browser) instead of being resolved as a file path, which is
what makes the open keymaps work on links in :messages, notification
buffers, comments and markdown.
Strict forms — an explicit scheme ("https://", "ftp://", "file://",
"mailto:") or a "www." prefix — are matched before every file
resolver: no local path can be spelled that way.
bare_hosts = true additionally accepts scheme-less targets such as
"github.com/neovim/neovim" and scp-style git remotes such as
"git@github.com:foo/bar.git", normalizing both to https://. Those
forms DO collide with real filenames, so they are only tried once
every file resolver has come up empty — an existing "notes.info"
still opens as a file. Set bare_hosts = false to require a scheme.
schemes / tlds: string[] that EXTEND the built-in scheme and TLD
lists — neither is replaced.
The URL is read from the buffer line rather than from <cfile>, so
query strings and fragments survive ('isfname' stops at "?" / "&" /
"#"). Wrapping delimiters and trailing sentence punctuation are
stripped: "(https://x)." resolves to "https://x".
*gopath-config-truncated*
truncated = {
enable = true,
use_cache = true,
cache_refresh_interval = 600,
max_cache_age = 3600,
live_search_fallback = true,
cache_roots = nil,
max_depth = 6,
excluded_dirs = { ".git", "node_modules", ... },
auto_rebuild_on_save = false,
}
"..." and "..." prefixed truncated path resolution.
*gopath-config-mappings*
mappings = {
open_here = "gP",
open_split = "g|",
open_vsplit = "g\\",
open_tab = "g}",
open_explorer = "gM",
open_filetree = "gT",
copy_location = "gY",
debug = "g?",
probe = "<leader>pp",
check = "gC",
}
Set any key to false to disable that mapping.
String array { "gP", "<2-LeftMouse>" } registers multiple keys.
*gopath-config-commands*
commands = {
resolve = true,
open = true,
copy = true,
debug = true,
}
Set to false to disable the individual alias commands.
Set commands = false to disable ALL user commands.
9. LANGUAGE SUPPORT
Lua (full support):
- require("a.b.c") -> lua/a/b/c.lua
- Bare identifier: local resolver = require("a.b"); cursor on resolver
alone -> a/b.lua (identifier_locator)
- Variable chains: local x = require("mod"); x.func() -> mod.lua @ func
- Table key lookup: config.get() -> definition of get in config module
All filetypes (universal):
- File paths with :line:col, (line), +line
- Env var paths ($VAR, ${VAR})
- Whole-line stacktrace extraction (linepath)
- Suffix-based partial-path search (tailsearch)
- Help tag resolution
- External file opening
10. HEALTH CHECK
:checkhealth gopath Verifies: - Neovim version (>= 0.10 required) - External tools: fd / fdfind, rg, git - Active LSP clients - Tree-sitter parsers for current filetype - Configuration: linepath, tailsearch, alternate, keymaps - Truncated cache: finder module, file count, staleness - Language resolvers enabled/disabled
11. TROUBLESHOOTING
gP does nothing, and :Gopath / :checkhealth gopath don't exist either:
gopath.nvim was never loaded. With lazy.nvim, an opts = {...} table with
no event/cmd/keys/ft trigger (and no lazy = false) is never sourced --
there is no error, setup() is simply never called. Add a trigger; see
|gopath-installation|.
gP does nothing, but :Gopath debug / :checkhealth gopath work:
:Gopath debug -- shows resolver output :checkhealth gopath -- check tools and config Requires Neovim >= 0.10
Path not found:
<leader>pp -- probe uses suffix search across more roots :Gopath cache add-root <dir> -- extend search roots Ensure fd or rg is installed
Truncated path ("...") not resolving:
:Gopath cache info -- check cache status :Gopath cache build -- rebuild the index :checkhealth gopath -- verify fd/rg is available
Multiple matches / wrong file:
tailsearch.ask_on_ambiguous = true shows vim.ui.select Set tailsearch.roots explicitly to narrow the scope