Custom Motions - FluxxField/smart-motion.nvim GitHub Wiki
Creating a Custom Motion
This guide walks you through creating your own motion using SmartMotion's modular pipeline system.
[!IMPORTANT] SmartMotion motions are composable, made up of collectors, extractors, filters, visualizers, and actions - plus an optional pipeline wrapper.
?? Basic Motion Structure
[!TIP] The first argument to
register_motion(name, config)
becomes the motion's name. Iftrigger_key
is not explicitly provided, it defaults to this name.This means you can do things like:
require("smart-motion").register_motion("find_2char", { trigger_key = "f", ... })
Here's what a minimal custom motion might look like:
require("smart-motion").register_motion("m", {
pipeline = {
collector = "lines",
extractor = "words",
filter = "filter_visible_lines",
visualizer = "hint_start",
},
pipeline_wrapper = "default",
action = "jump",
state = {
direction = DIRECTION.AFTER_CURSOR,
hint_position = HINT_POSITION.START,
},
map = true,
modes = { "n" },
metadata = {
label = "My Custom Motion",
description = "Example motion using word targets",
},
})
This would:
- Extract words from all visible lines
- Visualize with hints
- Let the user select one
- Then jump to it
?? Step-by-Step Breakdown
1. Collector
Fetches source data. Most start with:
collector = "lines"
But you could register one for git changes, Telescope results, etc.
2. Extractor
Extracts targets from the collected data.
extractor = "words"
Extractors yield target objects, including position and metadata.
3. Filter (Optional)
Narrows down the targets based on visibility, direction, or custom rules.
filter = "filter_visible_lines"
4. Visualizer
Renders your targets however you want - hints, popup, floating menu, etc.
visualizer = "hint_start"
5. Pipeline Wrapper (Optional)
Controls when and how the pipeline runs.
pipeline_wrapper = "text_search"
6. Action
What happens when a target is selected.
action = "jump"
Or use merge({ "jump", "delete" })
for compound motions.
?? State and Opts
Your motion config can include the following:
state
: for static configuration likedirection
,hint_position
opts
: for dynamic input likenum_of_char
These are passed to all modules in the pipeline.
[!NOTE] In a future version:
opts
will be deprecated - all dynamic input will be stored directly inmotion_state
direction
will be handled by filters instead of statehint_position
will likely become a property of the visualizer at registration time, so you'll register multiple visualizers forbefore
,after
, orboth
- reducing the need to specifyhint_position
instate
This improves consistency and separation of concerns.
2-char Find
?? Full Example: require("smart-motion").register_motion("f", {
pipeline = {
collector = "lines",
extractor = "text_search",
filter = "filter_visible_lines",
visualizer = "hint_start",
},
pipeline_wrapper = "text_search",
action = "jump",
state = {
direction = DIRECTION.AFTER_CURSOR,
hint_position = HINT_POSITION.START,
},
opts = {
num_of_char = 2,
},
map = true,
modes = { "n" },
})
?? Full Motion Type Reference
Here are the fields you can use when calling register_motion(name, config)
:
SmartMotionMotionEntry {
trigger_key?: string -- Optional override; defaults to `name`
infer?: boolean -- Is this an action trigger?
pipeline: {
collector: string
extractor: string
visualizer: string
filter?: string
}
pipeline_wrapper?: string
action: string
state?: SmartMotionMotionState -- Motion direction, hint position, etc.
opts?: table -- Dynamic input like character count (deprecated soon)
map?: boolean -- Whether to auto-create keymap
modes?: string[] -- Vim modes ("n", "v", etc.)
name?: string -- Internal ID; usually same as first argument
metadata?: {
label?: string
description?: string
}
}
[!NOTE] Most fields are optional, and many are auto-filled during registration:
trigger_key
defaults to the motion namename
,label
, anddescription
are generated if not givenopts
will be removed in favor of writing directly tomotion_state