A Neovim file picker that learns how you navigate. A neural network trains on your file selections to rank results by what you're most likely to open next, combining fuzzy matching with contextual signals like buffer state, directory proximity, frecency, and file-to-file transition patterns.
Inspired by smart-open.nvim, built for Snacks.nvim.
- Neural Network Ranking: An MLP trains online from your selections using pairwise hinge loss, learning to rank files by relative preference
- Pre-trained Defaults: Ships with default weights trained on over 16k samples so ranking is useful from the first launch
- 11 Scoring Features: Fuzzy match, virtual name, open/alternate buffer, directory proximity, project scope, frecency, recency, trigram similarity, file transition frecency, and not-current file indicator
- Self-Learning: Adapts to your navigation patterns over time and persists learned weights across sessions
- Score Preview: Enable
debug.previewto watch the model's score breakdowns and training in real time - Multiple Algorithms: Neural network (default), classic weighted sum based on smart-open.nvim, or naive baseline
- Fast Performance: Leverages Snacks.nvim's async file discovery and picker infrastructure
Note
Neural Open ships with pre-trained default weights so it's useful immediately, but the network is trained on one person's workflow. Give it a couple of days of normal use and it will start adapting to your specific navigation patterns.
Here's the debug preview showing the neural network's score breakdown and training metrics as you navigate:
- Neovim >= 0.11.0
- Snacks.nvim installed and configured
Using lazy.nvim:
{
"dtormoen/neural-open.nvim",
dependencies = {
"folke/snacks.nvim",
},
-- NeuralOpen implements lazy loading internally. It needs to be loaded for recency tracking to work.
lazy=false;
keys = {
{ "<leader><leader>", "<Plug>(NeuralOpen)", desc = "Neural Open Files" },
},
-- opts are optional. NeuralOpen will automatically use the defaults below.
opts = {},
}require("neural-open").setup({
-- Scoring algorithm: "nn" (neural network), "classic" (weighted features), or "naive"
algorithm = "nn",
-- Algorithm-specific configurations
algorithm_config = {
-- Neural network algorithm settings (default)
nn = {
architecture = { 11, 16, 16, 8, 1 }, -- Input → Hidden1 → Hidden2 → Hidden3 → Output
optimizer = "adamw", -- "sgd" or "adamw"
learning_rate = 0.001, -- Learning rate for gradient descent
batch_size = 128, -- Number of samples per training batch
history_size = 2000, -- Maximum stored historical selections
batches_per_update = 5, -- Number of batches per weight update
weight_decay = 0.0001, -- L2 regularization to prevent overfitting
layer_decay_multipliers = nil, -- Optional per-layer decay rates
dropout_rates = { 0, 0.25, 0.25 }, -- Dropout rates for hidden layers
warmup_steps = 10, -- Learning rate warmup steps (recommended for AdamW)
warmup_start_factor = 0.1, -- Start at 10% of learning rate
adam_beta1 = 0.9, -- AdamW first moment decay
adam_beta2 = 0.999, -- AdamW second moment decay
adam_epsilon = 1e-8, -- AdamW numerical stability
match_dropout = 0.25, -- Dropout rate for match/virtual_name during training
margin = 1.0, -- Margin for pairwise hinge loss
},
-- Classic algorithm settings (weighted feature scoring)
classic = {
learning_rate = 0.6, -- Learning rate for weight adjustments (0.0 to 1.0)
default_weights = {
match = 140, -- Snacks fuzzy matching
virtual_name = 131, -- Virtual name matching
open = 3, -- Open buffer bonus
alt = 4, -- Alternate buffer bonus
proximity = 13, -- Directory proximity
project = 10, -- Project (cwd) bonus
frecency = 17, -- Frecency score
recency = 9, -- Recency score
trigram = 10, -- Trigram similarity
transition = 5, -- File transition tracking
not_current = 5, -- Not-current-file indicator
},
},
naive = {
-- No configuration needed
},
},
-- Directory for all picker weight files
weights_dir = vim.fn.stdpath("data") .. "/neural-open/",
-- Deprecated: use weights_dir instead. If set, dirname is used as fallback weights_dir
-- weights_path = vim.fn.stdpath("data") .. "/neural-open/files.json",
-- Maximum number of files in persistent recency list
recency_list_size = 100,
-- Debug settings
debug = {
preview = false, -- Show detailed score breakdown in preview
snacks_scores = false, -- Show Snacks.nvim debug scores in picker
latency = false, -- Enable detailed latency tracking
latency_file = nil, -- Optional file path for persistent latency logging
latency_threshold_ms = 100, -- Only log operations exceeding this duration
latency_auto_clipboard = false, -- Copy timing report to clipboard
},
-- File sources for the default file picker
file_sources = { "buffers", "neural_recent", "files", "git_files" },
-- Algorithm config for item pickers (8-feature pipeline, separate from file pickers)
item_algorithm_config = {
classic = {
learning_rate = 0.6,
default_weights = {
match = 140, frecency = 17, cwd_frecency = 15,
recency = 9, cwd_recency = 8, text_length_inv = 3,
not_last_selected = 2, transition = 5,
},
},
nn = {
architecture = { 8, 16, 8, 1 }, -- 8 inputs for item features
optimizer = "adamw",
learning_rate = 0.001,
batch_size = 128,
history_size = 2000,
batches_per_update = 5,
},
},
-- Special files that include parent directory in virtual name
special_files = {
["__init__.py"] = true,
["index.js"] = true,
["index.jsx"] = true,
["index.ts"] = true,
["index.tsx"] = true,
["init.lua"] = true,
["init.vim"] = true,
["mod.rs"] = true,
},
})-- Via command
:NeuralOpen
-- Via Lua
require("neural-open").open()
-- With custom options
require("neural-open").open({
cwd = "/path/to/project",
prompt = "Neural Open> ",
})
-- Using Snacks.nvim directly
require("snacks").picker.pick("neural_open")Change the scoring algorithm at runtime:
" Show current algorithm
:NeuralOpen algorithm
" Switch to neural network algorithm (default)
:NeuralOpen algorithm nn
" Switch to classic weighted algorithm
:NeuralOpen algorithm classic
" Switch to naive algorithm
:NeuralOpen algorithm naiveCreate pickers for any string-based selection use case. Each picker trains an independent neural network.
-- Open a picker inline (registers and opens in one call)
require("neural-open").pick("just_recipes", {
title = "Just Recipes",
finder = function()
return {
{ text = "build", desc = "Build the project", value = "build" },
{ text = "test", desc = "Run tests", value = "test" },
}
end,
confirm = function(picker, item)
picker:close()
vim.cmd(string.format('TermExec cmd="just %s"', item.value))
end,
})
-- Or register once and open later
require("neural-open").register_picker("just_recipes", { ... })
require("neural-open").pick("just_recipes")
-- Open via command
:NeuralOpen pick just_recipesSee the examples/ directory for complete, copy-paste picker templates (just recipes, make targets, vim commands).
If the learned weights aren't producing good results, reset them:
require("neural-open").reset_weights()Or via command:
" Reset current algorithm's weights
:NeuralOpen reset
" Reset specific algorithm's weights
:NeuralOpen reset nn
:NeuralOpen reset classicFor each file in the picker, the plugin computes a set of normalized features capturing context like fuzzy match quality, buffer state, directory proximity, and usage history. These features are fed into one of three scoring algorithms to produce a final ranking. All algorithms learn from your file selections and persist their parameters to disk.
Each file receives a score based on 11 features, all normalized to [0,1]:
- Match: Fuzzy path matching score from Snacks.nvim's matcher, normalized with a sigmoid
- Virtual Name: Fuzzy match against a virtual name that includes the parent directory for index/init files (e.g.,
components/index.jsmatches "components"), normalized with a sigmoid - Open: Binary bonus for files currently open in a buffer
- Alternate: Binary bonus for the alternate buffer (
#file) - Proximity: Ratio of shared directory depth between the current file and the candidate, where same-directory files score 1.0
- Project: Binary bonus for files under the current working directory
- Frecency: Frequency + recency score from Snacks.nvim's built-in frecency tracking, normalized with
1 - 1/(1 + x/8) - Recency: Position in a persistent most-recently-accessed list (updated on BufEnter), scored with linear decay:
(max - rank + 1) / max - Trigram: Character-level similarity between the candidate's virtual name and the current filename using Dice coefficient over 3-character trigram sets
- Transition: Learned file-to-file navigation patterns using frecency with exponential decay (30-day half-life), scored with
1 - 1/(1 + score/4) - Not Current: Binary indicator (1.0 if the candidate is NOT the currently open file, 0.0 if it is), allowing the model to learn to deprioritize the current file in rankings
A multi-layer perceptron that takes the 11 normalized features as input and outputs a ranking score. Trained online using pairwise hinge loss: when you select a file, the network learns from (selected, non-selected) pairs constructed from the top-ranked candidates. Uses batch normalization and Leaky ReLU activations during training; at inference time, batch normalization is fused into the weight matrices so scoring runs with zero allocations per keystroke. Match/virtual_name features are randomly dropped during training to force the network to learn from contextual features (frecency, proximity, etc.), improving ranking before any query is typed. Supports AdamW (default) and SGD optimizers with optional learning rate warmup.
Based on smart-open.nvim's ranking approach, adapted for Snacks.nvim, and extended with trigram and transition features. Computes a weighted sum of the normalized features. When you select a file that wasn't ranked #1, the algorithm compares feature values between your selection and higher-ranked items and adjusts the weights using a configurable learning rate.
Simple unweighted sum of all normalized features. No learning. Useful for testing and as a baseline.
setup(opts)- Initialize the plugin with configurationopen(opts)- Open the neural file pickerpick(name, opts)- Open a custom picker with neural scoring (registers inline if not previously registered)register_picker(name, config)- Register a picker configuration for later use withpick()reset_weights(algorithm_name?)- Reset learned weights to defaults (optional algorithm name)set_algorithm(name?)- Set or display current algorithm ("classic", "naive", "nn")
<Plug>(NeuralOpen)- Open the neural picker
:NeuralOpen- Open the neural file picker:NeuralOpen pick <name>- Open a registered custom picker:NeuralOpen algorithm [name]- Show or set scoring algorithm:NeuralOpen reset [algorithm]- Reset weights for current or specified algorithm
The plugin registers two sources in Snacks.nvim:
neural_open— the main file picker with neural scoringneural_recent— combines neural-open's BufEnter-based recency list with Snacks' frecency database, replacing the built-inrecentsource for better file candidate discovery
require("snacks").picker.sources.neural_open
require("snacks").picker.sources.neural_recent# Install dependencies
just setup
# Run tests (isolated from your real Neovim environment)
just testTests run in complete isolation using temporary XDG directories to protect your real configuration.
# Run scorer hot-path benchmark (static features, per-keystroke scoring, transform phase across 1K/10K/100K files)
just benchmarkResults are documented in docs/benchmark-results.md.
Contributions are welcome! Please feel free to submit issues and pull requests.
MIT License - See LICENSE file for details
- smart-open.nvim - Original implementation and reference for many scoring features and the classic algorithm.
- Snacks.nvim - Modern and extensible fuzzy-finder.
