open.nvim · Files & navigation · vimdoc
:help open
Open files, URLs, and paths from anywhere in Neovim
doc/open.txt — rendered from the plugin's own vimdoc
*open.txt* Open files, URLs, and paths from anywhere in Neovim *open.nvim* Author: Stefan Bartl Version: 0.1.0
CONTENTS
1. Introduction ................. |open-intro| 2. Requirements ................. |open-requirements| 3. Installation ................. |open-installation| 4. Configuration ................ |open-config| 4.1 Office document auto-redirect |open-office-open| 5. The :Open command ............ |:Open| 5.1 Target argument .......... |open-target| 5.2 Scope argument ........... |open-scope| 5.3 Handlers ................. |open-handlers| 6. Keywords ..................... |open-keywords| 6.1 Built-in keywords ........ |open-keywords-builtin| 6.2 User-defined keywords .... |open-keywords-user| 7. Tab completion ............... |open-completion| 8. The :Open viewer command ..... |:Open-viewer| 8.1 Kind ..................... |open-viewer-kind| 8.2 Scope .................... |open-viewer-scope| 8.3 Options .................. |open-viewer-options| 8.4 The picker ............... |open-viewer-picker| 8.5 Outputs .................. |open-viewer-outputs| 9. Lua API ...................... |open-api| 10. Integrations ................. |open-integrations| 10.1 urlview.nvim ............ |open-integrations-urlview| 10.2 telescope.nvim .......... |open-integrations-telescope| 11. Health check ................. |open-health| 12. Architecture ................. |open-architecture|
1. INTRODUCTION
open.nvim provides a single :Open [target] [scope] command that routes the
thing under your cursor — path, URL, or plain text — to the right destination:
system file manager, browser (with named-browser support), GUI text editor,
terminal split rooted in the target's directory, inline image viewer (via
images.nvim, with fallback), or a Neovim split/tab.
Context-aware: knows when you are in a Neo-tree, nvim-tree, or netrw buffer
and opens the node under the cursor automatically.
Depends on lib.nvim (a deliberate shared dependency).
2. REQUIREMENTS
- Neovim 0.9 or later - lib.nvim - Platform tools as needed per handler (see |open-health|)
3. INSTALLATION
open.nvim only does anything once:Openis invoked, so it should be loaded lazily on that command rather than eagerly (lazy = false) or on a UI event (event = "VeryLazy") — those would just load the plugin sooner for no benefit. lazy.nvim:
{
"StefanBartl/open.nvim",
cmd = { "Open", "UrlView", "MDLinksView" },
dependencies = { "StefanBartl/lib.nvim" },
opts = {},
}
packer:
use {
"StefanBartl/open.nvim",
requires = { "StefanBartl/lib.nvim" },
cmd = { "Open", "UrlView", "MDLinksView" },
config = function() require("open").setup() end,
}
4. CONFIGURATION
require("open").setup({
command = "Open", -- user command name
default_filemanager = "filemanager", -- handler for paths (no explicit target)
default_browser = "browser", -- handler for URLs (no explicit target)
handlers = {
"filemanager",
"browser",
"notepad",
"nvim_internal",
"default",
"terminal",
"image",
},
builtin_keywords = true, -- set false to disable all built-in keywords
keywords = {}, -- user overrides / additions (keyword → path or fn)
custom_handlers = {}, -- user-defined handlers, registered alongside `handlers`
keymaps = {}, -- optional keymaps; none registered by default
filemanager = { reveal = true }, -- false: navigate instead of reveal/select
office_open = { -- redirect MS Office docs to the system app
enabled = true,
extensions = { "doc", "docx", "xls", "xlsx", "ppt", "pptx" },
},
debug = false, -- true: log gather/resolve/dispatch steps to :messages
picker = { enabled = false }, -- true: show a picker on ambiguous no-target calls
})
commandName of the registered user command. Change to avoid conflicts.default_filemanagerHandler key used when the context is path-like and no target was given.default_browserHandler key used when the context looks like a URL and no target was given.handlersList of handler module keys to load duringsetup(). Removing a key prevents those handlers from registering, hiding them from tab-completion. Valid values: "filemanager" | "browser" | "notepad" | "nvim_internal" | "default" | "terminal" | "image"builtin_keywordsBoolean (default true). When false, none of the built-in scope keywords (shell profiles, git, SSH, …) are loaded. See |open-keywords-builtin|.keywordsTable mapping keyword strings to paths. Values are either a string (expanded at resolution time) or afun(): string|nilfor dynamic paths. User entries override built-in entries with the same key. See |open-keywords-user|.custom_handlersList of user-definedOpenNvim.Handlertables (key,desc,run), registered in addition to the built-inhandlersmodules. Akeyhere overrides a built-in handler of the same name. Example:
require("open").setup({
custom_handlers = {
{
key = "zathura",
desc = "Open PDF in Zathura",
run = function(ctx)
return require("open.util").run_detached({ "zathura", ctx.text }, "zathura")
end,
},
},
})
keymapsOptional keymaps for common invocations. None registered by default. Accepted keys come from the live handler registry, not a fixed list:open_default(bare:Open) plusopen_<handler key>for every registered handler —open_browser,open_filemanager,open_split,open_vsplit,open_tab,open_terminal,open_image,open_notepad, the named browsers (open_firefox,open_chrome, ...), and any handler added viacustom_handlers— plus the historical aliasopen_manager(=open_filemanager). An unrecognized key warns, names the accepted keys, and registers nothing. A handler switched off viaopts.handlersis rejected the same way. Example:
require("open").setup({
keymaps = {
open_default = "<leader>oo",
open_browser = "<leader>ob",
open_manager = "<leader>of",
open_split = "<leader>os",
open_terminal = "<leader>ot",
},
})
filemanagerSettings for thefilemanagerhandler. Keys: reveal true (default): reveal a file (select it in its parent directory). false: navigate to it (open its parent directory without selecting). Directories are always navigated into regardless of this setting — there is nothing to select for a directory target. command Launcher override: a string ("thunar") or an argv list ({ "dolphin", "--select" }). The resolved path is appended as the last argument and the platform dispatch is skipped. Default nil (detect per platform).office_openSettings for the office-document auto-redirect (see |open-office-open|). Keys: enabled true (default): install the BufReadCmd redirect. extensions Bare extensions, no leading dot. Default: {"doc", "docx", "xls", "xlsx", "ppt", "pptx"}debugBoolean (default false). When true, logs everycontext.gather(),context.resolve(), andregistry.dispatch()step to:messages.pickerenabled(default false). When true, a no-target:Open/open.open()call whose context has more than one meaningful handler shows a picker (ui.kit.select, honoring anyvim.ui.selectoverride) instead of picking one automatically. Candidates: tree-buffer node default_filemanager only (no prompt) looks like a URL default_browser, notepad <cfile> is an existing path default_filemanager, split, vsplit, tab anything else default_filemanager only An explicit target always bypasses the picker.viewerSettings for |:Open-viewer|. Keys: commands Standalone wrapper command names, one per kind filter. Set a value to false to skip registering it. urls default "UrlView" mdlinks default "MDLinksView" all default false (use:Open viewer) sort Default ordering: "none" (default), "file", "kind", "alpha". output Default sink: "picker" (default), "table", "clipboard", "mdlinks", "csv". mdlinks_output Sink forout=mdlinks(default "clipboard"). open_file Handler used when a picked entry is a local file (default "split"). Any registered handler key works. All of them are overridable per invocation — see |open-viewer-options|.
4.1 Office document auto-redirect
.doc/.docx,.xls/.xlsx,.ppt/.pptxare binary containers — there is nothing useful Neovim can show by reading one as text. ABufReadCmdautocmd (see |open-architecture|,open/office_open.lua) intercepts a read of any configured extension, hands the path to the samelib.nvim.cross.open_defaultdispatch thedefaulthandler uses (Word, Excel, PowerPoint, or whatever the OS has registered), and wipes the empty placeholder buffer Neovim created for the read. Unlike everything else in this plugin, this is NOT something you invoke:BufReadCmdis Neovim's own read hook, so it fires for:e,gf, a picker result, or a tree plugin's<CR>alike — including filetree.nvim, nvim-tree, and neo-tree — without that caller needing to know open.nvim exists. Configure viaopts.office_open(see |open-config|):
require("open").setup({
office_open = {
enabled = true,
extensions = { "doc", "docx", "xls", "xlsx", "ppt", "pptx" },
},
})
Setenabled = false, orextensions = {}, to turn this off and get Neovim's normal (garbled) text-buffer behavior back for these extensions.
5. THE :Open COMMAND
:Open [target] [scope]
Built via lib.nvim.bindings.usercmd.composer (single flatpath = {}root route — no subcommand tree), which is also what drives |open-completion|. With no arguments,:Openuses a context-aware heuristic: - Current buffer is a tree (neo-tree / nvim-tree / netrw) → filemanager - cfile / cword looks like a URL → browser - Otherwise → filemanager
5.1 Target argument
The first argument selects a handler. Any registered key is valid. :Open filemanager :Open browser :Open firefox :Open split Omitting the target triggers the heuristic described above.
5.2 Scope argument
The second argument controls what text is passed to the handler. "%" Current buffer's file path. "cfile"<cfile>text under the cursor. "cwd" Neovim's current working directory. "git" Nearest Git root (nearest ancestor of the cwd with a.git). "path=<path>" Literal path (supports file completion afterpath=). <keyword> Named scope alias — see |open-keywords|. <anything> Any other text is used verbatim as the resolved path/URL. (omitted) Target-aware heuristic: PATH_TARGETS (filemanager/split/vsplit/tab): cfile→disk, fallback → buffer path Other targets: visual selection → cWORD → buffer path Examples using keywords:
:Open split nvim_init open Neovim init.lua in a horizontal split
:Open tab zshrc open ~/.zshrc in a new tab
:Open split pwsh_profile open PowerShell $PROFILE in a split
:Open split gitconfig open ~/.gitconfig in a split
:Open split MY_ROADMAP open a user-defined keyword
:Open filemanager git open the current Git root in the file manager
5.3 Handlers
default
Open in the system default application — equivalent to a double-click. The OS picks the app based on the file extension or URL scheme:.docx→ Word,https://→ default browser, etc. Windows:explorer.exe <path>WSL: converts path viawslpath, then same as Windows; falls back toxdg-openfor Linux-only paths macOS:open <path>Linux:xdg-open <path>(requires xdg-utils)
browser
Open a URL in the system default browser.
Non-URL text is treated as a Google search query.
Local file paths are wrapped in a file:// scheme.
chrome | chromium | firefox | edge | brave | opera | safari
Same asbrowserbut launch a specific named browser.safarionly works on macOS.
filemanager
Open a path in the system file manager. Windows:explorer.exe /select,<path>(reveals the file). WSL:explorer.exeviawslpathconversion. macOS:open -R <file>(Finder reveal) oropen <dir>. Linux: nautilus/nemo/dolphin --select/thunar/caja to reveal a file,xdg-openfor a directory. Withfilemanager.reveal = false, a file target navigates to its parent directory instead of being revealed/selected there. See |open-config|. The dispatch itself lives inlib.nvim.cross.reveal_in_fm, shared with filetree.nvim's<leader>fm. On Windows the launch also brings the Explorer window to the FRONT, via a short PowerShell helper in lib.nvim. Spawning explorer.exe directly does create the window, but Windows grants SetForegroundWindow only to the process that owns the foreground window — running inside a terminal that is the terminal host, not nvim.exe — so the window was created behind everything and:Open filemanagerlooked like it did nothing. Under a GUI Neovim (Neovide, nvim-qt) nvim.exe IS the foreground process and the same code worked, which is why this looked intermittent. See lib.nvim'sreveal_in_fm/README.md.
notepad | editor
Write the context text to a temporary.txtfile and open it in the platform GUI text editor: Windows:notepad.exeWSL:notepad.exe, with the temp path converted viawslpathfirst — a Windows binary cannot resolve a Linux path. macOS:open -e(TextEdit) Linux:xdg-open, then gedit/kate/mousepad/leafpad/pluma/xed
split | vsplit | tab
Open a file path inside the current Neovim session. URL contexts are rejected (usebrowserinstead).split→ horizontal split (split)vsplit→ vertical split (vsplit)tab→ new tab (tabedit)
terminal
Open a terminal split in the target's directory.
URL contexts are rejected. A file target resolves to its parent directory;
a directory target is used as-is.
:Open terminal terminal in the current buffer's directory
:Open terminal cfile terminal in <cfile>'s parent directory
6. KEYWORDS
Named scope aliases let you refer to common config files by a short keyword
instead of typing out a full path. Keywords are valid as the second argument
to :Open and appear in tab-completion.
:Open split nvim_init → opens Neovim init.lua in a split
:Open tab zshrc → opens ~/.zshrc in a new tab
:Open split gitconfig → opens ~/.gitconfig in a split
:Open split pwsh_profile → opens PowerShell $PROFILE in a split
6.1 Built-in keywords
Loaded automatically unless builtin_keywords = false.
Dynamic keywords (marked ✦) are resolved at invocation time.
Shell profiles
pwsh_profile PowerShell $PROFILE (✦ requires pwsh/powershell on PATH) zshrc ~/.zshrc zprofile ~/.zprofile bashrc ~/.bashrc bash_profile ~/.bash_profile profile ~/.profile fish_config ~/.config/fish/config.fish nushell_config ~/.config/nushell/config.nu
Editor / IDE
nvim_init init.lua or init.vim inside the Neovim config dir (✦) vimrc ~/.vimrc
Terminal emulators & multiplexers
tmux_conf ~/.config/tmux/tmux.conf or ~/.tmux.conf (✦) wezterm_conf ~/.config/wezterm/wezterm.lua or ~/.wezterm.lua (✦) kitty_conf ~/.config/kitty/kitty.conf alacritty_conf .toml preferred, .yml fallback (✦) starship_conf ~/.config/starship.toml
Git
gitconfig ~/.gitconfig gitignore_global fromgit config core.excludesFile, then common paths (✦) gitmessage fromgit config commit.template, then ~/.gitmessage (✦)
SSH
ssh_config ~/.ssh/config ssh_known_hosts ~/.ssh/known_hosts ssh_authorized_keys ~/.ssh/authorized_keys
Package managers & runtimes
npmrc ~/.npmrc
yarnrc ~/.yarnrc.yml
cargo_config ~/.cargo/config.toml
pip_conf ~/.config/pip/pip.conf (Unix) (✦)
%APPDATA%\pip\pip.ini (Windows)
gemrc ~/.gemrc
curlrc ~/.curlrc
System / misc
inputrc ~/.inputrc (Readline config) hosts /etc/hosts (Unix) · C:\Windows\System32\drivers\etc\hosts (Win) (✦) docker_config ~/.docker/config.json wsl_conf /etc/wsl.conf (WSL only) wslconfig ~/.wslconfig (Windows only)
6.2 User-defined keywords
Add entries in setup(). User entries override built-in entries with the
same key.
require("open").setup({
keywords = {
-- Static path (~ is expanded at resolution time):
MY_ROADMAP = "E:\\projects\\ROADMAP.md",
-- Override a built-in:
zshrc = "~/dotfiles/.zshrc",
-- Dynamic resolver (function called when the keyword is used):
MY_LOG = function()
return vim.fn.expand("~/logs/") .. os.date("%Y-%m-%d") .. ".md"
end,
},
})
To disable all built-in keywords:
require("open").setup({ builtin_keywords = false })
7. TAB COMPLETION
:Open completes context-sensitively:
:Open <Tab> all registered handler names
:Open browser <Tab> % cfile cwd git path= <keywords> <files>
:Open split zsh<Tab> → zshrc zprofile (keyword prefix match)
:Open filemanager path=<Tab> file/directory completion after path=
The viewer commands complete their own grammar:
:Open viewer <Tab> all urls mdlinks files paths % cwd buffers
:Open viewer urls <Tab> % cwd buffers <files>
:UrlView <Tab> % cwd buffers <files>
:UrlView cwd sort=<Tab> none file kind alpha
:UrlView cwd out=<Tab> picker table clipboard mdlinks csv echo file:
8. THE :Open viewer COMMAND
:Open viewer [kind] [scope] [options]:UrlView [scope] [options]*:UrlView*:MDLinksView [scope] [options]*:MDLinksView* Collect links in a scope, then pick one to open — or export the whole list.:UrlViewand:MDLinksVieware shallow wrappers that pin the kind, so their first argument is the SCOPE, not a kind. Rename or disable them with theviewer.commandssetting (see |open-config|). This replaces the former urlview.nvim dependency (see |open-integrations-urlview|).
:UrlView URLs in the current buffer -> picker
:MDLinksView cwd markdown links under the cwd
:Open viewer every link in the current buffer
:Open viewer urls cwd same as `:UrlView cwd`
:'<,'>UrlView URLs in the visual selection only
Recognized links: bare URLs (https://...,ftp://...,www....), markdown inline links (reported by target, with the label kept), and — with--paths— filesystem paths that exist on disk. Links inside fenced code blocks are skipped, a URL already inside a markdown link is not reported twice, and bare in-document anchors ([Context](#context)) are dropped unless--anchorsis given. Relative markdown targets are resolved against the directory of the file they were found in, so[x](../../lua/init.lua)inside a nested document is openable from anywhere. Note that "viewer" is a reserved handler key::Open viewermatches the literal subcommand before the flat:Open [target]grammar sees it, so a handler registered under that key would be unreachable.
8.1 Kind
The first argument of:Open viewerselects which links to keep. all (default) everything urls links whose TARGET is a URL, including [text](https://...) mdlinks links written with markdown SYNTAX, whatever they point at files links whose target is a local file or directory paths bare filesystem paths (requires --paths)urlsandmdlinksdeliberately overlap:urlsasks "can a browser open this?",mdlinksasks "was this written with brackets?". A[docs](https://x.dev)is in both. That split is what makes:UrlViewmean "things a browser can open" rather than "things without brackets". The kind is optional::Open viewer cwdis read as "all kinds, cwd scope", becausecwddoes not name a kind.
8.2 Scope
(omitted) / % Current buffer cwd Every file under|getcwd()|, recursively buffers Every listed, loaded buffer <path> A file, or a directory (recursively) (a range):'<,'>UrlViewor:10,20UrlViewscans only those lines Directory scans skip the conventional junk (.git, node_modules, ...) via lib.nvim's shared ignore list, and skip binary and oversized files.
8.3 Options
sort={how} Ordering: none (default) | file | kind | alpha
out={where} Output sink; see |open-viewer-outputs|
match={pat} Only scan files whose basename matches this Lua pattern,
e.g. match=%.md$
--paths Also report filesystem paths, not just URLs. Only paths
that exist on disk are reported.
--anchors Include bare in-document anchors, dropped by default
--dupes Keep duplicate targets (the default de-duplicates)
--flat Do not recurse into subdirectories
Flags and key=value options may appear in any order, before or after the
positional arguments.
8.4 The picker
The results list is ui.nvim'sui.kit.chooser, which means: - the WHOLE current line is highlighted (CursorLine:KitSelection) - the cursor moves only up and down — j/k and the arrow keys; h, l, 0, $, w, b and other horizontal motions are mapped to <Nop> - <CR> opens the entry, <Esc> or q closes <CR> is kind-aware: a URL opens in your browser, via thedefault_browserhandler a local file opens in a Neovim split (seeviewer.open_file) a directory opens in the system file manager Following a markdown link therefore lands you in an editable buffer, not in your file manager. Afile.md#headingtarget jumps to that heading after opening. Columns are aligned across the whole result set and shortened to fit the window: local targets are shown relative to the cwd, and long paths are elided in the middle rather than pushing the target off the right edge.
8.5 Outputs
picker Interactive list; see |open-viewer-picker| table GFM table (Kind / Location / Text / Target) in a scratch buffer csv The same columns as CSV, in a scratch buffer mdlinks[label](target)per line, copied to the clipboard clipboard The rendered table, copied to the clipboard echo Printed to the message area file:{path} The rendered table, written to {path}mdlinksreuses an existing markdown label when there is one, and otherwise labels a URL with its host and a path with its basename —[](...)would render as an invisible link.
:UrlView cwd sort=file out=table a table of every URL in the project
:MDLinksView cwd every markdown link, as a picker
:Open viewer files cwd --paths local targets, incl. bare paths
:UrlView cwd match=%.md$ out=mdlinks doc URLs as markdown, to clipboard
:UrlView % out=file:/tmp/links.md write this buffer's URLs to a file
9. LUA API
After callingsetup(), the following functions are available: *open.open()*require("open").open([target [, scope]])Open programmatically with an optional explicit handler and scope. Behaves identically to the:Opencommand. Scope may be a keyword.targetString handler key, or nil for the context-aware default.scopeScope token: "%", "cfile", "cwd", "git", "path=<path>", keyword, or literal. Examples:
require("open").open()
require("open").open("browser")
require("open").open("filemanager", "%")
require("open").open("split", "cfile")
require("open").open("split", "nvim_init")
require("open").open("tab", "pwsh_profile")
*open.context.with_cache()*require("open.context").with_cache(fn)For extension authors building onopen.contextdirectly (e.g. a picker that resolves several candidate handlers against the same context): memoizescontext.gather()'s result for the duration offn, so nestedgather()/resolve()calls reuse one read of editor state instead of re-reading it.:Openitself already runs through this.
10. INTEGRATIONS
10.1 urlview.nvim
https://github.com/axieax/urlview.nvim lists URLs found in the current buffer and lets you pick one.open.integrations.urlviewregisters a custom urlview action, "open_in_browser", that routes the picked URL through open.nvim's own registry/handler dispatch (thedefault_browserhandler — see |open-config|) instead of duplicating cross-platform browser-launch logic in a second place. lazy.nvim:
{
"axieax/urlview.nvim",
lazy = true,
cmd = { "UrlView" },
dependencies = { "StefanBartl/open.nvim" },
config = function()
require("open.integrations.urlview").setup()
end,
}
*open.integrations.urlview*require("open.integrations.urlview").setup([opts])Registers the "open_in_browser" action withurlview.actions, then callsurlview.setup(opts)withdefault_action = "open_in_browser"(anddefault_pickerset to telescope/fzf-lua if available) unless already set inopts. Passfalseinstead of a table to only register the action without callingurlview.setup()yourself.
10.2 telescope.nvim
Opt-in telescope.nvim source listing every registered handler, with a live preview of what it would open for the current context, and dispatching whichever one is picked — the same effect as:Open {key}, but browsable and previewable first. Not loaded byopen.setup().
require("open.integrations.telescope").picker()
*open.integrations.telescope.picker()*require("open.integrations.telescope").picker([opts])Opens the picker.optsis forwarded to telescope'spickers.new(). *open.integrations.telescope.extension()*require("open.integrations.telescope").extension()Returns a table shaped fortelescope.register_extension():
require("telescope").register_extension(
require("open.integrations.telescope").extension()
)
-- later, anywhere:
require("telescope").extensions.open.open()
11. HEALTH CHECK
:checkhealth open
Reports: core Neovim version, vim.system availability. lib.nvim lib.nvim.notify and lib.nvim.bindings.usercmd.composer presence. platform Detected OS (Windows / WSL / macOS / Linux). executables Per-platform tool availability. office_open Auto-redirect status and configured extensions. handlers All registered handlers with descriptions.
12. ARCHITECTURE
plugin/open.lua Load guard (vim.g.loaded_open)
lua/open/
init.lua setup() — config, handler loading, :Open command
config/
DEFAULTS.lua default configuration values
init.lua setup() merge + M.get() / M.is_debug()
registry.lua handler register / get / list
context.lua gather() + resolve() — two-stage resolution;
with_cache() memoizes gather() per invocation
platform.lua OS detection, cached per session
util.lua run_detached(), url_encode(), find_exec()
keywords.lua built-in named scope keyword table
picker.lua opt-in handler-choice picker (ui.kit)
office_open.lua BufReadCmd redirect for MS Office documents
health.lua :checkhealth open
@types/init.lua LuaLS type definitions
bindings/
usrcmds.lua :Open — built on lib.nvim.bindings.usercmd.composer
keymaps.lua optional keymaps declared via cfg.keymaps
handlers/
default.lua system default application (like a double-click)
filemanager.lua Explorer / Finder / xdg-open
browser.lua browser + chrome/chromium/firefox/edge/safari
notepad.lua notepad/editor (temp-file → GUI editor)
nvim_internal.lua split / vsplit / tab
terminal.lua terminal split in the target's directory
image.lua inline image render (images.nvim, with fallback)
viewer/
init.lua :Open viewer — filter/sort/format/open
scan.lua link extraction (URLs, md links, paths)
integrations/
urlview.lua urlview.nvim "open_in_browser" action (opt-in)
telescope.lua telescope.nvim handler-picker source (opt-in)
menu.lua nvzone/menu context-aware entries (opt-in)
Each leaf module exposes only plain functions or a register_all(fn) entry
point; init.lua is the only place that registers a user command.