https://github.com/beyondlex/finder
macOS Finder-style "Go to Folder" path browser for Neovim. Provides an interactive floating-window UI for selecting file or directory paths via fuzzy matching.
https://github.com/beyondlex/finder
file-browser file-picker finder fuzzy-matching neovim path save-dir
Last synced: about 1 month ago
JSON representation
macOS Finder-style "Go to Folder" path browser for Neovim. Provides an interactive floating-window UI for selecting file or directory paths via fuzzy matching.
- Host: GitHub
- URL: https://github.com/beyondlex/finder
- Owner: beyondlex
- Created: 2026-06-15T08:58:10.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-15T10:03:36.000Z (about 1 month ago)
- Last Synced: 2026-06-15T11:45:07.038Z (about 1 month ago)
- Topics: file-browser, file-picker, finder, fuzzy-matching, neovim, path, save-dir
- Language: Lua
- Homepage:
- Size: 30.3 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# finder.nvim
[English](./README.md) | [中文](./README.zh-CN.md)
---
macOS Finder-style "Go to Folder" path browser for Neovim. Provides an interactive
floating-window UI for selecting file or directory paths via fuzzy matching.
## Features
- **Three modes** — `dir` / `file` / `both` (filter by type)
- **Extension filter** — in `file` mode, restrict results to specific extensions (e.g. only `csv`, `tsv`, `json`)
- **Interactive input** — as you type, the result list updates in real-time
- **Smart mode switching** — trailing `/` lists directory contents; otherwise
fuzzy-matches items under the parent directory
- **Auto-listing** — paths without any `/` (e.g. `~`, `foo`) that resolve to a
directory auto-list its contents
- **Fuzzy matching** — first character prefix filter (case-insensitive) +
`matchfuzzypos` with lowercased query for subsequent characters
- **`*` prefix substring match** — prefix with `*` to match anywhere in the name
(e.g. `*abc` finds items containing `abc` anywhere). Results are still
fuzzy-ordered and highlighted.
- **Match highlighting** — matched characters highlighted only in the item name,
not in the parent path prefix
- **Tab completion** — completes the selected item's name into the input;
virtual text shows what Tab will complete
- **Parent navigation** — `` / `Cmd+↑` strips the last path segment
- **Isolated test environment** — `test/run.sh` launches via `NVIM_APPNAME`
without touching your system Neovim
## Installation
```lua
-- lazy.nvim
{
"beyondlex/finder",
config = function()
-- :Finder commands auto-registered
end,
}
```
## Commands
| Command | Mode | Description |
|---------|------|-------------|
| `:Finder ~/Downloads` | dir | Browse directories |
| `:FinderDir ~/Downloads` | dir | Alias for `:Finder` |
| `:FinderFile ~/Downloads` | file | Browse files |
| `:FinderBoth ~/Downloads` | both | Browse files + directories |
`--ext ` limits results to the given extensions (comma-separated, no dots):
| Example | Description |
|---------|-------------|
| `:FinderFile --ext csv,tsv,json ~/data` | Only `.csv`, `.tsv`, `.json` files |
| `:FinderFile --ext lua` | Only `.lua` files |
Arguments are optional. `` completion for paths (`complete=dir/file`).
## Lua API
```lua
local finder = require("finder")
finder.open({
mode = "dir", -- "dir" | "file" | "both"
initial_path = "~", -- starting path
extensions = {"csv", "tsv", "json"}, -- optional: only show files with these extensions (file mode)
on_confirm = function(path)
print("Selected: " .. path)
end,
on_cancel = function()
print("Cancelled")
end,
})
```
## Keymaps
| Key | Mode | Action |
|-----|------|--------|
| `` | Insert / Normal | Complete selected item into path |
| `` | Insert / Normal | Confirm selection |
| `` / `` | Insert / Normal | Cancel |
| `` / `` | Insert | Select previous / next item |
| `k` / `j` | Normal | Select previous / next item |
| `` / `Cmd+↑` | Insert | Go to parent directory |
## How It Works
### Path Mode Switching
The UI operates in two modes, determined by the current input:
- **Listing mode** — activated when the input ends with `/` (e.g. `~/ai/`) or
has no `/` at all and resolves to a directory (e.g. `~`, `foo`). Lists all
items in the expanded directory. The first result is always a **self-item**
representing the directory itself.
- **Matching mode** — activated when the input contains `/` but does not end
with it (e.g. `~/ai`, `~/do`). Extracts the parent directory (everything
before the last `/`) and fuzzy-matches items under it with the trailing text.
> `~/ai` keeps matching mode even when `~/ai` is a directory — it only lists
> if you add the trailing `/` to get `~/ai/`.
### Fuzzy Matching
1. **First-character filter** — items must start with the same character as the
query (case-insensitive). `Ai` only matches names starting with `a` or `A`.
2. **Subsequent fuzzy** — uses `vim.fn.matchfuzzypos` with the query lowered
to avoid case-sensitivity issues.
3. **Single char** — skips `matchfuzzypos` entirely, just returns the
first-char filtered results.
Each result carries `match_positions` (0-indexed byte offsets from
`matchfuzzypos`) for highlighting.
### Display & Match Highlighting
Results are displayed as `parent_display + item_name`. The `parent_display`
varies by mode:
| Mode | parent_display | Example |
|------|---------------|---------|
| Listing | effective path (trailing `/`) | `~/ai/` |
| Matching | everything before last `/` | `~/` |
| Auto-listing (no slash) | path with `/` appended | `~/` |
A `display_offset` (equal to `#parent_display`) is stored per item so that
match highlights are applied only to the item name portion, not the prefix.
### Tab Completion
Uses `self._parent_display` (computed and cached during `refresh()`) as the
prefix. Builds the new path as `prefix .. item.name`, appending `/` for
directories.
| Input | Result | Mode |
|-------|--------|------|
| `~/ai` + Tab | `~/ai/` | matching |
| `~` + Tab | `~/ai/` | auto-listing |
| `~/ai/` + Tab | `~/ai/projects/` | listing |
| `ai` + Tab | `some_dir/` | no-slash matching |
### Go to Parent (`` / `Cmd+↑`)
Strips the last path segment and ensures a trailing `/`:
| Input | Result |
|-------|--------|
| `~/ai/projects/` | `~/ai/` |
| `~/` | `/` |
| `~` | `/` |
| `/` | no-op |
### Confirm & Cancel
- **Enter** — passes the selected item's display path (without trailing `/`)
to `on_confirm(path)` and closes the UI. Does nothing when the result list
is empty.
- **Esc** / **Ctrl-C** — calls `on_cancel()` and closes the UI.
### UI Layout
```
┌─ Go to Path ────────────────┐
│ ~/ai/projects/cu │ ← Input (width × 1)
├─────────────────────────────┤
│ ~/ai/projects │ ← Self-item (selected)
│ ~/ai/projects/cursor/ │
│ ~/ai/projects/curl/ │ ← Results (width × ≤12)
│ ~/ai/projects/custom/ │
└─────────────────────────────┘
```
Centered in the editor, `minimal` style with rounded borders.
## File Structure
```
lua/finder/
├── init.lua # Entry point + Finder class
├── ui.lua # Floating window UI (input, results, virtual text)
├── fs.lua # Filesystem operations (expand, parent, list, ...)
└── matcher.lua # Fuzzy matching (first-char filter + matchfuzzypos)
plugin/
└── finder.lua # :Finder/:FinderDir/:FinderFile/:FinderBoth commands
test/
├── run.sh # Isolated test environment launcher
├── clean.sh # Delete all test data
├── init.lua # Test config (edit to add/remove plugins)
├── spec.md # Detailed mechanics documentation
├── spec.lua # Automated test suite (85+ tests)
└── README.md # Test environment instructions
```
## Example
See [example/](./example/) for a runnable Neovim config with demo keymaps:
```bash
nvim -u example/init.lua
```
## Development
```bash
# Launch isolated test environment
./test/run.sh
# With plugins (edit test/init.lua first)
./test/run.sh
# Clean and launch
./test/run.sh --clean
# Run automated tests
./test/run.sh --clean -- --headless -l test/spec.lua -c 'qa!'
# Clean all test data
./test/clean.sh
```
## License
MIT