mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-02 05:41:16 -03:00
feat(vi-mode): make word movements vi-compliant
- The behavior of `{,d}{w,W}`, `{,d}{,g}{e,E}` bindings in vi-mode is
now more compatible with vim, except that the underscore is not a
keyword (which can be achieved by setting `set iskeyword-=_` in vim).
- Add commands `{forward,kill}-{word,bigword}-vi`,
`{forward,backward,kill,backward-kill}-{word,bigword}-end` and
`kill-{a,inner}-{word,bigword}` corresponding to above-mentioned
bindings.
- Closes #10393.
Closes #12269
Co-authored-by: Johannes Altmanninger <aclopte@gmail.com>
This commit is contained in:
committed by
Johannes Altmanninger
parent
4c3fcc7b16
commit
bbb2f0de8d
@@ -9,6 +9,9 @@ Scripting improvements
|
||||
|
||||
Interactive improvements
|
||||
------------------------
|
||||
- Improves vi-mode to make its word movements more vim-compatible.
|
||||
- The behavior of ``{,d}{w,W}``, ``{,d}{,g}{e,E}`` bindings in vi-mode is now more compatible with vim, except that the underscore is not a keyword (which can be archived by setting ``set iskeyword-=_`` in vim).
|
||||
- Add commands ``{forward,kill}-{word,bigword}-vi``, ``{forward,backward,kill,backward-kill}-{word,bigword}-end`` and ``kill-{a,inner}-{word,bigword}`` corresponding to above-mentioned bindings.
|
||||
- Vi mode key bindings now support counts for movement and deletion commands (e.g. `d3w` or `3l`), via a new operator mode (:issue:`2192`).
|
||||
|
||||
New or improved bindings
|
||||
|
||||
@@ -131,18 +131,12 @@ The following special input functions are available:
|
||||
move one character to the left, but do not trigger any non-movement-related operations. If the cursor is at the start of
|
||||
the commandline, does nothing. Does not change the selected item in the completion pager UI when shown.
|
||||
|
||||
``backward-bigword``
|
||||
move one whitespace-delimited word to the left
|
||||
|
||||
``backward-token``
|
||||
move one argument to the left
|
||||
|
||||
``backward-delete-char``
|
||||
deletes one character of input to the left of the cursor
|
||||
|
||||
``backward-kill-bigword``
|
||||
move the whitespace-delimited word to the left of the cursor to the killring
|
||||
|
||||
``backward-kill-token``
|
||||
move the argument to the left of the cursor to the killring
|
||||
|
||||
@@ -155,13 +149,31 @@ The following special input functions are available:
|
||||
move one path component to the left of the cursor to the killring. A path component is everything likely to belong to a path component, i.e. not any of the following: `/={,}'\":@ |;<>&`, plus newlines and tabs.
|
||||
|
||||
``backward-kill-word``
|
||||
move the word to the left of the cursor to the killring. The "word" here is everything up to punctuation or whitespace.
|
||||
move the word to the left of the cursor to the killring, until the start of the current word (like vim's ``db``)
|
||||
|
||||
``backward-kill-bigword``
|
||||
move the whitespace-delimited word to the left of the cursor to the killring, until the start of the current word (like vim's ``dB``)
|
||||
|
||||
``backward-kill-word-end``
|
||||
move from the cursor to the end of the previous word to the killring (like vim's ``dge``)
|
||||
|
||||
``backward-kill-bigword-end``
|
||||
move from the cursor to the end of the previous whitespace-delimited word to the killring (like vim's ``dgE``)
|
||||
|
||||
``backward-path-component``
|
||||
move one :ref:`path component <cmd-bind-backward-kill-path-component>` to the left.
|
||||
move one :ref:`path component <cmd-bind-backward-kill-path-component>` to the left
|
||||
|
||||
``backward-word``
|
||||
move one word to the left
|
||||
move one word to the left, stopping at the start of the previous word (like vim's ``b``, or Emacs' ``M-b`` but differs slightly in word division rules)
|
||||
|
||||
``backward-bigword``
|
||||
move one whitespace-delimited word to the left, stopping at the start of the previous word (like vim's ``B``)
|
||||
|
||||
``backward-word-end``
|
||||
move to the end of the previous word (like vim's ``ge``)
|
||||
|
||||
``backward-bigword-end``
|
||||
move to the end of the previous whitespace-delimited word (like vim's ``gE``)
|
||||
|
||||
``beginning-of-buffer``
|
||||
moves to the beginning of the buffer, i.e. the start of the first line
|
||||
@@ -236,9 +248,6 @@ The following special input functions are available:
|
||||
``exit``
|
||||
exit the shell
|
||||
|
||||
``forward-bigword``
|
||||
move one whitespace-delimited word to the right
|
||||
|
||||
``forward-token``
|
||||
move one argument to the right
|
||||
|
||||
@@ -257,10 +266,29 @@ The following special input functions are available:
|
||||
``forward-single-char``
|
||||
move one character to the right; or if at the end of the commandline, accept a single char from the current autosuggestion.
|
||||
|
||||
.. _cmd-bind-forward-word:
|
||||
|
||||
``forward-word``
|
||||
move one word to the right; or if at the end of the commandline, accept one word
|
||||
move one word to the right, stopping after the end of the current word; or if at the end of the commandline, accept one word
|
||||
from the current autosuggestion.
|
||||
|
||||
``forward-word-vi``
|
||||
like :ref:`forward-word <cmd-bind-forward-word>`, but stops at the start of the next word (like vim's ``w``)
|
||||
|
||||
``forward-word-end``
|
||||
like :ref:`forward-word <cmd-bind-forward-word>`, but stops at the end of the next word (like vim's ``e``)
|
||||
|
||||
.. _cmd-bind-forward-bigword:
|
||||
|
||||
``forward-bigword``
|
||||
move one whitespace-delimited word to the right, stopping after the end of the current word; or if at the end of the commandline, accept one word from the current autosuggestion.
|
||||
|
||||
``forward-bigword-vi``
|
||||
like :ref:`forward-bigword <cmd-bind-forward-bigword>`, but stops at the start of the next word (like vim's ``W``)
|
||||
|
||||
``forward-bigword-end``
|
||||
like :ref:`forward-bigword <cmd-bind-forward-bigword>`, but stops at the end of the next word (like vim's ``E``)
|
||||
|
||||
``history-pager``
|
||||
invoke the searchable pager on history (incremental search); or if the history pager is already active, search further backwards in time.
|
||||
|
||||
@@ -312,9 +340,6 @@ The following special input functions are available:
|
||||
The input function is useful to emulate ``ib`` vi text object.
|
||||
The following brackets are considered: ``([{}])``
|
||||
|
||||
``kill-bigword``
|
||||
move the next whitespace-delimited word to the killring
|
||||
|
||||
``kill-token``
|
||||
move the next argument to the killring
|
||||
|
||||
@@ -334,7 +359,34 @@ The following special input functions are available:
|
||||
move the line (without the following newline) to the killring
|
||||
|
||||
``kill-word``
|
||||
move the next word to the killring
|
||||
move the next word to the killring, stopping after the end of the killed word
|
||||
|
||||
``kill-word-vi``
|
||||
move the next word to the killring, stopping at the start of the next word (like vim's ``dw``)
|
||||
|
||||
``kill-bigword``
|
||||
move the next whitespace-delimited word to the killring, stopping after the end of the current word
|
||||
|
||||
``kill-bigword-vi``
|
||||
move the next whitespace-delimited word to the killring, stopping at the start of the next word (like vim's ``dW``)
|
||||
|
||||
``kill-bigword-end``
|
||||
move from the cursor to the end of the current whitespace-delimited word to the killring (like vim's ``dE``)
|
||||
|
||||
``kill-word-end``
|
||||
move from the cursor to the end of the current word to the killring (like vim's ``de``)
|
||||
|
||||
``kill-inner-word``
|
||||
delete the word under the cursor (like vim's ``diw``)
|
||||
|
||||
``kill-inner-bigword``
|
||||
delete the whitespace-delimited word under the cursor (like vim's ``diW``)
|
||||
|
||||
``kill-a-word``
|
||||
delete the word under the cursor plus surrounding whitespace (like vim's ``daw``)
|
||||
|
||||
``kill-a-bigword``
|
||||
delete the whitespace-delimited word under the cursor plus surrounding whitespace (like vim's ``daW``)
|
||||
|
||||
``nextd-or-forward-word``
|
||||
if the commandline is empty, then move forward in the directory history, otherwise move one word to the right;
|
||||
|
||||
@@ -24,6 +24,19 @@ The following parameters are available:
|
||||
|
||||
Further information on how to use :ref:`vi mode <vi-mode>`.
|
||||
|
||||
Differences from Vim
|
||||
--------------------
|
||||
|
||||
Fish's vi mode aims to be familiar to vim users, but there are some differences:
|
||||
|
||||
**Word character handling**
|
||||
In vim, underscore (``_``) is treated as a keyword character by default, so word motions like ``w``, ``b``, and ``e`` treat ``foo_bar`` as a single word. In fish, underscore is treated as punctuation, so word motions stop at underscores. For example, pressing ``w`` on ``foo_bar`` in fish stops at the ``_``, while in vim it would jump past the entire identifier.
|
||||
|
||||
**The** ``cw`` **command**
|
||||
In vim, ``cw`` has special behavior: when the cursor is on a non-space character, it behaves like ``ce`` (change to end of word), but when the cursor is on a space, it behaves like ``dwi`` (delete word then insert).
|
||||
|
||||
In fish, ``cw`` always behaves like ``dwi`` - it deletes to the start of the next word (including trailing whitespace), then enters insert mode. To get vim's ``cw`` behavior in fish, use ``ce`` instead.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ function fish_vi_run_count --description 'Run a command $__fish_vi_count times'
|
||||
set -l count (__fish_vi_consume_count __fish_vi_count)
|
||||
|
||||
for i in (seq $count)
|
||||
# Check if the first argument is a defined shell function (like fish_vi_move_end_word)
|
||||
if functions -q -- $argv[1]
|
||||
$argv
|
||||
else
|
||||
@@ -72,51 +71,76 @@ function fish_vi_exec_motion
|
||||
commandline -f kill-whole-line yank
|
||||
end
|
||||
case swap-case
|
||||
for i in (seq $total)
|
||||
commandline -f beginning-of-line begin-selection down-line togglecase-selection end-selection
|
||||
# Not implemented yet
|
||||
return
|
||||
end
|
||||
else
|
||||
set -l use_selection true
|
||||
set -l swap_case_hack
|
||||
switch $motion
|
||||
case forward-word-vi forward-bigword-vi
|
||||
if test $__fish_vi_operator = swap-case
|
||||
set swap_case_hack (string replace -r -- '^forward-((?:big)?word)-vi$' '$1' $motion)
|
||||
else
|
||||
set use_selection false
|
||||
set motion (string replace -- forward kill $motion)
|
||||
end
|
||||
end
|
||||
commandline -f repaint-mode
|
||||
else
|
||||
if $use_selection
|
||||
commandline -f begin-selection
|
||||
else
|
||||
commandline -f begin-undo-group
|
||||
end
|
||||
switch $__fish_vi_operator
|
||||
case delete
|
||||
commandline -f begin-selection
|
||||
for i in (seq $total)
|
||||
commandline -f $motion
|
||||
end
|
||||
commandline -f kill-selection end-selection repaint-mode
|
||||
if $use_selection
|
||||
commandline -f kill-selection
|
||||
end
|
||||
case change
|
||||
commandline -f begin-selection
|
||||
for i in (seq $total)
|
||||
commandline -f $motion
|
||||
end
|
||||
commandline -f kill-selection end-selection repaint-mode
|
||||
# Switch to insert mode
|
||||
if $use_selection
|
||||
commandline -f kill-selection
|
||||
end
|
||||
set fish_bind_mode insert
|
||||
case yank
|
||||
commandline -f begin-selection
|
||||
for i in (seq $total)
|
||||
commandline -f $motion
|
||||
end
|
||||
commandline -f kill-selection yank end-selection repaint-mode
|
||||
if $use_selection
|
||||
commandline -f kill-selection
|
||||
end
|
||||
commandline -f yank
|
||||
case swap-case
|
||||
commandline -f begin-selection
|
||||
for i in (seq $total)
|
||||
commandline -f $motion
|
||||
end
|
||||
commandline -f togglecase-selection end-selection repaint-mode
|
||||
if set -q swap_case_hack[1]
|
||||
set -l word $swap_case_hack
|
||||
commandline -f \
|
||||
backward-$word \
|
||||
forward-$word-end \
|
||||
togglecase-selection \
|
||||
backward-$word \
|
||||
forward-$word-vi
|
||||
else
|
||||
commandline -f togglecase-selection
|
||||
end
|
||||
end
|
||||
if $use_selection
|
||||
commandline -f end-selection
|
||||
else
|
||||
commandline -f end-undo-group
|
||||
end
|
||||
end
|
||||
commandline -f repaint-mode
|
||||
set -g __fish_vi_operator
|
||||
end
|
||||
|
||||
function fish_vi_move_end_word
|
||||
set -l cmd $argv[1]
|
||||
set fish_cursor_end_mode exclusive
|
||||
commandline -f forward-single-char $cmd backward-char
|
||||
set fish_cursor_end_mode inclusive
|
||||
end
|
||||
|
||||
# TODO: Currently we do not support hexadecimal and octal values.
|
||||
function fish_vi_inc_dec --description 'increment or decrement the number below the cursor'
|
||||
# The cursor is zero based, but all string functions assume 1 to be
|
||||
@@ -276,14 +300,14 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
|
||||
bind -s --preset -M default b 'fish_vi_run_count backward-word'
|
||||
bind -s --preset -M default B 'fish_vi_run_count backward-bigword'
|
||||
bind -s --preset -M default g,e 'fish_vi_run_count backward-word'
|
||||
bind -s --preset -M default g,E 'fish_vi_run_count backward-bigword'
|
||||
bind -s --preset -M default g,e 'fish_vi_run_count backward-word-end'
|
||||
bind -s --preset -M default g,E 'fish_vi_run_count backward-bigword-end'
|
||||
|
||||
bind -s --preset -M default w 'fish_vi_run_count forward-word forward-single-char'
|
||||
bind -s --preset -M default W 'fish_vi_run_count forward-bigword forward-single-char'
|
||||
bind -s --preset -M default w 'fish_vi_run_count forward-word-vi'
|
||||
bind -s --preset -M default W 'fish_vi_run_count forward-bigword-vi'
|
||||
|
||||
bind -s --preset -M default e 'fish_vi_run_count fish_vi_move_end_word forward-word'
|
||||
bind -s --preset -M default E 'fish_vi_run_count fish_vi_move_end_word forward-bigword'
|
||||
bind -s --preset -M default e 'fish_vi_run_count forward-word-end'
|
||||
bind -s --preset -M default E 'fish_vi_run_count forward-bigword-end'
|
||||
|
||||
bind -s --preset -M default x 'fish_vi_run_count delete-char'
|
||||
bind -s --preset -M default X 'fish_vi_run_count backward-delete-char'
|
||||
@@ -317,6 +341,9 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
bind -s --preset ] history-token-search-forward
|
||||
bind -s --preset -m insert / history-pager repaint-mode
|
||||
|
||||
__fish_per_os_bind --preset $argv ctrl-right forward-token forward-word-vi
|
||||
# ctrl-left is same as emacs mode
|
||||
|
||||
bind -s --preset -M insert ctrl-n accept-autosuggestion
|
||||
|
||||
# Vi/Vim doesn't support these keys in insert mode but that seems silly so we do so anyway.
|
||||
@@ -355,12 +382,12 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
bind -s --preset -M operator j 'fish_vi_exec_motion down-line'
|
||||
bind -s --preset -M operator b 'fish_vi_exec_motion backward-word'
|
||||
bind -s --preset -M operator B 'fish_vi_exec_motion backward-bigword'
|
||||
bind -s --preset -M operator g,e 'fish_vi_exec_motion backward-word'
|
||||
bind -s --preset -M operator g,E 'fish_vi_exec_motion backward-bigword'
|
||||
bind -s --preset -M operator w 'fish_vi_exec_motion forward-word'
|
||||
bind -s --preset -M operator W 'fish_vi_exec_motion forward-bigword'
|
||||
bind -s --preset -M operator e 'fish_vi_exec_motion forward-single-char forward-word backward-char'
|
||||
bind -s --preset -M operator E 'fish_vi_exec_motion forward-single-char forward-bigword backward-char'
|
||||
bind -s --preset -M operator g,e 'fish_vi_exec_motion backward-word-end'
|
||||
bind -s --preset -M operator g,E 'fish_vi_exec_motion backward-bigword-end'
|
||||
bind -s --preset -M operator w 'fish_vi_exec_motion forward-word-vi'
|
||||
bind -s --preset -M operator W 'fish_vi_exec_motion forward-bigword-vi'
|
||||
bind -s --preset -M operator e 'fish_vi_exec_motion forward-word-end'
|
||||
bind -s --preset -M operator E 'fish_vi_exec_motion forward-bigword-end'
|
||||
|
||||
bind -s --preset -M operator 0 'fish_vi_exec_motion beginning-of-line'
|
||||
bind -s --preset -M operator \^ 'fish_vi_exec_motion beginning-of-line'
|
||||
@@ -383,10 +410,10 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
bind -s --preset d,\^ backward-kill-line
|
||||
bind -s --preset d,0 backward-kill-line
|
||||
|
||||
bind -s --preset d,i,w forward-single-char forward-single-char backward-word kill-word
|
||||
bind -s --preset d,i,W forward-single-char forward-single-char backward-bigword kill-bigword
|
||||
bind -s --preset d,a,w forward-single-char forward-single-char backward-word kill-word
|
||||
bind -s --preset d,a,W forward-single-char forward-single-char backward-bigword kill-bigword
|
||||
bind -s --preset d,i,w kill-inner-word
|
||||
bind -s --preset d,i,W kill-inner-bigword
|
||||
bind -s --preset d,a,w kill-a-word
|
||||
bind -s --preset d,a,W kill-a-bigword
|
||||
bind -s --preset d,i,b jump-till-matching-bracket and jump-till-matching-bracket and begin-selection jump-till-matching-bracket kill-selection end-selection
|
||||
bind -s --preset d,a,b jump-to-matching-bracket and jump-to-matching-bracket and begin-selection jump-to-matching-bracket kill-selection end-selection
|
||||
bind -s --preset d,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection
|
||||
@@ -401,10 +428,10 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
bind -s --preset -m insert c,\^ backward-kill-line repaint-mode
|
||||
bind -s --preset -m insert c,0 backward-kill-line repaint-mode
|
||||
|
||||
bind -s --preset -m insert c,i,w forward-single-char forward-single-char backward-word kill-word repaint-mode
|
||||
bind -s --preset -m insert c,i,W forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode
|
||||
bind -s --preset -m insert c,a,w forward-single-char forward-single-char backward-word kill-word repaint-mode
|
||||
bind -s --preset -m insert c,a,W forward-single-char forward-single-char backward-bigword kill-bigword repaint-mode
|
||||
bind -s --preset -m insert c,i,w kill-inner-word repaint-mode
|
||||
bind -s --preset -m insert c,i,W kill-inner-bigword repaint-mode
|
||||
bind -s --preset -m insert c,a,w kill-a-word repaint-mode
|
||||
bind -s --preset -m insert c,a,W kill-a-bigword repaint-mode
|
||||
bind -s --preset -m insert c,i,b jump-till-matching-bracket and jump-till-matching-bracket and begin-selection jump-till-matching-bracket kill-selection end-selection
|
||||
bind -s --preset -m insert c,a,b jump-to-matching-bracket and jump-to-matching-bracket and begin-selection jump-to-matching-bracket kill-selection end-selection
|
||||
bind -s --preset -m insert c,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection end-selection repaint-mode
|
||||
@@ -425,11 +452,10 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
bind -s --preset y,\$ kill-line yank
|
||||
bind -s --preset y,\^ backward-kill-line yank
|
||||
bind -s --preset y,0 backward-kill-line yank
|
||||
|
||||
bind -s --preset y,i,w forward-single-char forward-single-char backward-word kill-word yank
|
||||
bind -s --preset y,i,W forward-single-char forward-single-char backward-bigword kill-bigword yank
|
||||
bind -s --preset y,a,w forward-single-char forward-single-char backward-word kill-word yank
|
||||
bind -s --preset y,a,W forward-single-char forward-single-char backward-bigword kill-bigword yank
|
||||
bind -s --preset y,i,w kill-inner-word yank
|
||||
bind -s --preset y,i,W kill-inner-bigword yank
|
||||
bind -s --preset y,a,w kill-a-word yank
|
||||
bind -s --preset y,a,W kill-a-bigword yank
|
||||
bind -s --preset y,i,b jump-till-matching-bracket and jump-till-matching-bracket and begin-selection jump-till-matching-bracket kill-selection yank end-selection
|
||||
bind -s --preset y,a,b jump-to-matching-bracket and jump-to-matching-bracket and begin-selection jump-to-matching-bracket kill-selection yank end-selection
|
||||
bind -s --preset y,i backward-jump-till and repeat-jump-reverse and begin-selection repeat-jump kill-selection yank end-selection
|
||||
@@ -500,12 +526,12 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish'
|
||||
|
||||
bind -s --preset -M visual b backward-word
|
||||
bind -s --preset -M visual B backward-bigword
|
||||
bind -s --preset -M visual g,e backward-word
|
||||
bind -s --preset -M visual g,E backward-bigword
|
||||
bind -s --preset -M visual w forward-word
|
||||
bind -s --preset -M visual W forward-bigword
|
||||
bind -s --preset -M visual e 'set fish_cursor_end_mode exclusive' forward-single-char forward-word backward-char 'set fish_cursor_end_mode inclusive'
|
||||
bind -s --preset -M visual E 'set fish_cursor_end_mode exclusive' forward-single-char forward-bigword backward-char 'set fish_cursor_end_mode inclusive'
|
||||
bind -s --preset -M visual g,e backward-word-end
|
||||
bind -s --preset -M visual g,E backward-bigword-end
|
||||
bind -s --preset -M visual w forward-word-vi
|
||||
bind -s --preset -M visual W forward-bigword-vi
|
||||
bind -s --preset -M visual e forward-word-end
|
||||
bind -s --preset -M visual E forward-bigword-end
|
||||
bind -s --preset -M visual o swap-selection-start-stop repaint-mode
|
||||
|
||||
bind -s --preset -M visual % jump-to-matching-bracket
|
||||
|
||||
26
src/input.rs
26
src/input.rs
@@ -107,19 +107,23 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
|
||||
make_md(L!("accept-autosuggestion"), ReadlineCmd::AcceptAutosuggestion),
|
||||
make_md(L!("and"), ReadlineCmd::FuncAnd),
|
||||
make_md(L!("backward-bigword"), ReadlineCmd::BackwardBigword),
|
||||
make_md(L!("backward-bigword-end"), ReadlineCmd::BackwardBigwordEnd),
|
||||
make_md(L!("backward-char"), ReadlineCmd::BackwardChar),
|
||||
make_md(L!("backward-char-passive"), ReadlineCmd::BackwardCharPassive),
|
||||
make_md(L!("backward-delete-char"), ReadlineCmd::BackwardDeleteChar),
|
||||
make_md(L!("backward-jump"), ReadlineCmd::BackwardJump),
|
||||
make_md(L!("backward-jump-till"), ReadlineCmd::BackwardJumpTill),
|
||||
make_md(L!("backward-kill-bigword"), ReadlineCmd::BackwardKillBigword),
|
||||
make_md(L!("backward-kill-bigword-end"), ReadlineCmd::BackwardKillBigwordEnd),
|
||||
make_md(L!("backward-kill-line"), ReadlineCmd::BackwardKillLine),
|
||||
make_md(L!("backward-kill-path-component"), ReadlineCmd::BackwardKillPathComponent),
|
||||
make_md(L!("backward-kill-token"), ReadlineCmd::BackwardKillToken),
|
||||
make_md(L!("backward-kill-word"), ReadlineCmd::BackwardKillWord),
|
||||
make_md(L!("backward-kill-word-end"), ReadlineCmd::BackwardKillWordEnd),
|
||||
make_md(L!("backward-path-component"), ReadlineCmd::BackwardPathComponent),
|
||||
make_md(L!("backward-token"), ReadlineCmd::BackwardToken),
|
||||
make_md(L!("backward-word"), ReadlineCmd::BackwardWord),
|
||||
make_md(L!("backward-word-end"), ReadlineCmd::BackwardWordEnd),
|
||||
make_md(L!("begin-selection"), ReadlineCmd::BeginSelection),
|
||||
make_md(L!("begin-undo-group"), ReadlineCmd::BeginUndoGroup),
|
||||
make_md(L!("beginning-of-buffer"), ReadlineCmd::BeginningOfBuffer),
|
||||
@@ -146,7 +150,9 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
|
||||
make_md(L!("exit"), ReadlineCmd::Exit),
|
||||
make_md(L!("expand-abbr"), ReadlineCmd::ExpandAbbr),
|
||||
make_md(L!("force-repaint"), ReadlineCmd::ForceRepaint),
|
||||
make_md(L!("forward-bigword"), ReadlineCmd::ForwardBigword),
|
||||
make_md(L!("forward-bigword"), ReadlineCmd::ForwardBigwordEmacs),
|
||||
make_md(L!("forward-bigword-end"), ReadlineCmd::ForwardBigwordEnd),
|
||||
make_md(L!("forward-bigword-vi"), ReadlineCmd::ForwardBigwordVi),
|
||||
make_md(L!("forward-char"), ReadlineCmd::ForwardChar),
|
||||
make_md(L!("forward-char-passive"), ReadlineCmd::ForwardCharPassive),
|
||||
make_md(L!("forward-jump"), ReadlineCmd::ForwardJump),
|
||||
@@ -154,7 +160,9 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
|
||||
make_md(L!("forward-path-component"), ReadlineCmd::ForwardPathComponent),
|
||||
make_md(L!("forward-single-char"), ReadlineCmd::ForwardSingleChar),
|
||||
make_md(L!("forward-token"), ReadlineCmd::ForwardToken),
|
||||
make_md(L!("forward-word"), ReadlineCmd::ForwardWord),
|
||||
make_md(L!("forward-word"), ReadlineCmd::ForwardWordEmacs),
|
||||
make_md(L!("forward-word-end"), ReadlineCmd::ForwardWordEnd),
|
||||
make_md(L!("forward-word-vi"), ReadlineCmd::ForwardWordVi),
|
||||
make_md(L!("history-delete"), ReadlineCmd::HistoryDelete),
|
||||
make_md(L!("history-last-token-search-backward"), ReadlineCmd::HistoryLastTokenSearchBackward),
|
||||
make_md(L!("history-last-token-search-forward"), ReadlineCmd::HistoryLastTokenSearchForward),
|
||||
@@ -171,15 +179,23 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat
|
||||
make_md(L!("insert-line-under"), ReadlineCmd::InsertLineUnder),
|
||||
make_md(L!("jump-till-matching-bracket"), ReadlineCmd::JumpTillMatchingBracket),
|
||||
make_md(L!("jump-to-matching-bracket"), ReadlineCmd::JumpToMatchingBracket),
|
||||
make_md(L!("kill-bigword"), ReadlineCmd::KillBigword),
|
||||
make_md(L!("kill-a-bigword"), ReadlineCmd::KillABigWord),
|
||||
make_md(L!("kill-a-word"), ReadlineCmd::KillAWord),
|
||||
make_md(L!("kill-bigword"), ReadlineCmd::KillBigwordEmacs),
|
||||
make_md(L!("kill-bigword-end"), ReadlineCmd::KillBigwordEnd),
|
||||
make_md(L!("kill-bigword-vi"), ReadlineCmd::KillBigwordVi),
|
||||
make_md(L!("kill-inner-bigword"), ReadlineCmd::KillInnerBigWord),
|
||||
make_md(L!("kill-inner-line"), ReadlineCmd::KillInnerLine),
|
||||
make_md(L!("kill-inner-word"), ReadlineCmd::KillInnerWord),
|
||||
make_md(L!("kill-line"), ReadlineCmd::KillLine),
|
||||
make_md(L!("kill-path-component"), ReadlineCmd::KillPathComponent),
|
||||
make_md(L!("kill-selection"), ReadlineCmd::KillSelection),
|
||||
make_md(L!("kill-token"), ReadlineCmd::KillToken),
|
||||
make_md(L!("kill-whole-line"), ReadlineCmd::KillWholeLine),
|
||||
make_md(L!("kill-word"), ReadlineCmd::KillWord),
|
||||
make_md(L!("nextd-or-forward-word"), ReadlineCmd::NextdOrForwardWord),
|
||||
make_md(L!("kill-word"), ReadlineCmd::KillWordEmacs),
|
||||
make_md(L!("kill-word-end"), ReadlineCmd::KillWordEnd),
|
||||
make_md(L!("kill-word-vi"), ReadlineCmd::KillWordVi),
|
||||
make_md(L!("nextd-or-forward-word"), ReadlineCmd::NextdOrForwardWordEmacs),
|
||||
make_md(L!("or"), ReadlineCmd::FuncOr),
|
||||
make_md(L!("pager-toggle-search"), ReadlineCmd::PagerToggleSearch),
|
||||
make_md(L!("prevd-or-backward-word"), ReadlineCmd::PrevdOrBackwardWord),
|
||||
|
||||
@@ -50,15 +50,21 @@ pub enum ReadlineCmd {
|
||||
BackwardCharPassive,
|
||||
ForwardSingleChar,
|
||||
ForwardCharPassive,
|
||||
ForwardWord,
|
||||
BackwardWord,
|
||||
ForwardBigword,
|
||||
ForwardWordEmacs,
|
||||
ForwardBigwordEmacs,
|
||||
BackwardBigword,
|
||||
ForwardWordEnd,
|
||||
BackwardWordEnd,
|
||||
ForwardBigwordEnd,
|
||||
BackwardBigwordEnd,
|
||||
ForwardWordVi,
|
||||
ForwardBigwordVi,
|
||||
ForwardPathComponent,
|
||||
ForwardToken,
|
||||
BackwardPathComponent,
|
||||
BackwardToken,
|
||||
NextdOrForwardWord,
|
||||
NextdOrForwardWordEmacs,
|
||||
PrevdOrBackwardWord,
|
||||
HistoryDelete,
|
||||
HistorySearchBackward,
|
||||
@@ -81,13 +87,23 @@ pub enum ReadlineCmd {
|
||||
BackwardKillLine,
|
||||
KillWholeLine,
|
||||
KillInnerLine,
|
||||
KillWord,
|
||||
KillBigword,
|
||||
KillWordEmacs,
|
||||
KillBigwordEmacs,
|
||||
KillWordVi,
|
||||
KillBigwordVi,
|
||||
KillWordEnd,
|
||||
KillBigwordEnd,
|
||||
KillInnerWord,
|
||||
KillInnerBigWord,
|
||||
KillAWord,
|
||||
KillABigWord,
|
||||
KillPathComponent,
|
||||
KillToken,
|
||||
BackwardKillWord,
|
||||
BackwardKillWordEnd,
|
||||
BackwardKillPathComponent,
|
||||
BackwardKillBigword,
|
||||
BackwardKillBigwordEnd,
|
||||
BackwardKillToken,
|
||||
HistoryTokenSearchBackward,
|
||||
HistoryTokenSearchForward,
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
use super::history_search::{ReaderHistorySearch, SearchMode, smartcase_flags};
|
||||
use super::iothreads::{self, Debouncers};
|
||||
use super::word_motion::{MoveWordStateMachine, MoveWordStyle};
|
||||
use super::word_motion::{MoveWordDir, MoveWordStateMachine, MoveWordStyle};
|
||||
use crate::abbrs::abbrs_match;
|
||||
use crate::ast::{self, Kind, is_same_node};
|
||||
use crate::builtins::shared::ErrorCode;
|
||||
@@ -121,7 +121,6 @@
|
||||
have_proc_stat, hup_jobs, is_interactive_session, job_reap, jobs_requiring_warning_on_exit,
|
||||
print_exit_warning_for_jobs, proc_update_jiffies,
|
||||
};
|
||||
use crate::reader::word_motion::MoveWordDir;
|
||||
use crate::screen::is_dumb;
|
||||
use crate::screen::{CharOffset, Screen, screen_force_clear_to_end};
|
||||
use crate::should_flog;
|
||||
@@ -376,6 +375,7 @@ pub fn current_data() -> Option<&'static mut ReaderData> {
|
||||
.map(|data| unsafe { Pin::get_unchecked_mut(Pin::as_mut(data)) })
|
||||
}
|
||||
pub use current_data as reader_current_data;
|
||||
use fish_wchar::word_char::is_blank;
|
||||
|
||||
/// Add a new reader to the reader stack.
|
||||
/// If `history_name` is empty, then save history in-memory only; do not write it to disk.
|
||||
@@ -2187,39 +2187,64 @@ fn move_word(
|
||||
erase: bool,
|
||||
style: MoveWordStyle,
|
||||
newv: bool,
|
||||
to_word_end: bool,
|
||||
) {
|
||||
let move_right = direction == MoveWordDir::Right;
|
||||
// Return if we are already at the edge.
|
||||
let state_machine_dir = if to_word_end {
|
||||
match direction {
|
||||
MoveWordDir::Left => MoveWordDir::Right,
|
||||
MoveWordDir::Right => MoveWordDir::Left,
|
||||
}
|
||||
} else {
|
||||
direction
|
||||
};
|
||||
let el = self.edit_line(elt);
|
||||
let boundary = if move_right { el.len() } else { 0 };
|
||||
|
||||
// Return if we are already at the edge.
|
||||
if el.position() == boundary {
|
||||
return;
|
||||
}
|
||||
|
||||
// When moving left, a value of 1 means the character at index 0.
|
||||
let mut state = MoveWordStateMachine::new(style);
|
||||
let mut state = MoveWordStateMachine::new(style, state_machine_dir);
|
||||
let start_buff_pos = el.position();
|
||||
|
||||
let mut buff_pos = el.position();
|
||||
while buff_pos != boundary {
|
||||
let idx = if move_right { buff_pos } else { buff_pos - 1 };
|
||||
if !state.consume_char(el.text(), idx) {
|
||||
|
||||
let end = if move_right {
|
||||
if to_word_end { el.len() - 1 } else { el.len() }
|
||||
} else if to_word_end {
|
||||
usize::MAX
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
while buff_pos != end {
|
||||
if move_right && buff_pos >= el.len() {
|
||||
break;
|
||||
}
|
||||
buff_pos = if move_right {
|
||||
buff_pos + 1
|
||||
let char_pos = if move_right {
|
||||
if to_word_end { buff_pos + 1 } else { buff_pos }
|
||||
} else if to_word_end {
|
||||
buff_pos
|
||||
} else {
|
||||
buff_pos - 1
|
||||
buff_pos.wrapping_sub(1)
|
||||
};
|
||||
let consumed = state.consume_char(el.text(), char_pos);
|
||||
if consumed {
|
||||
buff_pos = if move_right {
|
||||
buff_pos + 1
|
||||
} else {
|
||||
buff_pos.wrapping_sub(1)
|
||||
};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Always consume at least one character.
|
||||
if buff_pos == start_buff_pos {
|
||||
buff_pos = if move_right {
|
||||
buff_pos + 1
|
||||
} else {
|
||||
buff_pos - 1
|
||||
};
|
||||
if buff_pos == usize::MAX {
|
||||
buff_pos = 0;
|
||||
}
|
||||
|
||||
// If we are moving left, buff_pos-1 is the index of the first character we do not delete
|
||||
@@ -2231,15 +2256,150 @@ fn move_word(
|
||||
}
|
||||
|
||||
if move_right {
|
||||
self.kill(elt, start_buff_pos..buff_pos, Kill::Append, newv);
|
||||
self.kill(
|
||||
elt,
|
||||
start_buff_pos..(buff_pos + if to_word_end { 1 } else { 0 }),
|
||||
Kill::Append,
|
||||
newv,
|
||||
);
|
||||
} else {
|
||||
self.kill(elt, buff_pos..start_buff_pos, Kill::Prepend, newv);
|
||||
self.kill(
|
||||
elt,
|
||||
buff_pos..start_buff_pos + if to_word_end { 1 } else { 0 },
|
||||
Kill::Prepend,
|
||||
newv,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.update_buff_pos(elt, Some(buff_pos));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_a_word(&mut self, elt: EditableLineTag, style: MoveWordStyle, newv: bool) {
|
||||
let el = self.edit_line(elt);
|
||||
if el.is_empty() {
|
||||
return;
|
||||
}
|
||||
let text_slice = el.text().as_char_slice();
|
||||
let pos = el.position();
|
||||
let on_blank = is_blank(text_slice[pos]);
|
||||
|
||||
if on_blank {
|
||||
// first move backward until non-spaces
|
||||
let mut begin_pos = 0;
|
||||
for idx in (0..pos).rev() {
|
||||
if !is_blank(text_slice[idx]) {
|
||||
begin_pos = idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.update_buff_pos(elt, Some(begin_pos));
|
||||
// then delete to next word end
|
||||
self.move_word(elt, MoveWordDir::Right, true, style, newv, true)
|
||||
} else {
|
||||
// first, move right by 1
|
||||
if pos < el.len() - 1 {
|
||||
self.update_buff_pos(elt, Some(el.position() + 1));
|
||||
}
|
||||
// then move to word start
|
||||
self.move_word(
|
||||
elt,
|
||||
MoveWordDir::Left,
|
||||
/*erase=*/ false,
|
||||
style,
|
||||
newv,
|
||||
false,
|
||||
);
|
||||
let word_start = self.edit_line(elt).position();
|
||||
self.move_word(
|
||||
elt,
|
||||
MoveWordDir::Right,
|
||||
/*erase=*/ false,
|
||||
style,
|
||||
newv,
|
||||
true,
|
||||
);
|
||||
|
||||
let el = self.edit_line(elt);
|
||||
let word_end = el.position() + 1;
|
||||
let text_slice = el.text().as_char_slice();
|
||||
let len = el.len();
|
||||
let kill_range = if word_end < len && is_blank(text_slice[word_end]) {
|
||||
let mut end_pos = len;
|
||||
for (idx, &c) in text_slice.iter().enumerate().skip(word_end + 1) {
|
||||
if !is_blank(c) {
|
||||
end_pos = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
word_start..end_pos
|
||||
} else if word_start > 0 && is_blank(text_slice[word_start - 1]) {
|
||||
let mut begin_pos = 0;
|
||||
for idx in (0..word_start - 1).rev() {
|
||||
if !is_blank(text_slice[idx]) {
|
||||
begin_pos = idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
begin_pos..word_end
|
||||
} else {
|
||||
word_start..word_end
|
||||
};
|
||||
self.update_buff_pos(elt, Some(word_start));
|
||||
self.kill(elt, kill_range, Kill::Append, newv);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_inner_word(&mut self, elt: EditableLineTag, style: MoveWordStyle, newv: bool) {
|
||||
let el = self.edit_line(elt);
|
||||
let len = el.len();
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
let pos = el.position();
|
||||
let text_slice = el.text().as_char_slice();
|
||||
if is_blank(text_slice[pos]) {
|
||||
// Cursor is on whitespace: delete whitespace only
|
||||
let mut begin_pos = 0;
|
||||
for idx in (0..pos).rev() {
|
||||
if !is_blank(text_slice[idx]) {
|
||||
begin_pos = idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut end_pos = len;
|
||||
for (idx, &c) in text_slice.iter().enumerate().skip(pos) {
|
||||
if !is_blank(c) {
|
||||
end_pos = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.kill(elt, begin_pos..end_pos, Kill::Append, newv);
|
||||
} else {
|
||||
if el.position() != 0 {
|
||||
// first, move right by 1
|
||||
self.update_buff_pos(elt, Some(el.position() + 1));
|
||||
// then move to word start
|
||||
self.move_word(
|
||||
elt,
|
||||
MoveWordDir::Left,
|
||||
/*erase=*/ false,
|
||||
style,
|
||||
newv,
|
||||
false,
|
||||
);
|
||||
}
|
||||
self.move_word(
|
||||
elt,
|
||||
MoveWordDir::Right,
|
||||
/*erase=*/ true,
|
||||
style,
|
||||
newv,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn jump_to_matching_bracket(
|
||||
&mut self,
|
||||
precision: JumpPrecision,
|
||||
@@ -3403,21 +3563,77 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
|
||||
}
|
||||
}
|
||||
}
|
||||
rl::BackwardKillWord | rl::BackwardKillPathComponent | rl::BackwardKillBigword => {
|
||||
rl::ForwardWordEmacs
|
||||
| rl::ForwardBigwordEmacs
|
||||
| rl::KillWordEmacs
|
||||
| rl::KillBigwordEmacs
|
||||
| rl::NextdOrForwardWordEmacs => {
|
||||
if c == rl::PrevdOrBackwardWord && self.command_line.is_empty() {
|
||||
self.eval_bind_cmd(L!("prevd"));
|
||||
self.schedule_prompt_repaint();
|
||||
return;
|
||||
}
|
||||
let (elt, el) = self.active_edit_line();
|
||||
let is_kill = matches!(c, rl::KillWordEmacs | rl::KillBigwordEmacs);
|
||||
let style = match c {
|
||||
rl::BackwardKillBigword => MoveWordStyle::Whitespace,
|
||||
rl::BackwardKillPathComponent => MoveWordStyle::PathComponents,
|
||||
rl::BackwardKillWord => MoveWordStyle::Punctuation,
|
||||
rl::ForwardWordEmacs | rl::NextdOrForwardWordEmacs | rl::KillWordEmacs => {
|
||||
MoveWordStyle::Punctuation
|
||||
}
|
||||
rl::ForwardBigwordEmacs | rl::KillBigwordEmacs => MoveWordStyle::Whitespace,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let is_word_end = el.position() + 1 < el.len()
|
||||
&& !is_blank(el.at(el.position()))
|
||||
&& is_blank(el.at(el.position() + 1));
|
||||
|
||||
// if at word end, forward/kill char, otherwise forward/kill word end
|
||||
if self.is_at_autosuggestion() {
|
||||
self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle {
|
||||
style,
|
||||
to_word_end: true,
|
||||
});
|
||||
} else if is_word_end {
|
||||
if is_kill {
|
||||
self.delete_char(/*backward*/ false);
|
||||
} else {
|
||||
let (_elt, el) = self.active_edit_line_mut();
|
||||
el.set_position(el.position() + 1);
|
||||
}
|
||||
} else {
|
||||
self.data.move_word(
|
||||
elt,
|
||||
MoveWordDir::Right,
|
||||
is_kill,
|
||||
style,
|
||||
self.rls().last_cmd != Some(c),
|
||||
true,
|
||||
);
|
||||
if !is_kill {
|
||||
let (_elt, el) = self.active_edit_line_mut();
|
||||
if el.position() + 1 < el.len() {
|
||||
el.set_position(el.position() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rl::BackwardKillWord
|
||||
| rl::BackwardKillPathComponent
|
||||
| rl::BackwardKillBigword
|
||||
| rl::BackwardKillWordEnd
|
||||
| rl::BackwardKillBigwordEnd => {
|
||||
let style = match c {
|
||||
rl::BackwardKillWord | rl::BackwardKillWordEnd => MoveWordStyle::Punctuation,
|
||||
rl::BackwardKillBigword | rl::BackwardKillBigwordEnd => {
|
||||
MoveWordStyle::Whitespace
|
||||
}
|
||||
rl::BackwardKillPathComponent => MoveWordStyle::PathComponents,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let to_word_end = matches!(c, rl::BackwardKillWordEnd | rl::BackwardKillBigwordEnd);
|
||||
// Is this the same killring item as the last kill?
|
||||
let newv = !matches!(
|
||||
self.rls().last_cmd,
|
||||
Some(
|
||||
rl::BackwardKillWord
|
||||
| rl::BackwardKillPathComponent
|
||||
| rl::BackwardKillBigword
|
||||
)
|
||||
Some(rl::BackwardKillWord | rl::BackwardKillBigword)
|
||||
);
|
||||
self.data.move_word(
|
||||
self.active_edit_line_tag(),
|
||||
@@ -3425,25 +3641,52 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
|
||||
/*erase=*/ true,
|
||||
style,
|
||||
newv,
|
||||
to_word_end,
|
||||
)
|
||||
}
|
||||
rl::KillWord | rl::KillPathComponent | rl::KillBigword => {
|
||||
rl::KillWordVi
|
||||
| rl::KillBigwordVi
|
||||
| rl::KillPathComponent
|
||||
| rl::KillWordEnd
|
||||
| rl::KillBigwordEnd => {
|
||||
// The "bigword" functions differ only in that they move to the next whitespace, not
|
||||
// punctuation.
|
||||
let style = match c {
|
||||
rl::KillBigword => MoveWordStyle::Whitespace,
|
||||
rl::KillWordVi | rl::KillWordEnd => MoveWordStyle::Punctuation,
|
||||
rl::KillBigwordVi | rl::KillBigwordEnd => MoveWordStyle::Whitespace,
|
||||
rl::KillPathComponent => MoveWordStyle::PathComponents,
|
||||
rl::KillWord => MoveWordStyle::Punctuation,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let to_word_end = matches!(c, rl::KillWordEnd | rl::KillBigwordEnd);
|
||||
self.data.move_word(
|
||||
self.active_edit_line_tag(),
|
||||
MoveWordDir::Right,
|
||||
/*erase=*/ true,
|
||||
style,
|
||||
self.rls().last_cmd != Some(c),
|
||||
to_word_end,
|
||||
);
|
||||
}
|
||||
rl::KillInnerWord | rl::KillInnerBigWord => {
|
||||
let style = match c {
|
||||
rl::KillInnerBigWord => MoveWordStyle::Whitespace,
|
||||
rl::KillInnerWord => MoveWordStyle::Punctuation,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let elt = self.active_edit_line_tag();
|
||||
let newv = self.rls().last_cmd != Some(c);
|
||||
self.data.delete_inner_word(elt, style, newv);
|
||||
}
|
||||
rl::KillAWord | rl::KillABigWord => {
|
||||
let style = match c {
|
||||
rl::KillABigWord => MoveWordStyle::Whitespace,
|
||||
rl::KillAWord => MoveWordStyle::Punctuation,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let elt = self.active_edit_line_tag();
|
||||
let newv = self.rls().last_cmd != Some(c);
|
||||
self.data.delete_a_word(elt, style, newv);
|
||||
}
|
||||
rl::BackwardKillToken => {
|
||||
let Some(new_position) = self.backward_token() else {
|
||||
return;
|
||||
@@ -3506,51 +3749,59 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
|
||||
}
|
||||
}
|
||||
rl::BackwardWord
|
||||
| rl::BackwardWordEnd
|
||||
| rl::BackwardPathComponent
|
||||
| rl::BackwardBigword
|
||||
| rl::BackwardBigwordEnd
|
||||
| rl::PrevdOrBackwardWord => {
|
||||
if c == rl::PrevdOrBackwardWord && self.command_line.is_empty() {
|
||||
self.eval_bind_cmd(L!("prevd"));
|
||||
self.schedule_prompt_repaint();
|
||||
return;
|
||||
}
|
||||
let to_word_end = matches!(c, rl::BackwardWordEnd | rl::BackwardBigwordEnd);
|
||||
|
||||
let style = match c {
|
||||
rl::BackwardBigword => MoveWordStyle::Whitespace,
|
||||
rl::BackwardWord | rl::BackwardWordEnd | rl::PrevdOrBackwardWord => {
|
||||
MoveWordStyle::Punctuation
|
||||
}
|
||||
rl::BackwardBigword | rl::BackwardBigwordEnd => MoveWordStyle::Whitespace,
|
||||
rl::BackwardPathComponent => MoveWordStyle::PathComponents,
|
||||
rl::BackwardWord | rl::PrevdOrBackwardWord => MoveWordStyle::Punctuation,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.data.move_word(
|
||||
self.active_edit_line_tag(),
|
||||
MoveWordDir::Left,
|
||||
/*erase=*/ false,
|
||||
style,
|
||||
false,
|
||||
to_word_end,
|
||||
);
|
||||
}
|
||||
rl::ForwardWord
|
||||
| rl::ForwardPathComponent
|
||||
| rl::ForwardBigword
|
||||
| rl::NextdOrForwardWord => {
|
||||
if c == rl::NextdOrForwardWord && self.command_line.is_empty() {
|
||||
self.eval_bind_cmd(L!("nextd"));
|
||||
self.schedule_prompt_repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
rl::ForwardWordVi
|
||||
| rl::ForwardBigwordVi
|
||||
| rl::ForwardWordEnd
|
||||
| rl::ForwardBigwordEnd
|
||||
| rl::ForwardPathComponent => {
|
||||
let style = match c {
|
||||
rl::ForwardBigword => MoveWordStyle::Whitespace,
|
||||
rl::ForwardWordVi | rl::ForwardWordEnd => MoveWordStyle::Punctuation,
|
||||
rl::ForwardBigwordVi | rl::ForwardBigwordEnd => MoveWordStyle::Whitespace,
|
||||
rl::ForwardPathComponent => MoveWordStyle::PathComponents,
|
||||
rl::ForwardWord | rl::NextdOrForwardWord => MoveWordStyle::Punctuation,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let to_word_end = matches!(c, rl::ForwardWordEnd | rl::ForwardBigwordEnd);
|
||||
|
||||
if self.is_at_autosuggestion() {
|
||||
self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle(style));
|
||||
self.accept_autosuggestion(AutosuggestionPortion::PerMoveWordStyle {
|
||||
style,
|
||||
to_word_end,
|
||||
});
|
||||
} else if !self.is_at_end() {
|
||||
let (elt, _el) = self.active_edit_line();
|
||||
self.move_word(elt, MoveWordDir::Right, /*erase=*/ false, style, false);
|
||||
self.move_word(
|
||||
elt,
|
||||
MoveWordDir::Right,
|
||||
/*erase=*/ false,
|
||||
style,
|
||||
false,
|
||||
to_word_end,
|
||||
);
|
||||
}
|
||||
}
|
||||
rl::BeginningOfHistory | rl::EndOfHistory => {
|
||||
@@ -3810,6 +4061,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
|
||||
false,
|
||||
MoveWordStyle::Punctuation,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
let (elt, el) = self.active_edit_line();
|
||||
let mut replacement = WString::new();
|
||||
@@ -5160,7 +5412,10 @@ fn get_autosuggestion_performer(
|
||||
enum AutosuggestionPortion {
|
||||
Count(usize),
|
||||
Line,
|
||||
PerMoveWordStyle(MoveWordStyle),
|
||||
PerMoveWordStyle {
|
||||
style: MoveWordStyle,
|
||||
to_word_end: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -5341,16 +5596,24 @@ fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) {
|
||||
suggested[..line_end].to_owned(),
|
||||
)
|
||||
}
|
||||
AutosuggestionPortion::PerMoveWordStyle(style) => {
|
||||
AutosuggestionPortion::PerMoveWordStyle { style, to_word_end } => {
|
||||
// Accept characters according to the specified style.
|
||||
let mut state = MoveWordStateMachine::new(style);
|
||||
|
||||
let state_machine_dir = if to_word_end {
|
||||
MoveWordDir::Left
|
||||
} else {
|
||||
MoveWordDir::Right
|
||||
};
|
||||
let mut state = MoveWordStateMachine::new(style, state_machine_dir);
|
||||
let have = search_string_range.len();
|
||||
let mut want = have;
|
||||
while want < autosuggestion_text.len() {
|
||||
if !state.consume_char(autosuggestion_text, want) {
|
||||
let consumed = state.consume_char(autosuggestion_text, want);
|
||||
if consumed {
|
||||
want += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
want += 1;
|
||||
}
|
||||
(
|
||||
search_string_range.end..search_string_range.end,
|
||||
@@ -5878,13 +6141,19 @@ fn command_ends_paging(c: ReadlineCmd, focused_on_search_field: bool) -> bool {
|
||||
}
|
||||
rl::BeginningOfLine
|
||||
| rl::EndOfLine
|
||||
| rl::ForwardWord
|
||||
| rl::ForwardBigwordVi
|
||||
| rl::ForwardWordVi
|
||||
| rl::KillBigwordVi
|
||||
| rl::KillWordVi
|
||||
| rl::ForwardWordEmacs
|
||||
| rl::ForwardBigwordEmacs
|
||||
| rl::KillWordEmacs
|
||||
| rl::KillBigwordEmacs
|
||||
| rl::BackwardWord
|
||||
| rl::ForwardBigword
|
||||
| rl::BackwardBigword
|
||||
| rl::ForwardToken
|
||||
| rl::BackwardToken
|
||||
| rl::NextdOrForwardWord
|
||||
| rl::NextdOrForwardWordEmacs
|
||||
| rl::PrevdOrBackwardWord
|
||||
| rl::DeleteChar
|
||||
| rl::BackwardDeleteChar
|
||||
@@ -5894,8 +6163,6 @@ fn command_ends_paging(c: ReadlineCmd, focused_on_search_field: bool) -> bool {
|
||||
| rl::BackwardKillLine
|
||||
| rl::KillWholeLine
|
||||
| rl::KillInnerLine
|
||||
| rl::KillWord
|
||||
| rl::KillBigword
|
||||
| rl::KillToken
|
||||
| rl::BackwardKillWord
|
||||
| rl::BackwardKillPathComponent
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::reader::is_backslashed;
|
||||
use crate::tokenizer::tok_is_string_character;
|
||||
use fish_wchar::word_char::{WordCharClass, is_blank};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MoveWordStyle {
|
||||
@@ -19,12 +22,14 @@ pub enum MoveWordDir {
|
||||
}
|
||||
|
||||
pub struct MoveWordStateMachine {
|
||||
direction: MoveWordDir,
|
||||
state: MoveWordState,
|
||||
}
|
||||
|
||||
enum MoveWordState {
|
||||
SmallWord(State<SmallWordMovementState>),
|
||||
PathComponent(State<PathComponentState>),
|
||||
PathComponentBackward(State<PathComponentBackwardState>),
|
||||
PathComponentForward(State<PathComponentForwardState>),
|
||||
BigWord(State<BigWordMovementState>),
|
||||
}
|
||||
|
||||
@@ -40,222 +45,321 @@ enum NonFinalState<S> {
|
||||
}
|
||||
|
||||
trait HasNextState {
|
||||
fn next_state(state: NonFinalState<&Self>, text: &wstr, idx: usize, c: char) -> State<Self>
|
||||
fn next_state(
|
||||
direction: MoveWordDir,
|
||||
state: NonFinalState<&Self>,
|
||||
text: &wstr,
|
||||
idx: usize,
|
||||
c: char,
|
||||
) -> State<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl MoveWordStateMachine {
|
||||
pub fn new(style: MoveWordStyle) -> Self {
|
||||
pub fn new(style: MoveWordStyle, direction: MoveWordDir) -> Self {
|
||||
use MoveWordState as MWS;
|
||||
use MoveWordStyle as Style;
|
||||
let state = match style {
|
||||
Style::Punctuation => MWS::SmallWord(State::Initial),
|
||||
Style::PathComponents => MWS::PathComponent(State::Initial),
|
||||
Style::PathComponents => match direction {
|
||||
MoveWordDir::Left => MWS::PathComponentBackward(State::Initial),
|
||||
MoveWordDir::Right => MWS::PathComponentForward(State::Initial),
|
||||
},
|
||||
Style::Whitespace => MWS::BigWord(State::Initial),
|
||||
};
|
||||
Self { state }
|
||||
Self { direction, state }
|
||||
}
|
||||
pub fn consume_char(&mut self, text: &wstr, idx: usize) -> bool {
|
||||
use MoveWordState as MWS;
|
||||
let direction = self.direction;
|
||||
match &mut self.state {
|
||||
MWS::SmallWord(state) => Self::to_next_state(state, text, idx),
|
||||
MWS::PathComponent(state) => Self::to_next_state(state, text, idx),
|
||||
MWS::BigWord(state) => Self::to_next_state(state, text, idx),
|
||||
MWS::SmallWord(state) => Self::to_next_state(direction, state, text, idx),
|
||||
MWS::PathComponentBackward(state) => Self::to_next_state(direction, state, text, idx),
|
||||
MWS::PathComponentForward(state) => Self::to_next_state(direction, state, text, idx),
|
||||
MWS::BigWord(state) => Self::to_next_state(direction, state, text, idx),
|
||||
}
|
||||
}
|
||||
fn to_next_state<MWS: HasNextState>(state: &mut State<MWS>, text: &wstr, idx: usize) -> bool {
|
||||
fn to_next_state<MWS: HasNextState>(
|
||||
direction: MoveWordDir,
|
||||
state: &mut State<MWS>,
|
||||
text: &wstr,
|
||||
idx: usize,
|
||||
) -> bool {
|
||||
let input_state = match &*state {
|
||||
State::Initial => NonFinalState::Initial,
|
||||
State::Live(s) => NonFinalState::Live(s),
|
||||
State::Final => panic!(),
|
||||
};
|
||||
let c = text.as_char_slice()[idx];
|
||||
*state = MWS::next_state(input_state, text, idx, c);
|
||||
*state = MWS::next_state(direction, input_state, text, idx, c);
|
||||
!matches!(*state, State::Final)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum SmallWordMovementState {
|
||||
Rest,
|
||||
WhitespaceRest,
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
trait WordMovementState: HasNextState {
|
||||
fn from_last_char_class(char_class: WordCharClass) -> Self;
|
||||
fn last_char_class(&self) -> WordCharClass;
|
||||
}
|
||||
|
||||
struct SmallWordMovementState {
|
||||
last_char_class: WordCharClass,
|
||||
}
|
||||
impl WordMovementState for SmallWordMovementState {
|
||||
fn from_last_char_class(last_char_class: WordCharClass) -> Self {
|
||||
Self { last_char_class }
|
||||
}
|
||||
fn last_char_class(&self) -> WordCharClass {
|
||||
self.last_char_class
|
||||
}
|
||||
}
|
||||
impl HasNextState for SmallWordMovementState {
|
||||
fn next_state(state: NonFinalState<&Self>, _text: &wstr, _idx: usize, c: char) -> State<Self> {
|
||||
use {NonFinalState as NFS, SmallWordMovementState as S};
|
||||
let final_state = State::Final;
|
||||
State::Live(match state {
|
||||
NFS::Initial => {
|
||||
if c.is_whitespace() {
|
||||
S::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
S::Alphanumeric
|
||||
} else {
|
||||
// Don't allow switching type (ws->nonws) after non-whitespace and
|
||||
// non-alphanumeric.
|
||||
S::Rest
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Rest) => {
|
||||
if c.is_whitespace() {
|
||||
// Consume only trailing whitespace.
|
||||
S::WhitespaceRest
|
||||
} else if c.is_alphanumeric() {
|
||||
// Consume only alnums.
|
||||
S::Alphanumeric
|
||||
} else {
|
||||
return final_state;
|
||||
}
|
||||
}
|
||||
NFS::Live(s @ S::WhitespaceRest) | NFS::Live(s @ S::Whitespace) => {
|
||||
// "whitespace" consumes whitespace and switches to alnums,
|
||||
// "whitespace_rest" only consumes whitespace.
|
||||
if c.is_whitespace() {
|
||||
// Consumed whitespace.
|
||||
*s
|
||||
} else {
|
||||
if !matches!(s, S::Whitespace) {
|
||||
return final_state;
|
||||
}
|
||||
if c.is_alphanumeric() {
|
||||
S::Alphanumeric
|
||||
} else {
|
||||
return final_state;
|
||||
}
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Alphanumeric) => {
|
||||
if c.is_alphanumeric() {
|
||||
S::Alphanumeric
|
||||
} else {
|
||||
return final_state;
|
||||
}
|
||||
}
|
||||
})
|
||||
fn next_state(
|
||||
direction: MoveWordDir,
|
||||
state: NonFinalState<&Self>,
|
||||
_text: &wstr,
|
||||
_idx: usize,
|
||||
c: char,
|
||||
) -> State<Self> {
|
||||
let char_class = WordCharClass::from_char(c);
|
||||
next_word_movement_state(direction, state, char_class)
|
||||
}
|
||||
}
|
||||
|
||||
// Consume a "word" of printable characters plus any leading whitespace.
|
||||
enum BigWordMovementState {
|
||||
Blank,
|
||||
Graph,
|
||||
struct BigWordMovementState {
|
||||
last_char_class: WordCharClass,
|
||||
}
|
||||
impl HasNextState for BigWordMovementState {
|
||||
fn next_state(state: NonFinalState<&Self>, _text: &wstr, _idx: usize, c: char) -> State<Self> {
|
||||
use {BigWordMovementState as S, NonFinalState as NFS};
|
||||
State::Live(match state {
|
||||
NFS::Initial => {
|
||||
// always consume the first character
|
||||
// If it's not whitespace, only consume those from here.
|
||||
if !c.is_whitespace() {
|
||||
S::Graph
|
||||
} else {
|
||||
// If it's whitespace, keep consuming whitespace until the graphs.
|
||||
S::Blank
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Blank) => {
|
||||
if c.is_whitespace() {
|
||||
// consumed whitespace
|
||||
S::Blank
|
||||
} else {
|
||||
S::Graph
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Graph) => {
|
||||
if !c.is_whitespace() {
|
||||
// consumed printable non-space
|
||||
S::Graph
|
||||
} else {
|
||||
return State::Final;
|
||||
}
|
||||
}
|
||||
})
|
||||
fn next_state(
|
||||
direction: MoveWordDir,
|
||||
state: NonFinalState<&Self>,
|
||||
_text: &wstr,
|
||||
_idx: usize,
|
||||
c: char,
|
||||
) -> State<Self> {
|
||||
let char_class = if c == '\n' {
|
||||
WordCharClass::Newline
|
||||
} else if is_blank(c) {
|
||||
WordCharClass::Blank
|
||||
} else {
|
||||
WordCharClass::Word
|
||||
};
|
||||
next_word_movement_state(direction, state, char_class)
|
||||
}
|
||||
}
|
||||
impl WordMovementState for BigWordMovementState {
|
||||
fn from_last_char_class(last_char_class: WordCharClass) -> Self {
|
||||
Self { last_char_class }
|
||||
}
|
||||
fn last_char_class(&self) -> WordCharClass {
|
||||
self.last_char_class
|
||||
}
|
||||
}
|
||||
|
||||
fn is_path_component_character(c: char) -> bool {
|
||||
tok_is_string_character(c, None) && !L!("/={,}'\":@#").as_char_slice().contains(&c)
|
||||
fn next_word_movement_state<WMS: WordMovementState>(
|
||||
direction: MoveWordDir,
|
||||
state: NonFinalState<&WMS>,
|
||||
char_class: WordCharClass,
|
||||
) -> State<WMS> {
|
||||
let last_char_class_state = match state {
|
||||
NonFinalState::Initial => None,
|
||||
NonFinalState::Live(wms) => Some(wms.last_char_class()),
|
||||
};
|
||||
if consume_char_word_movement(direction, last_char_class_state, char_class).is_break() {
|
||||
return State::Final;
|
||||
}
|
||||
State::Live(WMS::from_last_char_class(char_class))
|
||||
}
|
||||
|
||||
fn consume_char_word_movement(
|
||||
direction: MoveWordDir,
|
||||
last_char_class: Option<WordCharClass>,
|
||||
cur_class: WordCharClass,
|
||||
) -> ControlFlow<()> {
|
||||
enum Transition {
|
||||
Blank,
|
||||
Newline,
|
||||
SameClass,
|
||||
DifferentClass,
|
||||
}
|
||||
use Transition as T;
|
||||
let transition = if cur_class == WordCharClass::Blank {
|
||||
T::Blank
|
||||
} else if cur_class == WordCharClass::Newline {
|
||||
T::Newline
|
||||
} else if last_char_class == Some(cur_class) {
|
||||
T::SameClass
|
||||
} else {
|
||||
T::DifferentClass
|
||||
};
|
||||
|
||||
let Some(last_char_class) = last_char_class else {
|
||||
return ControlFlow::Continue(());
|
||||
};
|
||||
if match direction {
|
||||
MoveWordDir::Left => match last_char_class {
|
||||
WordCharClass::Blank => false,
|
||||
WordCharClass::Newline => matches!(transition, T::Newline),
|
||||
_ => matches!(transition, T::Blank | T::Newline | T::DifferentClass),
|
||||
},
|
||||
MoveWordDir::Right => match last_char_class {
|
||||
WordCharClass::Blank => {
|
||||
matches!(transition, T::SameClass | T::DifferentClass)
|
||||
}
|
||||
WordCharClass::Newline => {
|
||||
matches!(transition, T::Newline | T::SameClass | T::DifferentClass)
|
||||
}
|
||||
_ => matches!(transition, T::DifferentClass),
|
||||
},
|
||||
} {
|
||||
ControlFlow::Break(())
|
||||
} else {
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
|
||||
/// a path component contains either
|
||||
/// - [space +] end word
|
||||
/// - punc + end word
|
||||
/// - space + end punc
|
||||
#[derive(Clone, Copy)]
|
||||
enum PathComponentState {
|
||||
Whitespace,
|
||||
Separator,
|
||||
Slash,
|
||||
PathComponentCharacters,
|
||||
InitialSeparator,
|
||||
enum PathComponentTransition {
|
||||
Blank,
|
||||
PathComponent,
|
||||
Punctuation,
|
||||
}
|
||||
impl HasNextState for PathComponentState {
|
||||
fn next_state(state: NonFinalState<&Self>, text: &wstr, idx: usize, c: char) -> State<Self> {
|
||||
use {NonFinalState as NFS, PathComponentState as S};
|
||||
let is_escaped = is_backslashed(text, idx);
|
||||
let is_whitespace = c.is_whitespace() && !is_escaped;
|
||||
let is_path_component_character =
|
||||
is_path_component_character(c) || (c.is_whitespace() && is_escaped);
|
||||
|
||||
fn path_component_state_transition(
|
||||
text: &wstr,
|
||||
idx: usize,
|
||||
c: char,
|
||||
) -> ControlFlow<(), PathComponentTransition> {
|
||||
let escaped = is_backslashed(text, idx);
|
||||
if c == '\\' && !escaped {
|
||||
return ControlFlow::Break(());
|
||||
};
|
||||
|
||||
use PathComponentTransition as T;
|
||||
ControlFlow::Continue(if is_blank(c) && !escaped {
|
||||
T::Blank
|
||||
} else if is_path_component_character(c) || (is_blank(c) && escaped) {
|
||||
T::PathComponent
|
||||
} else {
|
||||
T::Punctuation
|
||||
})
|
||||
}
|
||||
|
||||
fn is_path_component_character(c: char) -> bool {
|
||||
tok_is_string_character(c, None) && !L!("/={,}'\":@#").as_char_slice().contains(&c)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PathComponentForwardState {
|
||||
PathComponent,
|
||||
Punctuation,
|
||||
Blank,
|
||||
}
|
||||
impl HasNextState for PathComponentForwardState {
|
||||
fn next_state(
|
||||
_direction: MoveWordDir,
|
||||
state: NonFinalState<&Self>,
|
||||
text: &wstr,
|
||||
idx: usize,
|
||||
c: char,
|
||||
) -> State<Self> {
|
||||
// Forward path component movement: skip current homogeneous component, stop at next.
|
||||
// Each component type (word, slash, punctuation, whitespace) is a unit.
|
||||
|
||||
let ControlFlow::Continue(transition) = path_component_state_transition(text, idx, c)
|
||||
else {
|
||||
return match state {
|
||||
NFS::Initial => State::Initial,
|
||||
NFS::Live(s) => State::Live(*s),
|
||||
};
|
||||
};
|
||||
|
||||
use {NonFinalState as NFS, PathComponentForwardState as S, PathComponentTransition as T};
|
||||
let final_state = State::Final;
|
||||
State::Live(match state {
|
||||
NFS::Initial | NFS::Live(S::PathComponent) => match transition {
|
||||
T::Blank => S::Blank,
|
||||
T::PathComponent => S::PathComponent,
|
||||
T::Punctuation => S::Punctuation,
|
||||
},
|
||||
NFS::Live(S::Punctuation) => match transition {
|
||||
T::Blank => S::Blank,
|
||||
T::PathComponent => return final_state,
|
||||
T::Punctuation => S::Punctuation,
|
||||
},
|
||||
NFS::Live(S::Blank) => match transition {
|
||||
T::Blank => S::Blank,
|
||||
T::PathComponent => return final_state,
|
||||
T::Punctuation => return final_state,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PathComponentBackwardState {
|
||||
Punctuation,
|
||||
PathComponent,
|
||||
Space,
|
||||
EndPathComponent,
|
||||
EndPunctuation,
|
||||
}
|
||||
impl HasNextState for PathComponentBackwardState {
|
||||
fn next_state(
|
||||
_direction: MoveWordDir,
|
||||
state: NonFinalState<&Self>,
|
||||
text: &wstr,
|
||||
idx: usize,
|
||||
c: char,
|
||||
) -> State<Self> {
|
||||
use {NonFinalState as NFS, PathComponentBackwardState as S, PathComponentTransition as T};
|
||||
|
||||
let ControlFlow::Continue(transition) = path_component_state_transition(text, idx, c)
|
||||
else {
|
||||
return match state {
|
||||
NFS::Initial => State::Initial,
|
||||
NFS::Live(s) => State::Live(*s),
|
||||
};
|
||||
};
|
||||
|
||||
let final_state = State::Final;
|
||||
|
||||
State::Live(match state {
|
||||
NFS::Initial => {
|
||||
if !is_path_component_character && !is_whitespace {
|
||||
S::InitialSeparator
|
||||
} else {
|
||||
if is_path_component_character {
|
||||
S::PathComponentCharacters
|
||||
} else {
|
||||
S::Whitespace
|
||||
}
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Whitespace) => {
|
||||
if is_whitespace {
|
||||
S::Whitespace
|
||||
} else if c == '/' {
|
||||
S::Slash // path component
|
||||
} else if is_path_component_character {
|
||||
S::PathComponentCharacters
|
||||
} else {
|
||||
S::Separator // path separator
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Separator) => {
|
||||
if !is_whitespace && !is_path_component_character {
|
||||
S::Separator
|
||||
} else {
|
||||
return final_state;
|
||||
}
|
||||
}
|
||||
NFS::Live(S::Slash) => {
|
||||
if c == '/' {
|
||||
S::Slash
|
||||
} else {
|
||||
S::PathComponentCharacters
|
||||
}
|
||||
}
|
||||
NFS::Live(S::PathComponentCharacters) => {
|
||||
if is_path_component_character {
|
||||
S::PathComponentCharacters
|
||||
} else {
|
||||
return final_state;
|
||||
}
|
||||
}
|
||||
NFS::Live(S::InitialSeparator) => {
|
||||
if is_path_component_character {
|
||||
S::PathComponentCharacters
|
||||
} else if is_whitespace {
|
||||
return final_state;
|
||||
} else {
|
||||
S::InitialSeparator
|
||||
}
|
||||
}
|
||||
NFS::Initial => match transition {
|
||||
T::Blank => S::Space,
|
||||
T::PathComponent => S::PathComponent,
|
||||
T::Punctuation => S::Punctuation,
|
||||
},
|
||||
NFS::Live(ls) => match ls {
|
||||
S::Punctuation => match transition {
|
||||
T::Blank => return final_state,
|
||||
T::PathComponent => S::EndPathComponent,
|
||||
T::Punctuation => S::Punctuation,
|
||||
},
|
||||
S::PathComponent => match transition {
|
||||
T::Blank => return final_state,
|
||||
T::PathComponent => S::EndPathComponent,
|
||||
T::Punctuation => return final_state,
|
||||
},
|
||||
S::Space => match transition {
|
||||
T::Blank => S::Space,
|
||||
T::PathComponent => S::PathComponent,
|
||||
T::Punctuation => S::EndPunctuation,
|
||||
},
|
||||
S::EndPathComponent => match transition {
|
||||
T::Blank => return final_state,
|
||||
T::PathComponent => S::EndPathComponent,
|
||||
T::Punctuation => return final_state,
|
||||
},
|
||||
S::EndPunctuation => match transition {
|
||||
T::Blank => return final_state,
|
||||
T::PathComponent => return final_state,
|
||||
T::Punctuation => S::EndPunctuation,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -301,7 +405,7 @@ macro_rules! validate {
|
||||
let direction = $direction;
|
||||
let (command, mut stops, mut idx, end) = setup(direction, $line);
|
||||
assert!(!command.is_empty());
|
||||
let new_sm = || MoveWordStateMachine::new($style);
|
||||
let new_sm = || MoveWordStateMachine::new($style, direction);
|
||||
let mut sm = new_sm();
|
||||
while idx != end {
|
||||
let word_idx = if direction == MoveWordDir::Left {
|
||||
@@ -352,27 +456,69 @@ macro_rules! validate {
|
||||
validate!(Left, PathComponents, "^aaa ^@@@ ^@@^aa^");
|
||||
validate!(Left, PathComponents, "^aa^@@ ^aa@@^a^");
|
||||
validate!(Left, PathComponents, r#"^a\ ^b\ c/^d"^e\ f"^g"#);
|
||||
validate!(Left, PathComponents, r#"^a\ ^b\ c/^d"^e\\\ f"^g"#);
|
||||
validate!(Left, PathComponents, r#"^a\"^bc^"#);
|
||||
|
||||
validate!(Left, PathComponents, "^/^foo/^bar/^baz/^");
|
||||
validate!(Left, PathComponents, "^echo ^--foo ^--bar^");
|
||||
validate!(Left, PathComponents, "^echo ^hi ^> ^/^dev/^null^");
|
||||
validate!(Right, PathComponents, "^/^foo/^bar/^baz/^");
|
||||
validate!(Right, PathComponents, "^echo ^--foo ^--bar^");
|
||||
validate!(Right, PathComponents, "^echo ^hi ^> ^/^dev/^null^");
|
||||
validate!(Right, PathComponents, "^echo ^/^foo/^bar{^aaa,^ccc}^bak/^");
|
||||
validate!(Right, PathComponents, "^echo ^bak ^///^");
|
||||
validate!(Right, PathComponents, "^aaa ^@ ^@^aaa^");
|
||||
validate!(Right, PathComponents, "^aa@@ ^aa@@^a^");
|
||||
|
||||
// General punctuation tests
|
||||
validate!(Right, Punctuation, "^a^ bcd^");
|
||||
validate!(Right, Punctuation, "a^b^ cde^");
|
||||
validate!(Right, Punctuation, "^ab^ cde^");
|
||||
validate!(Right, Punctuation, "^ab^&cd^ ^& ^e^ f^&^");
|
||||
validate!(Left, Punctuation, "^a ^bcd^");
|
||||
validate!(Left, Punctuation, "^ab ^cd^e");
|
||||
validate!(Left, Punctuation, "^ab ^c^de");
|
||||
validate!(Left, Punctuation, "^ab ^cde^");
|
||||
validate!(Right, Punctuation, "^a ^bcd^");
|
||||
validate!(Right, Punctuation, "a^b ^cde^");
|
||||
validate!(Right, Punctuation, "ab^ ^cde^");
|
||||
validate!(Right, Punctuation, "^ab ^cde^");
|
||||
|
||||
validate!(Left, Punctuation, "^echo ^hello_^world.^txt^");
|
||||
validate!(Right, Punctuation, "^echo^ hello^_world^.txt^");
|
||||
validate!(Left, Punctuation, "^echo ^hello^_^world^.^txt^");
|
||||
validate!(Right, Punctuation, "^echo ^hello^_^world^.^txt^");
|
||||
|
||||
validate!(Left, Punctuation, "^echo ^foo_^foo_^foo/^/^/^/^/^ ^");
|
||||
validate!(Right, Punctuation, "^echo^ foo^_foo^_foo^/^/^/^/^/ ^");
|
||||
validate!(Left, Punctuation, "^echo ^foo^__^foo^_^foo^// ^");
|
||||
validate!(Right, Punctuation, "^echo ^foo^__^foo^_^foo^//^");
|
||||
|
||||
// General whitespace tests
|
||||
validate!(Right, Whitespace, "^a-b-c^ d-e-f^");
|
||||
validate!(Right, Whitespace, "^a-b-c^\n d-e-f^ ^");
|
||||
validate!(Right, Whitespace, "^a-b-c^\n\nd-e-f^ ^");
|
||||
validate!(Right, Punctuation, "^ab^&^cd ^& ^e ^f^&^");
|
||||
validate!(Left, Punctuation, "^ab^&^cd ^& ^e ^f^&^");
|
||||
|
||||
// General Whiltespace tests
|
||||
validate!(Left, Whitespace, "^a ^bcd^");
|
||||
validate!(Left, Whitespace, "^ab ^cd^e");
|
||||
validate!(Left, Whitespace, "^ab ^c^de");
|
||||
validate!(Left, Whitespace, "^ab ^cde^");
|
||||
validate!(Right, Whitespace, "^a ^bcd^");
|
||||
validate!(Right, Whitespace, "a^b ^cde^");
|
||||
validate!(Right, Whitespace, "ab^ ^cde^");
|
||||
validate!(Right, Whitespace, "^ab ^cde^");
|
||||
|
||||
// Newline-related tests
|
||||
validate!(Right, Punctuation, "^a \n ^bcd^");
|
||||
validate!(Left, Punctuation, "^a \n ^bcd^");
|
||||
validate!(Right, Whitespace, "^a \n ^bcd^");
|
||||
validate!(Left, Whitespace, "^a \n ^bcd^");
|
||||
|
||||
validate!(Right, Punctuation, "^a\n^\n^b^-^cd^");
|
||||
validate!(Left, Punctuation, "^a\n^\n^b^_^cd^");
|
||||
validate!(Right, Whitespace, "^a\n^\n^b_cd^");
|
||||
validate!(Left, Whitespace, "^a\n^\n^b_cd^");
|
||||
|
||||
validate!(Right, Punctuation, "^a \n \n \n^\n ^bcd^");
|
||||
validate!(Left, Punctuation, "^a \n \n \n^\n ^bcd^");
|
||||
validate!(Right, Whitespace, "^a \n \n \n^\n ^bcd^");
|
||||
validate!(Left, Whitespace, "^a \n \n \n^\n ^bcd^");
|
||||
|
||||
// Unicode-related tests
|
||||
validate!(Right, Punctuation, "^hello ^中^@^文^あいう^漢字^");
|
||||
validate!(Left, Punctuation, "^hello ^中^@^文^あいう^漢字^");
|
||||
validate!(Right, Whitespace, "^hello ^中文あいう漢字^");
|
||||
validate!(Left, Whitespace, "^hello ^中文あいう漢字^");
|
||||
|
||||
validate!(Right, Punctuation, "^café ^naïve^");
|
||||
validate!(Left, Punctuation, "^café ^naïve^");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,199 @@ expect_prompt(
|
||||
unmatched="Unexpected vi bind modes",
|
||||
)
|
||||
|
||||
# Test word movements
|
||||
# Test 'w' with underscore - should not jump over single punctuation
|
||||
send("echo abc_def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwD\r") # From start, 'w' twice should stop at underscore, delete from there
|
||||
expect_prompt(
|
||||
"\r\n.*abc\r\n", unmatched="vi mode 'w' should stop at single punctuation"
|
||||
)
|
||||
|
||||
# Test 'w' with multiple spaces - should skip spaces and land at start of next word
|
||||
send("echo abc def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwwD\r") # Skip 'echo', 'abc', 'def', then delete last char
|
||||
expect_prompt("\r\n.*abc de\r\n", unmatched="vi mode 'w' with multiple spaces")
|
||||
|
||||
# Test 'w' with multiple punctuations - should stop at punctuation group
|
||||
send("echo abc...def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwD\r") # Skip 'echo', then 'w' should stop at first '.', delete to end
|
||||
expect_prompt("\r\n.*abc\r\n", unmatched="vi mode 'w' with multiple punctuations")
|
||||
|
||||
# Test 'diw' when cursor is on space - should delete only spaces
|
||||
send("echo abc def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwhdiw\r") # Move to 'def', back to space, delete inner word (spaces only)
|
||||
expect_prompt(
|
||||
"\r\n.*abcdef\r\n", unmatched="vi mode 'diw' on space should delete spaces"
|
||||
)
|
||||
|
||||
# Test 'daw' - should delete word and trim trailing space
|
||||
send("echo abc def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwdaw\r") # Skip 'echo', move to 'def', delete word with space
|
||||
expect_prompt("\r\n.*abc ghi\r\n", unmatched="vi mode 'daw' should trim trailing space")
|
||||
|
||||
# Test 'b' backward movement with punctuation - should stop at punctuation
|
||||
send("echo abc_def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("bD\r") # From end, 'b' should stop at 'd', delete to end
|
||||
expect_prompt("\r\n.*abc_\r\n", unmatched="vi mode 'b' should stop at punctuation")
|
||||
|
||||
# Test 'e' end-of-word movement
|
||||
send("echo abc_def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0weD\r") # From start, 'w' to 'abc', 'e' to end of 'abc', delete to end
|
||||
expect_prompt("\r\n.*ab\r\n", unmatched="vi mode 'e' should move to word end")
|
||||
|
||||
# Test 'W' WORD movement - should skip punctuation within WORD
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wWD\r") # From start, 'w' to 'abc', 'W' should skip 'abc-def', delete 'ghi'
|
||||
expect_prompt(
|
||||
"\r\n.*abc-def\r\n",
|
||||
unmatched="vi mode 'W' should treat punctuation as part of WORD",
|
||||
)
|
||||
|
||||
# Test 'E' end-of-WORD movement
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wED\r") # From start, 'w' to 'abc', 'E' to end of 'abc-def', delete to end
|
||||
expect_prompt("\r\n.*abc-de\r\n", unmatched="vi mode 'E' should move to WORD end")
|
||||
|
||||
# Test 'B' backward WORD movement
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("BD\r") # From end, 'B' backward to 'ghi', delete to end
|
||||
expect_prompt("\r\n.*abc-def\r\n", unmatched="vi mode 'B' backward WORD movement")
|
||||
|
||||
# Test 'ge' backward to end of previous word
|
||||
send("echo abc def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwgex\r") # Move to 'def', 'ge' to 'c' of 'abc', delete char with 'x'
|
||||
expect_prompt(
|
||||
"\r\n.*ab def\r\n", unmatched="vi mode 'ge' should move to previous word end"
|
||||
)
|
||||
|
||||
# Test 'gE' backward to end of previous WORD
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send(
|
||||
"0WWgEx\r"
|
||||
) # Use 'W' to move by WORDs: to 'abc-def', then 'ghi', then 'gE' back to 'f' of 'abc-def', delete char
|
||||
expect_prompt(
|
||||
"\r\n.*abc-de ghi\r\n", unmatched="vi mode 'gE' should move to previous WORD end"
|
||||
)
|
||||
|
||||
# Test 'diW' (delete inner WORD) with punctuation
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wldiW\r") # Move to 'bc-def', delete inner WORD
|
||||
expect_prompt("\r\n.*ghi\r\n", unmatched="vi mode 'diW' should delete entire WORD")
|
||||
|
||||
# Test 'daW' (delete a WORD) with punctuation
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wldaW\r") # Move to 'bc-def', delete a WORD with space
|
||||
expect_prompt("\r\n.*ghi\r\n", unmatched="vi mode 'daW' should delete WORD and space")
|
||||
|
||||
# Test Unicode character category separation
|
||||
# In vim, different unicode categories are separated into words
|
||||
send("echo abcあいう")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwD\r") # Skip 'echo', then from 'a' of 'abc', 'w' should stop at 'あ'
|
||||
expect_prompt(
|
||||
"\r\n.*abc\r\n", unmatched="vi mode 'w' should stop at Unicode category boundary"
|
||||
)
|
||||
|
||||
# Test Unicode with multiple categories
|
||||
send("echo abcあいう甲乙")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wwwD\r") # Skip 'echo', 'abc', hiragana, then at kanji, delete to end
|
||||
expect_prompt(
|
||||
"\r\n.*abcあいう\r\n", unmatched="vi mode 'w' should separate hiragana and kanji"
|
||||
)
|
||||
|
||||
# Test 'cw' - change word, deletes to start of next word (like vim's 'dw')
|
||||
send("echo abc def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wcwXXX\r") # Move to 'abc', 'cw' deletes 'abc ' (including space), type 'XXX'
|
||||
expect_prompt(
|
||||
"\r\n.*XXXdef\r\n", unmatched="vi mode 'cw' should delete to start of next word"
|
||||
)
|
||||
|
||||
# Test 'ce' - change to end of word (like vim's 'de')
|
||||
send("echo abc def")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("0wceXXX\r") # Move to 'abc', 'ce' deletes 'abc' (not the space), type 'XXX'
|
||||
expect_prompt(
|
||||
"\r\n.*XXX def\r\n", unmatched="vi mode 'ce' should change to end of word"
|
||||
)
|
||||
|
||||
# Test 'cW' - change WORD, deletes to start of next WORD (like vim's 'dW')
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send(
|
||||
"0wcWXXX\r"
|
||||
) # Move to 'abc-def', 'cW' deletes 'abc-def ' (including space), type 'XXX'
|
||||
expect_prompt(
|
||||
"\r\n.*XXXghi\r\n", unmatched="vi mode 'cW' should delete to start of next WORD"
|
||||
)
|
||||
|
||||
# Test 'cE' - change to end of WORD (like vim's 'dE')
|
||||
send("echo abc-def ghi")
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send(
|
||||
"0wcEXXX\r"
|
||||
) # Move to 'abc-def', 'cE' deletes 'abc-def' (not the space), type 'XXX'
|
||||
expect_prompt(
|
||||
"\r\n.*XXX ghi\r\n", unmatched="vi mode 'cE' should change to end of WORD"
|
||||
)
|
||||
|
||||
# Test running commands on empty line (should not crash)
|
||||
send("\033")
|
||||
sleep(0.200)
|
||||
send("dawdiwdwdedgedgE\r") # run many commands
|
||||
expect_prompt()
|
||||
|
||||
# Test accepting autosuggestions with w/W
|
||||
sendline("echo test-suggestion test-suggestion")
|
||||
expect_prompt()
|
||||
send("echo te")
|
||||
sleep(0.100)
|
||||
send("\033") # Enter normal mode
|
||||
sleep(0.200)
|
||||
send("w") # forward-word-vi should accept 'st' from autosuggestion
|
||||
expect_str("echo test")
|
||||
send("w") # forward-word-vi should accept '-'
|
||||
expect_str("echo test-")
|
||||
send("w") # forward-word-vi should accept 'suggestion ' from autosuggestion
|
||||
expect_str("echo test-suggestion ")
|
||||
send("W\r") # forward-word-vi should accept 'test-suggestion' from autosuggestion
|
||||
expect_prompt("test-suggestion test-suggestion")
|
||||
|
||||
# Switch back to regular (emacs mode) key bindings.
|
||||
sendline("set -g fish_key_bindings fish_default_key_bindings")
|
||||
expect_prompt()
|
||||
|
||||
Reference in New Issue
Block a user