_____ __ __ ___ __ _ _
/ ___/____ ___ ____ ______/ /_/ |/ /___ / /_(_)___ ____ ____ _ __(_)___ ___
\__ \/ __ `__ \/ __ `/ ___/ __/ /|_/ / __ \/ __/ / __ \/ __ \ / __ \ | / / / __ `__ \
___/ / / / / / / /_/ / / / /_/ / / / /_/ / /_/ / /_/ / / / / / / / / |/ / / / / / / /
/____/_/ /_/ /_/\__,_/_/ \__/__/ /_/\____/\__/_/\____/_/ /_(_)__/ /_/|___/_/_/ /_/ /_/
A motion framework for Neovim. Enable a motion, enable an operator. They compose automatically.
{
"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)
}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.
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.
Select a target, then press any motion key within 300ms for instant movement with no labels. Chain different motions: w → j → b → w, 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
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
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.
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.
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.
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
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.
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.
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.
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 |
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.
{
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
},
}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.
- Presets Guide: every preset explained in detail
- Recipes: customize built-in motions with practical examples
- Advanced Recipes: treesitter motions, text objects, and composable operators
- Migration Guide: coming from flash, leap, hop, or mini.jump
- Advanced Features: flow state, operator-pending, multi-window, history browser
- Building Custom Motions: create your own with the pipeline API
- Pipeline Architecture: how the framework works internally
- API Reference: full module and motion_state reference
- Configuration: all settings
Built by FluxxField
