pub enum Motion {
Show 49 variants
Left,
Right,
Up,
Down,
WordStartNext,
WordEndNext,
WordStartPrev,
WordEndPrev,
BigWordStartNext,
BigWordEndNext,
BigWordStartPrev,
BigWordEndPrev,
LineStart,
LineFirstNonBlank,
LineLastNonBlank,
LineEnd,
Column(u32),
LineDownFirstNonBlank,
LineUpFirstNonBlank,
LinewiseDown,
DocStart,
DocEnd,
FindChar {
ch: char,
backward: bool,
till: bool,
},
RepeatFind {
reverse: bool,
},
MatchPair,
MarkExact(char),
MarkLine(char),
ParagraphNext,
ParagraphPrev,
SentenceNext,
SentencePrev,
ScreenTop,
ScreenMiddle,
ScreenBottom,
PageUp,
PageDown,
HalfPageUp,
HalfPageDown,
GotoLine(u32),
ForwardSexp,
BackwardSexp,
UpList,
DownList,
BeginningOfDefun,
EndOfDefun,
BeginningOfSexp,
EndOfSexp,
SearchNext,
SearchPrev,
}Expand description
Cursor motions — primitive movements the keymap compiles user keys to.
Two families:
- Text motions — vim-ish char/word/line/doc/page motions.
- Structural motions — Lisp-aware
(forward-sexp)/(backward-sexp)/(up-list)/(down-list)equivalents. Enabled on buffers whose major mode opts in via(defmajor-mode … :structural-lisp #t). Matches paredit’s model — equal-or-superior to emacs on Lisp UX.
Variants§
Left
Right
Up
Down
WordStartNext
WordEndNext
WordStartPrev
WordEndPrev
ge — to the END of the previous word. vim’s only backward-inclusive
motion, and the reason is_inclusive cannot simply mean “widen right”.
BigWordStartNext
BigWordEndNext
BigWordStartPrev
BigWordEndPrev
LineStart
LineFirstNonBlank
LineLastNonBlank
g_ — the LAST non-blank on the line. Inclusive, unlike $.
LineEnd
Column(u32)
| — to a 1-based screen column on the current line.
LineDownFirstNonBlank
+ / <CR> — first non-blank of the next line.
LineUpFirstNonBlank
- — first non-blank of the previous line.
LinewiseDown
_ — count-1 lines downward, on the first non-blank. LINEWISE, which
is the whole reason it is not an alias of Self::LineFirstNonBlank:
^ and _ land the cursor on the same character, and d^ deletes
back to the indent while d_ deletes the whole line. Aliasing them —
which escriba did until 2026-08-14 — makes d_ a no-op at column 0,
because the exclusive range [cursor, first-non-blank) is empty there.
DocStart
DocEnd
FindChar
f{c} (backward=false, till=false), t{c} (till=true),
F{c} / T{c} (backward=true). The character is carried IN the
motion so df( is one composed ApplyOperator like every other
operated motion — a separate “pending char” the operator had to read
would be a second composition mechanism beside the FSM.
RepeatFind
; (reverse=false) / , (reverse=true) — repeat the last
Motion::FindChar. Resolved against runtime state, so like
Motion::SearchNext the enum stays a pure description.
MatchPair
% — to the match of the bracket under (or next on) the cursor.
MarkExact(char)
`{a-z} — to a mark’s exact line AND column.
MarkLine(char)
'{a-z} — to the first non-blank of a mark’s LINE. vim’s two spellings
are two motions, not one motion and a modifier: `a is exclusive
and 'a is linewise, so d'a and d`a delete different things.
ParagraphNext
} — to the next blank line (paragraph boundary).
ParagraphPrev
{ — to the previous blank line.
SentenceNext
) — to the start of the next sentence.
SentencePrev
( — to the start of the previous sentence.
ScreenTop
ScreenMiddle
ScreenBottom
PageUp
PageDown
HalfPageUp
HalfPageDown
GotoLine(u32)
ForwardSexp
Move to the start of the next sibling s-expression.
BackwardSexp
Move to the start of the previous sibling s-expression.
UpList
Move up one parenthesis level — to the opening ( of the enclosing list.
DownList
Move down into the current list — past the opening (.
BeginningOfDefun
Move to the start of the enclosing top-level defun / top form.
EndOfDefun
Move to the end of the enclosing top-level defun / top form.
BeginningOfSexp
Move to the start of the current s-expression (current atom / list open).
EndOfSexp
Move to the end of the current s-expression (matching close).
SearchNext
To the next search match — vim’s n used as a MOTION, which is what
makes d/foo<CR>, dn and y* work. Search being a motion rather
than a bare cursor jump is the difference between a search box and vim
search; resolving it needs the committed SearchState, so the executor
supplies it — the enum stays a pure description, like every other arm.
SearchPrev
To the previous search match (vim’s N as a motion).
Implementations§
Source§impl Motion
impl Motion
Sourcepub const fn is_inclusive(self) -> bool
pub const fn is_inclusive(self) -> bool
Does this motion name a character to ACT ON, rather than a boundary to stop before?
vim’s exclusive/inclusive split, and it is not cosmetic: dw deletes
up to the next word and de deletes through the current one. An
operator range is [cursor, target), so an inclusive motion’s target
has to be widened by one character or the operator leaves the last
character behind — off by exactly one, on the key most likely to be
used to delete a word without its trailing space.
RepeatFind answers false here and that is not a classification:
whether ; is inclusive depends on the direction of the find it
repeats, which is runtime state. The executor resolves it to the
concrete Motion::FindChar and asks THAT — so there is still exactly
one rule, applied to a known motion.
Exhaustive match since 2026-08-14, for the reason
Self::is_linewise gives at length: as a matches! this answered
false for every variant added after it was written, and it had
already been wrong that way. ge / gE were backward-inclusive
(the enum’s own note on Self::WordEndPrev said so) and unlisted, so
dge dropped the character under the cursor — "foo bar baz" at the
b of baz gave "foo babaz" where vim gives "foo baaz".
Sourcepub const fn is_linewise(self) -> bool
pub const fn is_linewise(self) -> bool
Does an operator over this motion act on WHOLE LINES?
vim has three motion kinds, not two — exclusive, inclusive, and
linewise — and escriba modelled only the first two until
2026-08-14. The consequence was a whole silently-wrong class rather
than one bad key: dj deleted one line instead of two, dgg stopped a
line short, and every one of them left a charwise register, so
yjp spliced two lines into the middle of a third instead of opening
lines below. The text was plausible and the register kind was invisible
until a later put, which is why nothing caught it.
Written as an exhaustive match rather than matches! on purpose
— and that is the load-bearing difference from Self::is_inclusive,
which is a matches! and therefore answers false for any variant
added after it was written. That silent default is exactly how this
class was born: Down, DocEnd, ScreenTop and the rest arrived as
cursor motions, and nobody was ever asked whether they were linewise.
Here a new Motion fails to compile until it is classified, so the
question cannot be skipped a second time.
pub const fn is_structural(self) -> bool
Trait Implementations§
impl Copy for Motion
Source§impl<'de> Deserialize<'de> for Motion
impl<'de> Deserialize<'de> for Motion
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for Motion
Source§impl JsonSchema for Motion
impl JsonSchema for Motion
Source§fn schema_name() -> String
fn schema_name() -> String
Source§fn schema_id() -> Cow<'static, str>
fn schema_id() -> Cow<'static, str>
Source§fn json_schema(generator: &mut SchemaGenerator) -> Schema
fn json_schema(generator: &mut SchemaGenerator) -> Schema
Source§fn is_referenceable() -> bool
fn is_referenceable() -> bool
$ref keyword. Read more