You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
misc=true, -- . g. g1-g9 gp gP gA-gZ gmd gmy (repeat, history, pins, multi-cursor)
33
34
},
@@ -63,7 +64,7 @@ presets = {
63
64
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:
@@ -92,6 +93,39 @@ yaa yank an argument =if indent a function body
92
93
cic change inside a class >ac indent a class
93
94
```
94
95
96
+
### Surround with labeled targets
97
+
98
+
SmartMotion replaces nvim-surround with something better: **every pair on screen gets a label.**
99
+
100
+
```
101
+
ds( → labels appear on every () pair → pick one → delimiters deleted
102
+
cs( → labels appear → pick one → delimiters highlight → type replacement
103
+
ysaw( → word labels appear → pick one → word wrapped with ( )
104
+
dsq → labels on all quotes ("/'/`) → pick one → quotes deleted
105
+
dst → labels on all HTML/XML tags → pick one → tags removed
106
+
dsf → labels on all function calls → pick one → unwrapped
107
+
cst → labels on tags → pick one → tag name deleted → insert mode with live sync
108
+
csf → labels on calls → pick one → name deleted → insert mode at name position
109
+
```
110
+
111
+
`di(`, `da{`, `ci"`, `vi[` all show labels on every matching pair visible. No cursor proximity guessing — you pick exactly which pair you mean. `q` matches any quote type. `t` matches HTML/XML tags. `f` targets function calls for surround operations (while `dif`/`daf` still act on function bodies).
112
+
113
+
Opening chars add padding (`ysaw(` → `( word )`), closing chars don't (`ysaw)` → `(word)`). Visual mode `S` wraps a selection. `gza` is a quick "wrap this word" shortcut.
114
+
115
+
### Expand your target with `+` / `-`
116
+
117
+
After picking a target, press `+` to grow the selection forward or `-` to grow backward. Dim hints show you what's expandable:
118
+
119
+
```
120
+
ysaw [word labels] → pick "foo"
121
+
"foo" highlights, dim + on next word, dim - on previous
122
+
+ → range grows to "foo bar"
123
+
+ → range grows to "foo bar baz"
124
+
( → wraps as ( foo bar baz )
125
+
```
126
+
127
+
No other motion plugin does this. `BS` undoes the last expansion. Any non-expansion key (like the delimiter) confirms and executes. Zero friction when you don't need it — just type the delimiter immediately.
128
+
95
129
### Modify the search mid-selection
96
130
97
131
Other motion plugins lock you in once labels appear. SmartMotion lets you change the search context without cancelling:
@@ -181,7 +215,7 @@ Press an operator, then any motion key. Labels appear, pick a target, action run
181
215
| Combo | What it does |
182
216
|-------|-------------|
183
217
|`dw`| Labels words after cursor → pick one → delete it |
184
-
|`ds`| Live search → pick match → delete it |
218
+
|`ds`| Live search → pick match → delete it (without surround preset) |
185
219
|`df`| 2-char find → delete from cursor to target (inclusive) |
186
220
|`dt`| 2-char till → delete from cursor to just before target |
187
221
|`dd`| Delete current line |
@@ -287,6 +321,59 @@ Works best with [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim). Mul
|`ds(`| n | Delete surrounding parens (leaves content) |
355
+
|`cs(`| n | Change surrounding parens (prompts for replacement) |
356
+
|`dsq`| n | Delete nearest surrounding quote (any type) |
357
+
|`csq`| n | Change nearest quote to something else |
358
+
|`dst`| n | Delete surrounding HTML/XML tag |
359
+
|`cst`| n | Change surrounding tag (in-place, live multi-cursor) |
360
+
|`dsf`| n | Unwrap function call: `print(x)` → `x`|
361
+
|`csf`| n | Change function name (in-place insert mode) |
362
+
|`ys`| n | Add surround: `ysaw(` = wrap word with parens |
363
+
|`gza`| n | Quick surround: word hints → pick → type delimiter |
364
+
|`gzp`| n | Paste surround: wrap with previously yanked pair |
365
+
|`S`| x | Surround visual selection (type delimiter to wrap) |
366
+
367
+
**Padding**: opening chars add spaces (`ysaw(` → `( word )`), closing chars don't (`ysaw)` → `(word)`). Configure with `surround_pad`.
368
+
369
+
Unlike native text objects, SmartMotion pair text objects label ALL matching pairs on screen. `di(` shows hints on every `()` pair visible, pick the one you want.
370
+
371
+
**Special surround types**: `t` prompts for a tag name (`ysiw t` → "Tag: " → type `div` → `<div>word</div>`). `f` prompts for a function name. `q` matches all three quote types at once. `cst` and `csf` use in-place editing instead of prompts -- see below.
372
+
373
+
**Target expansion**: After picking a target with `ys` or `gza`, press `+` to grow forward, `-` to grow backward, `BS` to shrink. Wrap multiple words in one go: `ysaw` → pick → `++` → `(` → `( three words here )`.
374
+
375
+
</details>
376
+
290
377
<details>
291
378
<summary><b>Misc</b>: repeat, history, pins, global pins, multi-cursor</summary>
292
379
@@ -369,6 +456,7 @@ With all presets enabled, SmartMotion consolidates:
369
456
370
457
```
371
458
flash.nvim → search, treesitter, labels
459
+
nvim-surround → ds, cs, ys, visual S + pair text objects + tags + function calls
372
460
harpoon → pins (g1-g9, gp) + history (g.)
373
461
nvim-treesitter- → af/if/ac/ic/aa/ia text objects
374
462
textobjects
@@ -392,6 +480,9 @@ One plugin, one config. Your pins know about your history. Your text objects wor
392
480
| Fuzzy search |||| yes |
393
481
| Treesitter navigation ||| yes | yes |
394
482
| Treesitter text objects |||| yes |
483
+
| Surround (ds/cs/ys) |||| yes |
484
+
| Pair text objects (di(/a{) |||| yes |
485
+
| Target expansion (+/-) |||| yes |
395
486
| Composable d/y/c/p ||| partial | full |
396
487
| Remote operations ||| yes | yes |
397
488
| Multi-window || via plugin | yes | yes |
@@ -436,6 +527,12 @@ SmartMotion wouldn't exist without these plugins. See [Why SmartMotion](https://
436
527
max_pins=9, -- maximum pin slots
437
528
search_timeout_ms=500, -- auto-proceed after typing in search
438
529
search_idle_timeout_ms=2000, -- exit search with no input
530
+
surround_pad="opening", -- "opening", "closing", or false
Text objects (`i`/`a` prefix) live in a separate registry from composable motions. The same key can exist in both registries (`f` composable = 2-char find, `f` textobject = function) because they are consulted in different contexts.
Returns the textobject entry for `key`, or `nil` if not registered.
159
+
160
+
### has
161
+
162
+
```lua
163
+
require("smart-motion").textobjects.has(key)
164
+
```
165
+
166
+
Returns `true` if a textobject is registered for `key`.
167
+
168
+
### Entry Fields
169
+
170
+
| Field | Type | Required | Description |
171
+
|-------|------|----------|-------------|
172
+
|`collector`| string | Yes | Collector module name |
173
+
|`extractor`| string | Yes | Extractor module name |
174
+
|`modifier`| string | No | Modifier module name |
175
+
|`filter`| string | No | Filter module name |
176
+
|`visualizer`| string | No | Visualizer module name |
177
+
|`metadata`| table | No | Additional metadata |
178
+
|`inside`| table | No |`motion_state` overrides merged when the `i` prefix is used |
179
+
|`around`| table | No |`motion_state` overrides merged when the `a` prefix is used |
180
+
|`surround`| table | No |`motion_state` overrides merged for surround operators (`ds`, `cs`) |
181
+
|`default`| string | No | Which prefix to use when none is specified (`"inside"` or `"around"`) |
182
+
183
+
The `inside`, `around`, and `surround` fields are plain `motion_state` fragments. When the user types a prefix (`i`, `a`, or a surround operator), the corresponding fragment is merged into `motion_state` before the pipeline runs.
A textobject's `inside`, `around`, or `surround` fragments can include `_collector_override` and `_extractor_override` fields. When present, these swap the textobject's collector and/or extractor for that prefix only. This allows a single textobject key to use completely different pipeline modules depending on whether it's accessed via `i`/`a` or via surround operators.
188
+
189
+
**Example:** The `f` textobject uses `treesitter` collector for `dif`/`daf` (function bodies) but swaps to `function_calls` collector for `dsf`/`csf` (function call expressions):
190
+
191
+
```lua
192
+
f= {
193
+
collector="treesitter", -- default: function definitions
194
+
extractor="pass_through",
195
+
-- ...
196
+
inside= { ts_inner_body=true },
197
+
around= {},
198
+
surround= {
199
+
_collector_override="function_calls", -- swap collector for surround
200
+
_extractor_override="pairs", -- swap extractor for surround
201
+
pair_scope="surround",
202
+
is_surround=true,
203
+
},
204
+
}
205
+
```
206
+
207
+
This pattern is useful when the same conceptual textobject key (`f` for "function") should behave differently in different contexts.
0 commit comments