Skip to content

FluxxField/smart-motion.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

521 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SmartMotion.nvim

   _____                      __  __  ___      __  _                          _
  / ___/____ ___  ____ ______/ /_/  |/  /___  / /_(_)___  ____    ____ _   __(_)___ ___
  \__ \/ __ `__ \/ __ `/ ___/ __/ /|_/ / __ \/ __/ / __ \/ __ \  / __ \ | / / / __ `__ \
 ___/ / / / / / / /_/ / /  / /_/ /  / / /_/ / /_/ / /_/ / / / / / / / / |/ / / / / / / /
/____/_/ /_/ /_/\__,_/_/   \__/__/  /_/\____/\__/_/\____/_/ /_(_)__/ /_/|___/_/_/ /_/ /_/

A motion framework for Neovim. Enable a motion, enable an operator. They compose automatically.

SmartMotion in action


Quick Start

{
  "FluxxField/smart-motion.nvim",
  opts = {
    presets = {
      words = true,        -- w, b, e, ge
      lines = true,        -- j, k
      search = true,       -- s, S, f, F, t, T, ;, ,
      delete = true,       -- d + any motion
      yank = true,         -- y + any motion
      change = true,       -- c + any motion
      treesitter = true,   -- ]], [[, af, if, ac, ic, aa, ia, fn, saa, gS, R
      diagnostics = true,  -- ]d, [d, ]e, [e
      misc = true,         -- . g. g1-g9 gp gP gA-gZ gmd gmy (repeat, history, pins, multi-cursor)
    },
  },
}

Everything is opt-in. Enable only what you want. Disable individual keys within a preset too:

presets = {
  words = { e = false, ge = false },  -- only w and b
  search = { s = false },             -- keep native s (substitute)
}

What Happens When You Use It

Press w. Labels appear on every word ahead of your cursor. Press a label and you're there. No counting, no guessing. You looked at the word, you pressed w, you pressed the label.

Press dw. Same labels appear. Press one and that word is deleted. Press w again instead (dww) and the word under your cursor is deleted. yw, cw, pw all work the same way.

That's the entire mental model. Motion key shows targets. Label picks one. Operator + motion shows targets and acts on your pick.


Why This Is Different

Composable operators with zero explicit mappings

Enable words and delete as presets. Now dw, db, de, dge all work. Enable search and ds, dS, df, dt work too. Enable lines and you get dj, dk. Every motion preset multiplies with every operator preset:

11 composable motions  ×  5 operators  =  55+ compositions
         from 16 keys, zero mappings defined

Unknown keys fall through to native Vim. d$, d0, dG work exactly as expected.

Flow State chains motions without labels

Select a target, then press any motion key within 300ms for instant movement with no labels. Chain different motions: wjbw, all without hints.

Hold w. Labels flash once, then it moves word-by-word like native Vim. That's Flow State.

w  [labels]  f  → jump        (within 300ms)
                  j  → instant   (within 300ms)
                       b  → instant   (within 300ms)
                            w  → instant

Text objects that work with any operator

af, if, ac, ic, aa, ia are real text objects in operator-pending and visual mode. They work with everything, not just d/y/c:

daf   delete a function       gqaf  format a function
yaa   yank an argument        =if   indent a function body
cic   change inside a class   >ac   indent a class

Modify the search mid-selection

Other motion plugins lock you in once labels appear. SmartMotion lets you change the search context without cancelling:

w  [labels appear]  <M-d>  → labels flip to backward words
s  [search, labels] <M-w>  → labels expand to all visible windows
j  [line labels]    <M-e>  → scope doubles, more labels appear

Toggle direction, toggle multi-window, expand scope, flip hint positions — all while labels are on screen. Pipeline-modifying handlers re-run the entire motion pipeline and regenerate labels in place. Fully configurable via selection_keys, and you can register your own handlers.

It's a framework, not just a plugin

Every built-in motion uses the same public API:

require("smart-motion").register_motion("custom_jump", {
  collector = "lines",
  extractor = "words",
  filter = "filter_words_after_cursor",
  visualizer = "hint_start",
  action = "jump_centered",
  map = true,
  modes = { "n", "v" },
})

Collector → Extractor → Modifier → Filter → Visualizer → Selection → Action. Every stage is swappable. Register custom collectors, extractors, filters, actions. Build motions that don't exist yet.


All Presets

Words: w b e ge
Key Mode Description
w n, v, o Jump to start of word after cursor
b n, v, o Jump to start of word before cursor
e n, v, o Jump to end of word after cursor
ge n, v, o Jump to end of word before cursor

Works with any operator in operator-pending mode: >w, gUw, =w

Lines: j k
Key Mode Description
j n, v, o Jump to line after cursor (supports count: 5j)
k n, v, o Jump to line before cursor (supports count: 3k)

Works with any operator: =j, gqj, >j

Search: s S f F t T ; , gs
Key Mode Description
s n, o Live search with labels across all visible text
S n, o Fuzzy search: type partial patterns to match words
f n, o 2-char find forward (inclusive, line-constrained)
F n, o 2-char find backward (inclusive, line-constrained)
t n, o 2-char till forward (exclusive, line-constrained)
T n, o 2-char till backward (exclusive, line-constrained)
; n, v Repeat last f/F/t/T (same direction)
, n, v Repeat last f/F/t/T (reversed direction)
gs n Visual select: pick two targets, enter visual mode

Multi-window: s and S show labels across all visible splits. Label conflict avoidance ensures labels can't be valid search continuations.

f/F/t/T can be made multi-line or multi-window. See Customizing Motions.

Operators: d y c p P + any motion

Press an operator, then any motion key. Labels appear, pick a target, action runs:

Combo What it does
dw Labels words after cursor → pick one → delete it
ds Live search → pick match → delete it
df 2-char find → delete from cursor to target (inclusive)
dt 2-char till → delete from cursor to just before target
dd Delete current line

All work identically with y (yank), c (change), p/P (paste).

Repeat the motion key for the target under cursor: dww = delete this word, yww = yank this word.

Remote operations (cursor stays in place):

Key Description
rdw Remote delete word
rdl Remote delete line
ryw Remote yank word
ryl Remote yank line
Treesitter: ]] [[ ]c [c ]b [b + text objects + saa gS R

Navigation (multi-window):

Key Mode Description
]] n, o Jump to next function
[[ n, o Jump to previous function
]c n, o Jump to next class/struct
[c n, o Jump to previous class/struct
]b n, o Jump to next block/scope
[b n, o Jump to previous block/scope

Text objects (work with ANY operator: daf, gqaf, =if, >ac):

Key Mode Description
af x, o Around function
if x, o Inside function body
ac x, o Around class/struct
ic x, o Inside class/struct body
aa x, o Around argument (includes separator)
ia x, o Inside argument
fn o Function name (dfn, cfn, yfn)

Advanced:

Key Mode Description
saa n Swap two arguments
gS n, x Incremental select (; expand, , shrink)
R n, x, o Search text → pick match → pick syntax scope

Works across Lua, Python, JavaScript, TypeScript, Rust, Go, C, C++, Java, C#, Ruby.

Diagnostics: ]d [d ]e [e
Key Mode Description
]d n, o Jump to next diagnostic
[d n, o Jump to previous diagnostic
]e n, o Jump to next error
[e n, o Jump to previous error

Multi-window. Works with operators: d]d, y]e

Git: ]g [g
Key Mode Description
]g n, o Jump to next git hunk (changed region)
[g n, o Jump to previous git hunk

Works best with gitsigns.nvim. Multi-window.

Quickfix: ]q [q ]l [l
Key Mode Description
]q n, o Jump to next quickfix entry
[q n, o Jump to previous quickfix entry
]l n, o Jump to next location list entry
[l n, o Jump to previous location list entry
Marks: g' gm
Key Mode Description
g' n, o Show labels on all marks, jump to selected
gm n Set mark at labeled target (prompts for mark name)
Misc: repeat, history, pins, global pins, multi-cursor
Key Mode Description
. n Repeat last SmartMotion
g. n History browser with pins, frecency, preview, search, remote actions
g0 n Jump to most recent location
g1-g9 n Jump to pin 1-9
gp n Toggle pin at cursor (up to 9)
gp1-gp9 n Set pin at specific slot
gP n Toggle global pin (cross-project, prompts A-Z)
gA-gZ n Jump to global pin (works from any project)
gmd n Multi-cursor delete: toggle-select, Enter to delete
gmy n Multi-cursor yank: toggle-select, Enter to yank

Pins workflow: gp at main file → gp at test file → gp at config → now g1 = main, g2 = tests, g3 = config.


Operator-Pending Mode

SmartMotion motions work with any Vim operator:

>w    indent to labeled word       gUw   uppercase to labeled word
=j    auto-indent to labeled line  gqj   format to labeled line
>]]   indent to labeled function   zf]]  fold to labeled function

Multi-Window

Search, treesitter, diagnostic, git, quickfix, and mark motions show labels across all visible splits. Select a label in another window and jump there.

Word and line motions stay single-window by default. See Customizing Motions for multi-window overrides.


Customizing Motions

Every motion is Collector -> Extractor -> Filter -> Visualizer -> Action. Swap any part:

presets = {
  search = {
    f = { filter = "filter_words_after_cursor" },  -- make f multiline
    F = { filter = "filter_words_before_cursor" },  -- make F multiline
  },
}

Here's a taste of what you can change with a single override:

I want to... Change this Example override
Make f single-char extractor extractor = "text_search_1_char"
Make f multiline filter filter = "filter_words_after_cursor"
Make f a live search extractor extractor = "live_search"
Jump to camelCase boundaries metadata word_pattern = [[\v(\u\l+|\l+|\u+|\d+)]]
Make word jump bidirectional filter filter = "filter_words_around_cursor"
Delete without moving cursor action action = "remote_delete"
Make any motion cross-window metadata multi_window = true
Auto-jump to closest target filter filter = "first_target"
Match by vim regex instead of TS collector collector = "patterns", patterns = { "\\v\\f+" }
Adapt per filetype metadata filetype_overrides = { gitcommit = { ... } }
Customize labels for a motion keys keys = "fdsarewq"
Exclude keys from labels exclude_keys exclude_keys = "jk"

See the Recipes guide for 20+ practical examples, or Advanced Recipes for treesitter motions, custom text objects, and composable operators.


What You're Replacing

With all presets enabled, SmartMotion consolidates:

flash.nvim          →  search, treesitter, labels
harpoon             →  pins (g1-g9, gp) + history (g.)
nvim-treesitter-    →  af/if/ac/ic/aa/ia text objects
  textobjects
mini.ai             →  around/inside objects

One plugin, one config. Your pins know about your history. Your text objects work with flow state. Your operators compose with motions you haven't thought of yet.


Honest Comparison

Feature matrix vs hop, leap, flash
Feature hop leap flash SmartMotion
Word/line jumping yes yes yes yes
2-char search yes yes yes
Live incremental search yes yes
Fuzzy search yes
Treesitter navigation yes yes
Treesitter text objects yes
Composable d/y/c/p partial full
Remote operations yes yes
Multi-window via plugin yes yes
Operator-pending mode yes yes yes yes
Label conflict avoidance yes
Flow state chaining yes
Multi-cursor selection yes
Argument swap yes
Visual range selection yes
Motion history + pins yes
Global cross-project pins yes
Extensible pipeline yes
Build custom motions limited limited limited full

When to choose something else

leap.nvim: The 2-character search UX is beautifully refined. If that's your primary motion pattern, leap's polish is hard to beat.

flash.nvim: The most feature-complete alternative. Excellent treesitter integration, large community. If you're happy with flash, it's a great plugin.

hop.nvim: Simpler and battle-tested. If you just need word/line jumping with hints, hop does its job with less surface area.

SmartMotion wouldn't exist without these plugins. See Why SmartMotion for the full story.


Configuration

{
  keys = "fjdksleirughtynm",        -- label characters, home row first
  flow_state_timeout_ms = 300,       -- chaining window, 0 to disable
  dim_background = true,              -- dim non-target text
  auto_select_target = false,        -- auto-jump on single target
  native_search = true,              -- labels during / search
  count_behavior = "target",         -- "target" or "native" for j/k counts
  history_max_size = 20,             -- persistent history entries
  open_folds_on_jump = true,         -- open folds at target position
  save_to_jumplist = true,           -- save position to jumplist before jumping (j/k excluded to match native vim)
  max_pins = 9,                      -- maximum pin slots
  search_timeout_ms = 500,           -- auto-proceed after typing in search
  search_idle_timeout_ms = 2000,     -- exit search with no input
  yank_highlight_duration = 150,     -- yank flash duration (ms)
  history_max_age_days = 30,         -- prune history entries older than this
  selection_keys = {                  -- key-action map during label selection
    ["<CR>"] = "select_first",       -- Enter picks the first target
  },
}

Selection Keys

During label selection, special keys can trigger actions instead of picking a label. Only <CR>select_first is enabled by default. Enable the others to modify the search mid-selection:

selection_keys = {
    ["<CR>"]   = "select_first",         -- Enter picks the first target
    ["<M-h>"]  = "toggle_hint_position", -- Flip hints between start/end of targets
    ["<M-d>"]  = "toggle_direction",     -- Flip search direction (forward/backward)
    ["<M-w>"]  = "toggle_multi_window",  -- Toggle single/multi-window
    ["<M-e>"]  = "expand_search_scope",  -- Double the search scope
}

Disable all selection keys with selection_keys = false. Remap to different keys by changing the key string. See Configuration: Selection Keys for custom handlers.

See Configuration for the full reference.


Documentation


License

GPL-3.0

Built by FluxxField

About

Label-based motion framework for Neovim — jump to words, lines, search matches, treesitter nodes, and more with composable operators (d/y/c/p) and multi-window support.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages