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

recommender.nvim analyzes the current Lua buffer for dotted chains that appear
at or above a configurable frequency threshold and suggests local alias
declarations for them.

Example — if vim.api appears 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 *recommender-requirements*

  * Neovim >= 0.9
  * lib.nvim (StefanBartl/lib.nvim) — required; :Recommender is registered
    via lib.nvim.bindings.usercmd.composer, with no fallback (notify/map
    specifically 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 for analyzer = "treesitter"
    Install with: :TSInstall lua

3. INSTALLATION *recommender-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 *recommender-setup*

Call setup() once:

  require("recommender").setup({
    -- see |recommender-config|
  })
setup() registers, once:

  * The :Recommender user command (see |:Recommender|).
  * Global keymaps when keymaps = 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-merge opts into the active config, which takes effect immediately —
every subsequent :Recommender invocation 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 *recommender-config*

All keys are optional. Unset keys use the defaults shown below.

An unknown key or an invalid analyzer/progress_style/float_layout
value 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 *recommender-config-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). Suggests const %s = %s;.
                  No parser dependency.

  "python"      Regex-based, for Python dotted chains. Suggests a plain
                  %s = %s assignment (Python has no local/const
                  keyword). 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 *recommender-config-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 *recommender-config-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 *recommender-config-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 *recommender-config-keymaps*

  keymaps = true
When true, the following global normal-mode keymaps are installed by
setup():

  <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>lr prefix is automatically labeled as a group — no extra config
needed.

Set keymaps = false to skip all of these and define your own. Full
cheatsheet: doc/BINDINGS.md (repo root: |recommender-architecture|).

5.6 cwd_ignore *recommender-config-cwd-ignore*

  cwd_ignore = {
    ".git", "node_modules", ".venv", "venv", "__pycache__",
    "dist", "build", ".next", "target", ".tox",
  }
Directory names skipped, at any depth, by a cwd/path scope 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 *recommender-config-cwd-max-files*

  cwd_max_files = 500   -- 0 = unbounded
Safety cap on the number of files a cwd/path scope 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 names cwd_max_files as the key to raise.

5.8 float_layout *recommender-config-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.lua reads rendering.stride (3 for
detailed, 1 for compact) instead of a hardcoded line count.

5.9 progress_style *recommender-config-progress-style*

  progress_style = "auto"
    -- "auto" | "notify" | "statusline" | "fidget" | "float" | "kit"
Indicator shown while a cwd/path scope 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"      Plain vim.notify.
  "statusline"  Headless — read lib.nvim.progress.styles.statusline
                  from 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 via
                  ui.kit.

Needs lib.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 — see bindings/usrcmds.lua's module-level scan generation
counter (|recommender-architecture|).

6. COMMAND *:Recommender*

: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 for regex, treesitter, javascript,
  python, perf, buffer, path, cwd, cfile, line, -r,
  --replace, -c, --cwd.

  Built via lib.nvim.bindings.usercmd.composer: a single flat path = {} root
  route (this grammar has no subcommand word) declares -r/--replace and
  -c/--cwd as 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 5 and :Recommender 5 javascript cwd
  dispatch 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 *recommender-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 :Recommender was
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 of config.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 *recommender-replace-mode*

Enabled with -r or --replace. When Enter is pressed in the float:

1. The plugin looks for a :Replace user 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-shot WinClosed autocmd detects when the replace prompt (a
   TelescopePrompt window) closes.
4. After the replace, the local alias = chain declaration is inserted.

If :Replace is 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 *recommender-scopes*

By default :Recommender analyzes only the current buffer. {scope} (see
|:Recommender|) changes what gets scanned before the threshold is applied:
buffer (default), path, cwd, cfile, line. Pressing Enter/A
always inserts into the buffer that was active when :Recommender was
invoked — scope only changes where chains are *counted*, never where the
alias is written.

9.1 cwd / path *recommender-cwd-scope*

cwd scans every file under getcwd(); path runs the identical scan but
rooted at the **current buffer's own directory** instead — matching the
active analyzer's extensions:

  regex / treesitter   *.lua
  javascript             *.js, *.jsx, *.ts, *.tsx
  python                 *.py

Each 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. path requires the
current buffer to have a file path (errors on an unnamed buffer).

-c / --cwd is a backward-compatible flag alias for {scope} = cwd; an
explicit {scope} positional always wins over it.

Directories are skipped by exact name match via config.cwd_ignore (see
|recommender-config-cwd-ignore|), and the scan is capped at
config.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 — with config.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 *recommender-cfile-scope*

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 order gf effectively
relies on. Errors if nothing file-like is under the cursor, or the resolved
file isn't readable.

9.3 line *recommender-line-scope*

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 to 1
for this scope specifically (not config.threshold) unless an explicit
{threshold} token is given.

9.4 Analyzer support *recommender-scope-analyzers*

Only the regex-based analyzers (regex, javascript, python, perf)
support any non-buffer scope. treesitter parses a live Neovim buffer's
syntax tree via vim.treesitter.get_parser(bufnr, "lua"), not raw file
text, so it cannot run over files on disk or a single extracted line;
combining treesitter with path/cwd/cfile/line is a hard error
naming the supported analyzers instead.

10. ANALYZERS *recommender-analyzers*


regex *recommender-analyzer-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 *recommender-analyzer-treesitter*

Uses vim.treesitter.query.parse to find field_expression and
call_expression nodes 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 treesitter warns explicitly rather than reporting
"No suggestions" — the two are different causes and are not shown the same
way. Run :checkhealth recommender to 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 share
vim.api), the alias targets the prefix rather than the full chain.

javascript *recommender-analyzer-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). Suggests
const %s = %s; instead of a local declaration.

python *recommender-analyzer-python*

Regex-based, for Python buffers. Same chain-extraction approach as
"regex". Suggests a plain %s = %s assignment, since Python has no
local/const declaration keyword.

perf *recommender-analyzer-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 than t[#t+1] = v /
                                     t[i] = v (indexed assignment)

  x = x .. y accumulator 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 .. usage

  for _, v in ipairs(t) do         ~2x slower than for 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 like regex
it can misfire inside comments or string literals.

Enter/A insert 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.threshold applies 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 1 to see every
instance regardless of count.

11. LUA API *recommender-api*

After setup(), only one public function is exposed:

                                              *recommender.setup()*
require("recommender").setup([opts])

  Initialize the plugin. Safe to call multiple times: the :Recommender
  command and global keymaps register only on the first call, but opts is
  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 *recommender-health*

  :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 active cwd_ignore/
cwd_max_files settings shared by the cwd/path scopes, the active
float_layout, whether setup() has actually run (:Recommender
registered — not just the plugin file having been sourced), any unknown or
invalid option from the last setup() call, lib.nvim.notify/which-key
detection and lib.nvim.progress availability (soft, cosmetic only — the
async cwd/path scan itself never depends on it, only progress_style's
indicator does), whether global keymaps are enabled, and whether a
:Replace command is available for replace mode.

13. ARCHITECTURE *recommender-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