recommender.nvim · Debug & inspect · vimdoc
:help recommender
Lua alias suggester for Neovim.
doc/recommender.txt — rendered from the plugin's own vimdoc
*recommender.txt* Lua alias suggester for Neovim. *recommender.nvim* recommender
CONTENTS
1. Introduction .............. |recommender-intro| 2. Requirements .............. |recommender-requirements| 3. Installation .............. |recommender-installation| 4. Setup ..................... |recommender-setup| 5. Configuration ............. |recommender-config| 5.1 analyzer .............. |recommender-config-analyzer| 5.2 threshold ............. |recommender-config-threshold| 5.3 custom_aliases ........ |recommender-config-aliases| 5.4 blacklist ............. |recommender-config-blacklist| 5.5 keymaps ............... |recommender-config-keymaps| 5.6 cwd_ignore ............ |recommender-config-cwd-ignore| 5.7 cwd_max_files ......... |recommender-config-cwd-max-files| 5.8 float_layout .......... |recommender-config-float-layout| 5.9 progress_style ........ |recommender-config-progress-style| 6. Command ................... |:Recommender| 7. Float keymaps ............. |recommender-float-keymaps| 8. Replace mode .............. |recommender-replace-mode| 9. Scopes .................... |recommender-scopes| 10. Analyzers ................. |recommender-analyzers| 11. Lua API ................... |recommender-api| 12. Health-check ............. |recommender-health| 13. Architecture .............. |recommender-architecture|
1. INTRODUCTION
recommender.nvim analyzes the current Lua buffer for dotted chains that appear at or above a configurable frequency threshold and suggestslocalalias declarations for them. Example — ifvim.apiappears 6 times the plugin suggests:
local api = vim.api
Two analysis backends are available: a fast regex scanner and a precise Tree-sitter-based scanner. Results are shown in a floating window with navigation, insert, yank, and batch-insert keymaps.
2. REQUIREMENTS
* Neovim >= 0.9 * lib.nvim (StefanBartl/lib.nvim) — required;:Recommenderis registered vialib.nvim.bindings.usercmd.composer, with no fallback (notify/mapspecifically still degrade to a native fallback if somehow absent at that call site, but the command layer itself does not) * (optional) Lua Tree-sitter parser — only needed foranalyzer = "treesitter"Install with::TSInstall lua
3. INSTALLATION
lazy.nvim:
{
"StefanBartl/recommender.nvim",
dependencies = { "StefanBartl/lib.nvim" },
ft = { "lua" },
cmd = { "Recommender" },
config = function()
require("recommender").setup()
end,
}
packer.nvim:
use {
"StefanBartl/recommender.nvim",
requires = { "StefanBartl/lib.nvim" },
config = function()
require("recommender").setup()
end,
}
vim-plug:
Plug 'StefanBartl/lib.nvim'
Plug 'StefanBartl/recommender.nvim'
lua require("recommender").setup()
4. SETUP
Call setup() once:
require("recommender").setup({
-- see |recommender-config|
})
setup()registers, once: * The:Recommenderuser command (see |:Recommender|). * Global keymaps whenkeymaps = true(the default). Calling it again does not re-register either of those (an explicit repeat call is reported as such rather than silently doing nothing), but it does re-mergeoptsinto the active config, which takes effect immediately — every subsequent:Recommenderinvocation reads analyzer/threshold/ blacklist/float_keymaps/etc. fresh from the same config table. Only the global keymaps' actual key bindings stay as first configured.
5. CONFIGURATION
All keys are optional. Unset keys use the defaults shown below. An unknown key or an invalidanalyzer/progress_style/float_layoutvalue is dropped before the merge rather than silently kept:setup()still succeeds, the default is used for whatever was rejected, and:checkhealth recommender(see |recommender-health|) reports exactly what was dropped and why (with a "did you mean" hint for a near-miss key name).
5.1 analyzer
analyzer = "regex" -- "regex"|"treesitter"|"javascript"|"python"|"perf"
Controls which backend scans the buffer."regex"Fast, pure Lua pattern matching over Lua chains. Works on any buffer, no parser installation required. May produce false positives inside strings or comments."treesitter"Uses the Lua Tree-sitter grammar. More precise — only actual field expressions and call expressions are counted. Requires:TSInstall lua."javascript"Regex-based, for JS/TS dotted chains ($is treated as a valid identifier character). Suggestsconst %s = %s;. No parser dependency."python"Regex-based, for Python dotted chains. Suggests a plain%s = %sassignment (Python has nolocal/constkeyword). No parser dependency."perf"Not a chain counter — flags four benchmarked Lua performance anti-patterns instead. See |recommender-analyzer-perf|. You can override the analyzer per invocation::Recommender treesitter,:Recommender javascript,:Recommender python,:Recommender perf
5.2 threshold
threshold = 3
A chain must appear at least this many times to appear as a suggestion.
Lower values produce more suggestions; higher values reduce noise in large
files. Override per invocation: :Recommender regex 5
5.3 custom_aliases
custom_aliases = {
["vim.api"] = "api",
["vim.fn"] = "fn",
["vim.keymap.set"] = "km_set",
["table.insert"] = "tbl_insert",
["string.format"] = "str_fmt",
-- …
}
Maps a chain to a preferred alias variable name. When a chain has an entry
here, the suggestion reads local <alias> = <chain> instead of the
auto-generated name (last segment of the chain).
The built-in default map covers common Neovim and stdlib chains. Pass your
own table to extend or override it:
custom_aliases = vim.tbl_extend("force",
require("recommender.custom_aliases"),
{ ["my.custom.chain"] = "mcc" }
)
5.4 blacklist
blacklist = {} -- empty by default
A list of chain prefixes that are never suggested. Matching is prefix-based: an entry of"vim.fn"blocks"vim.fn","vim.fn.expand","vim.fn.bufadd", etc.
blacklist = {
"vim.fn", -- block all vim.fn.* chains
"table.insert", -- block exactly table.insert
}
The blacklist is evaluated before the threshold — a chain that appears 100 times but is blacklisted will never appear.
5.5 keymaps
keymaps = true
Whentrue, the following global normal-mode keymaps are installed bysetup():<leader>lr:Recommender<leader>lR:Recommender -r(replace mode)<leader>lrr:Recommender regex<leader>lrt:Recommender treesitter<leader>lrj:Recommender javascript<leader>lrp:Recommender python<leader>lrh:Recommender regex 5(high threshold)<leader>lrc:Recommender -c(project-wide, cwd scope) If which-key (https://github.com/folke/which-key.nvim) is installed, the<leader>lrprefix is automatically labeled as a group — no extra config needed. Setkeymaps = falseto skip all of these and define your own. Full cheatsheet: doc/BINDINGS.md (repo root: |recommender-architecture|).
5.6 cwd_ignore
cwd_ignore = {
".git", "node_modules", ".venv", "venv", "__pycache__",
"dist", "build", ".next", "target", ".tox",
}
Directory names skipped, at any depth, by acwd/pathscope scan (see |recommender-scopes|). Matching is an exact path-segment name comparison, not a pattern — extend the list to skip additional vendor/build directories specific to your projects.
5.7 cwd_max_files
cwd_max_files = 500 -- 0 = unbounded
Safety cap on the number of files acwd/pathscope scan reads. The scan itself is asynchronous (see |recommender-config-progress-style| and |recommender-scopes|) and never blocks the editor regardless of this setting; the cap exists to bound how much of the tree — and how many stale results — one invocation covers on a very large repository/directory. If the cap is hit, a warning namescwd_max_filesas the key to raise.
5.8 float_layout
float_layout = "detailed" -- "detailed" | "compact"
Controls how suggestions are rendered in the float window (see |recommender-float-keymaps|)."detailed"(default) 3 lines per suggestion: chain + hit count, the alias declaration, and a blank separator."compact"1 line per suggestion: → chain.name (N) local alias = chain.name Every float keymap (navigation, insert, yank, ignore, …) works identically in both layouts —float/keymaps.luareadsrendering.stride(3 for detailed, 1 for compact) instead of a hardcoded line count.
5.9 progress_style
progress_style = "auto"
-- "auto" | "notify" | "statusline" | "fidget" | "float" | "kit"
Indicator shown while acwd/pathscope scan runs (see |recommender-scopes|): both the directory walk and the file reads are asynchronous, so this is purely visual, never a blocking wait."auto"(default)vim.notify, or fidget.nvim's LSP-style progress if installed."notify"Plainvim.notify."statusline"Headless — readlib.nvim.progress.styles.statuslinefrom your own statusline component."fidget"Delegates to fidget.nvim."float"Small floating window; focus it and press <Esc> to cancel the scan."kit"Same interaction as"float", themed viaui.kit. Needslib.nvim.progress; a no-op otherwise (the scan still runs, just without an indicator — see |recommender-health|). Cancelling (via the"float"/"kit"styles) or starting a newer scan before this one finishes abandons it — seebindings/usrcmds.lua's module-level scan generation counter (|recommender-architecture|).
6. COMMAND
:Recommender [{flags}] [{analyzer}] [{threshold}] [{scope}]
Open the Recommender float, scoped to the current buffer by default.
Running the command while the float is already open closes it (toggle
behavior).
Arguments (all optional; the three positional slots accept any of
{analyzer}/{threshold}/{scope} in any order — see below):
{flags}
-r / --replace Enable replace mode (see |recommender-replace-mode|).
-c / --cwd Backward-compatible alias for {scope} = cwd
(see |recommender-scopes|); an explicit {scope}
positional always wins over this flag.
{analyzer}
regex Use the Lua regex backend.
treesitter Use the Lua Tree-sitter backend.
javascript Use the JS/TS regex backend.
python Use the Python regex backend.
perf Flag Lua perf anti-patterns instead of chains
(see |recommender-analyzer-perf|).
{threshold}
Any positive integer. Overrides the configured threshold.
{scope}
buffer (default) Analyze only the current buffer.
path Scan every matching file under the current
buffer's own directory.
cwd Scan every matching file under the working
directory.
cfile Scan the single file named under the cursor.
line Scan only the current line.
See |recommender-scopes| for full details on each.
Examples:
:Recommender
:Recommender treesitter
:Recommender regex 5
:Recommender -r treesitter 4
:Recommender javascript
:Recommender python 4
:Recommender perf 1
:Recommender cwd
:Recommender -c
:Recommender path
:Recommender cfile
:Recommender line
:Recommender cwd javascript 5
Tab-completion is available forregex,treesitter,javascript,python,perf,buffer,path,cwd,cfile,line,-r,--replace,-c,--cwd. Built vialib.nvim.bindings.usercmd.composer: a single flatpath = {}root route (this grammar has no subcommand word) declares-r/--replaceand-c/--cwdas short-flag aliases and three optional positional slots. Each positional token is classified by content (a scope name, then an analyzer name, then a number) rather than by which slot it landed in, so:Recommender cwd javascript 5and:Recommender 5 javascript cwddispatch identically. An undeclared--flag(unlike an undeclared-x, which stays a lenient positional) is a hard error instead of being silently treated as a no-op positional value -- and so is a positional token matching none of the three (not a known analyzer/scope name, not a number): it is named in an error rather than silently dropped.
7. FLOAT KEYMAPS
All keymaps are buffer-local to the float window.
Key Action
j / ↓ Move to next suggestion k / ↑ Move to previous suggestion Enter Insert selected alias into the source buffer y Yank selected alias to system clipboard (+and*registers) A Insert ALL visible aliases into the source buffer at once Backspace Ignore this suggestion for the current buffer session U Un-ignore all — restore all dismissed suggestions q / Esc Close the float ? Display keymap reference via vim.notify The "source buffer" is the buffer that was active when:Recommenderwas called. Insertion puts the alias on a new line at the current cursor position in the most recently used normal window. These keymaps behave identically regardless ofconfig.float_layout(see |recommender-config-float-layout|) — the rendered lines differ, but navigation, selection, and every action above work the same in both the default "detailed" layout and the "compact" one.
8. REPLACE MODE
Enabled with-ror--replace. WhenEnteris pressed in the float: 1. The plugin looks for a:Replaceuser command (vim.fn.exists(":Replace") == 2). 2. If found, it runs:Replace <chain> <alias_var> %to substitute all occurrences of the chain in the buffer with the alias name. 3. A one-shotWinClosedautocmd detects when the replace prompt (a TelescopePrompt window) closes. 4. After the replace, thelocal alias = chaindeclaration is inserted. If:Replaceis not available, replace mode falls back to a plain alias insert (same as normal mode). The detection mechanism is event-driven — no polling, no timers, no race conditions. The autocmd is removed immediately after firing.
9. SCOPES
By default:Recommenderanalyzes only the current buffer.{scope}(see |:Recommender|) changes what gets scanned before the threshold is applied:buffer(default),path,cwd,cfile,line. PressingEnter/Aalways inserts into the buffer that was active when:Recommenderwas invoked — scope only changes where chains are *counted*, never where the alias is written.
9.1 cwd / path
cwdscans every file undergetcwd();pathruns the identical scan but rooted at the **current buffer's own directory** instead — matching the active analyzer's extensions:regex/treesitter*.luajavascript*.js,*.jsx,*.ts,*.tsxpython*.pyEach matching file is read from disk (vim.fn.readfile()) and every file's lines are concatenated into one combined line list before the usual count/threshold/alias-format pipeline runs — so a chain's count reflects its occurrences across every scanned file, not just one.pathrequires the current buffer to have a file path (errors on an unnamed buffer).-c/--cwdis a backward-compatible flag alias for{scope}=cwd; an explicit{scope}positional always wins over it. Directories are skipped by exact name match viaconfig.cwd_ignore(see |recommender-config-cwd-ignore|), and the scan is capped atconfig.cwd_max_files(see |recommender-config-cwd-max-files|) files — a warning names the config key to raise if the cap is hit. Both the directory walk and the file reads are asynchronous — the editor never blocks, however large the tree — withconfig.progress_style(see |recommender-config-progress-style|) tracking it. A second scan (another invocation, or an ignore/un-ignore refresh) supersedes whatever is still running rather than racing it to open a float.
9.2 cfile
Scans a single file: whichever file is named under the cursor (<cfile>). Resolution order: as typed, then relative to the current buffer's directory, then via Vim's'path'option — the same ordergfeffectively relies on. Errors if nothing file-like is under the cursor, or the resolved file isn't readable.
9.3 line
Scans only the current line. Each analyzer dedups a chain to at most one hit per line scanned (see |recommender-architecture|), so a 1-line scan maxes out at count 1 per chain — the threshold therefore defaults to1for this scope specifically (notconfig.threshold) unless an explicit{threshold}token is given.
9.4 Analyzer support
Only the regex-based analyzers (regex,javascript,python,perf) support any non-bufferscope.treesitterparses a live Neovim buffer's syntax tree viavim.treesitter.get_parser(bufnr, "lua"), not raw file text, so it cannot run over files on disk or a single extracted line; combiningtreesitterwithpath/cwd/cfile/lineis a hard error naming the supported analyzers instead.
10. ANALYZERS
regex
Scans lines with Lua string patterns. Collects 3-part chains first (vim.api.nvim_*), then 2-part (vim.api). Chains are deduplicated per line. Fast; works on unsaved buffers. May count chains inside string literals or comments.
treesitter
Usesvim.treesitter.query.parseto findfield_expressionandcall_expressionnodes in the Lua grammar. Counts only syntactically valid chains. Ignores string contents and comments automatically. Requires the Lua parser (TSInstall lua). If the Lua parser is missing (or the buffer otherwise cannot be parsed),:Recommender treesitterwarns explicitly rather than reporting "No suggestions" — the two are different causes and are not shown the same way. Run:checkhealth recommenderto check parser availability. Treesitter also applies a "common prefix" heuristic: if multiple chains share a common prefix of depth >= 2 (e.g.,vim.api.nvim_buf_*chains all sharevim.api), the alias targets the prefix rather than the full chain.
javascript
Regex-based, for JavaScript/TypeScript buffers. Same chain-extraction approach as"regex", but the identifier character class also accepts$(a valid character in JS/TS identifiers, e.g. jQuery-style$el). Suggestsconst %s = %s;instead of alocaldeclaration.
python
Regex-based, for Python buffers. Same chain-extraction approach as"regex". Suggests a plain%s = %sassignment, since Python has nolocal/constdeclaration keyword.
perf
Not a chain counter — flags four Lua performance anti-patterns, each backed by an isolated before/after benchmark rather than repetition count. Benchmarking this plugin's own core premise found dotted-chain aliasing has no measurable benefit under LuaJIT (Neovim's runtime hoists the loop-invariant lookup itself); these four patterns are the ones that DID show a real, repeatable win in the same benchmark run:table.insert(t, v)in a loop ~4-5x slower thant[#t+1] = v/t[i] = v(indexed assignment)x = x .. yaccumulator in a O(n^2) vs.table.concat()'s O(n) — loop (self-referential concat) only self-concat (s = s .. chunk) is flagged, not general..usagefor _, v in ipairs(t) do~2x slower thanfor i = 1, #t do; flagged on its own line regardless of nesting (the per-iteration cost doesn't depend on it)string.format(...)in a loop ~3x slower than..concatenation "In a loop" is detected via a lightweight line-based block tracker (for/while/repeat vs. if/function/do) — not a real parser, so likeregexit can misfire inside comments or string literals.Enter/Ainsert the tip as a plain-- perf: ...comment, never an automatic rewrite — the concrete replacement depends on variable names/context this analyzer can't safely infer.config.thresholdapplies here too, but means something different: "how many instances of this pattern exist in the scanned scope", not a repetition-worthiness cutoff. Pass:Recommender perf 1to see every instance regardless of count.
11. LUA API
Aftersetup(), only one public function is exposed: *recommender.setup()*require("recommender").setup([opts])Initialize the plugin. Safe to call multiple times: the:Recommendercommand and global keymaps register only on the first call, butoptsis re-merged into the active config every time and takes effect right away (see |recommender-setup|). Internal modules can be imported directly if needed:
local rendering = require("recommender.rendering")
rendering.close() -- programmatically close the float
local bl = require("recommender.blacklist")
bl.is_blacklisted("vim.fn.expand", { "vim.fn" }) -- true
local regex = require("recommender.analyzers.regex")
local suggestions = regex.analyze(3, {}, {})
12. HEALTH-CHECK
:checkhealth recommender
Reports Neovim version, lib.nvim.bindings.usercmd.composer availability (required — see |recommender-requirements|), Tree-sitter Lua parser availability, javascript/python/perf analyzer availability, the activecwd_ignore/cwd_max_filessettings shared by thecwd/pathscopes, the activefloat_layout, whethersetup()has actually run (:Recommenderregistered — not just the plugin file having been sourced), any unknown or invalid option from the lastsetup()call, lib.nvim.notify/which-key detection and lib.nvim.progress availability (soft, cosmetic only — the asynccwd/pathscan itself never depends on it, onlyprogress_style's indicator does), whether global keymaps are enabled, and whether a:Replacecommand is available for replace mode.
13. ARCHITECTURE
lua/recommender/
init.lua setup() entry point
@types.lua LuaLS type definitions
health.lua :checkhealth recommender
config/
DEFAULTS.lua immutable default configuration
init.lua merge + access to the active config
util/
notify.lua prefixed vim.notify wrapper (via util/lib.lua)
lib.lua soft bridge to lib.nvim (notify/map), fallback
progress.lua soft bridge to lib.nvim.progress (async cwd/path scan)
bindings/
init.lua orchestrates usrcmds/keymaps/autocmds
usrcmds.lua :Recommender command + per-invocation state;
owns the cwd/path scan's cancel/supersede generation
keymaps.lua global keymaps (config.keymaps ~= false); also
carries the which-key group label
autocmds.lua empty (structural symmetry only)
float/
rendering.lua builds ui.kit.select items (layout: detailed/compact), opens the picker
keymaps.lua <CR>-insert handler + extra float keymaps (y/A/<BS>/U/?) on top of kit.select's nav/close
autocmds.lua one-shot WinClosed hook (replace-mode)
blacklist.lua prefix matching + default list
custom_aliases.lua built-in chain → alias name map
project.lua file discovery for cwd/path/cfile scopes;
find_files_async/read_lines_async for cwd/path
analyzers/
regex.lua regex-based scanner (Lua)
treesitter.lua Tree-sitter-based scanner (Lua)
javascript.lua regex-based scanner (JS/TS)
python.lua regex-based scanner (Python)
perf.lua fixed-pattern perf anti-pattern detector (Lua)
plugin/
recommender.lua loaded-guard (vim.g.loaded_recommender)
recommender_autodoc.lua generates doc/tags on first load if missing