gopath.nvim · Files & navigation · vimdoc

:help gopath

Intelligent file navigation for Neovim

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 *gopath-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-intro*

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 *gopath-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 *gopath-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 *gopath-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 *gopath-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-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 *gopath-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 *gopath-config*

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 *gopath-languages*

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 *gopath-health*

  :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 *gopath-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