Adds word and sentence text objects along with a new vim testing system which uses cached neovim data to verify our test accuracy

K Simmons created

Change summary

Cargo.lock                                                         | 396 
assets/keymaps/vim.json                                            |  32 
crates/editor/src/display_map.rs                                   |  85 
crates/editor/src/editor.rs                                        |   4 
crates/editor/src/element.rs                                       |   2 
crates/editor/src/movement.rs                                      | 147 
crates/vim/Cargo.toml                                              |  17 
crates/vim/src/insert.rs                                           |   2 
crates/vim/src/motion.rs                                           |  34 
crates/vim/src/normal.rs                                           |  46 
crates/vim/src/normal/change.rs                                    |  27 
crates/vim/src/normal/delete.rs                                    |  69 
crates/vim/src/normal/yank.rs                                      |  27 
crates/vim/src/object.rs                                           | 488 
crates/vim/src/state.rs                                            |  14 
crates/vim/src/test_contexts.rs                                    |   9 
crates/vim/src/test_contexts/neovim_backed_binding_test_context.rs |  56 
crates/vim/src/test_contexts/neovim_backed_test_context.rs         | 374 
crates/vim/src/test_contexts/vim_binding_test_context.rs           |  69 
crates/vim/src/test_contexts/vim_test_context.rs                   |  66 
crates/vim/src/vim.rs                                              |  25 
crates/vim/src/visual.rs                                           |   6 
crates/vim/test_data/test_change_around_sentence/0text.txt         |   1 
crates/vim/test_data/test_change_around_sentence/10head.txt        |   1 
crates/vim/test_data/test_change_around_sentence/1head.txt         |   1 
crates/vim/test_data/test_change_around_sentence/2mode.txt         |   1 
crates/vim/test_data/test_change_around_sentence/3text.txt         |   1 
crates/vim/test_data/test_change_around_sentence/4head.txt         |   1 
crates/vim/test_data/test_change_around_sentence/5mode.txt         |   1 
crates/vim/test_data/test_change_around_sentence/6text.txt         |   1 
crates/vim/test_data/test_change_around_sentence/7head.txt         |   1 
crates/vim/test_data/test_change_around_sentence/8mode.txt         |   1 
crates/vim/test_data/test_change_around_sentence/9text.txt         |   1 
crates/vim/test_data/test_change_around_word/0text.txt             |  12 
crates/vim/test_data/test_change_around_word/100head.txt           |   1 
crates/vim/test_data/test_change_around_word/101mode.txt           |   1 
crates/vim/test_data/test_change_around_word/102text.txt           |  12 
crates/vim/test_data/test_change_around_word/103head.txt           |   1 
crates/vim/test_data/test_change_around_word/104mode.txt           |   1 
crates/vim/test_data/test_change_around_word/105text.txt           |  12 
crates/vim/test_data/test_change_around_word/106head.txt           |   1 
crates/vim/test_data/test_change_around_word/107mode.txt           |   1 
crates/vim/test_data/test_change_around_word/108text.txt           |  12 
crates/vim/test_data/test_change_around_word/109head.txt           |   1 
crates/vim/test_data/test_change_around_word/10head.txt            |   1 
crates/vim/test_data/test_change_around_word/110mode.txt           |   1 
crates/vim/test_data/test_change_around_word/111text.txt           |  12 
crates/vim/test_data/test_change_around_word/112head.txt           |   1 
crates/vim/test_data/test_change_around_word/113mode.txt           |   1 
crates/vim/test_data/test_change_around_word/114text.txt           |  12 
crates/vim/test_data/test_change_around_word/115head.txt           |   1 
crates/vim/test_data/test_change_around_word/116mode.txt           |   1 
crates/vim/test_data/test_change_around_word/117text.txt           |   9 
crates/vim/test_data/test_change_around_word/118head.txt           |   1 
crates/vim/test_data/test_change_around_word/119mode.txt           |   1 
crates/vim/test_data/test_change_around_word/11mode.txt            |   1 
crates/vim/test_data/test_change_around_word/120text.txt           |  10 
crates/vim/test_data/test_change_around_word/121head.txt           |   1 
crates/vim/test_data/test_change_around_word/122mode.txt           |   1 
crates/vim/test_data/test_change_around_word/123text.txt           |  11 
crates/vim/test_data/test_change_around_word/124head.txt           |   1 
crates/vim/test_data/test_change_around_word/125mode.txt           |   1 
crates/vim/test_data/test_change_around_word/126text.txt           |  12 
crates/vim/test_data/test_change_around_word/127head.txt           |   1 
crates/vim/test_data/test_change_around_word/128mode.txt           |   1 
crates/vim/test_data/test_change_around_word/129text.txt           |  12 
crates/vim/test_data/test_change_around_word/12text.txt            |  12 
crates/vim/test_data/test_change_around_word/130head.txt           |   1 
crates/vim/test_data/test_change_around_word/131mode.txt           |   1 
crates/vim/test_data/test_change_around_word/132text.txt           |  11 
crates/vim/test_data/test_change_around_word/133head.txt           |   1 
crates/vim/test_data/test_change_around_word/134mode.txt           |   1 
crates/vim/test_data/test_change_around_word/135text.txt           |  11 
crates/vim/test_data/test_change_around_word/136head.txt           |   1 
crates/vim/test_data/test_change_around_word/137mode.txt           |   1 
crates/vim/test_data/test_change_around_word/13head.txt            |   1 
crates/vim/test_data/test_change_around_word/14mode.txt            |   1 
crates/vim/test_data/test_change_around_word/15text.txt            |  12 
crates/vim/test_data/test_change_around_word/16head.txt            |   1 
crates/vim/test_data/test_change_around_word/17mode.txt            |   1 
crates/vim/test_data/test_change_around_word/18text.txt            |  11 
crates/vim/test_data/test_change_around_word/19head.txt            |   1 
crates/vim/test_data/test_change_around_word/1head.txt             |   1 
crates/vim/test_data/test_change_around_word/20mode.txt            |   1 
crates/vim/test_data/test_change_around_word/21text.txt            |  11 
crates/vim/test_data/test_change_around_word/22head.txt            |   1 
crates/vim/test_data/test_change_around_word/23mode.txt            |   1 
crates/vim/test_data/test_change_around_word/24text.txt            |  11 
crates/vim/test_data/test_change_around_word/25head.txt            |   1 
crates/vim/test_data/test_change_around_word/26mode.txt            |   1 
crates/vim/test_data/test_change_around_word/27text.txt            |  11 
crates/vim/test_data/test_change_around_word/28head.txt            |   1 
crates/vim/test_data/test_change_around_word/29mode.txt            |   1 
crates/vim/test_data/test_change_around_word/2mode.txt             |   1 
crates/vim/test_data/test_change_around_word/30text.txt            |  12 
crates/vim/test_data/test_change_around_word/31head.txt            |   1 
crates/vim/test_data/test_change_around_word/32mode.txt            |   1 
crates/vim/test_data/test_change_around_word/33text.txt            |  12 
crates/vim/test_data/test_change_around_word/34head.txt            |   1 
crates/vim/test_data/test_change_around_word/35mode.txt            |   1 
crates/vim/test_data/test_change_around_word/36text.txt            |  12 
crates/vim/test_data/test_change_around_word/37head.txt            |   1 
crates/vim/test_data/test_change_around_word/38mode.txt            |   1 
crates/vim/test_data/test_change_around_word/39text.txt            |  12 
crates/vim/test_data/test_change_around_word/3text.txt             |  12 
crates/vim/test_data/test_change_around_word/40head.txt            |   1 
crates/vim/test_data/test_change_around_word/41mode.txt            |   1 
crates/vim/test_data/test_change_around_word/42text.txt            |  12 
crates/vim/test_data/test_change_around_word/43head.txt            |   1 
crates/vim/test_data/test_change_around_word/44mode.txt            |   1 
crates/vim/test_data/test_change_around_word/45text.txt            |  12 
crates/vim/test_data/test_change_around_word/46head.txt            |   1 
crates/vim/test_data/test_change_around_word/47mode.txt            |   1 
crates/vim/test_data/test_change_around_word/48text.txt            |   9 
crates/vim/test_data/test_change_around_word/49head.txt            |   1 
crates/vim/test_data/test_change_around_word/4head.txt             |   1 
crates/vim/test_data/test_change_around_word/50mode.txt            |   1 
crates/vim/test_data/test_change_around_word/51text.txt            |  10 
crates/vim/test_data/test_change_around_word/52head.txt            |   1 
crates/vim/test_data/test_change_around_word/53mode.txt            |   1 
crates/vim/test_data/test_change_around_word/54text.txt            |  11 
crates/vim/test_data/test_change_around_word/55head.txt            |   1 
crates/vim/test_data/test_change_around_word/56mode.txt            |   1 
crates/vim/test_data/test_change_around_word/57text.txt            |  12 
crates/vim/test_data/test_change_around_word/58head.txt            |   1 
crates/vim/test_data/test_change_around_word/59mode.txt            |   1 
crates/vim/test_data/test_change_around_word/5mode.txt             |   1 
crates/vim/test_data/test_change_around_word/60text.txt            |  12 
crates/vim/test_data/test_change_around_word/61head.txt            |   1 
crates/vim/test_data/test_change_around_word/62mode.txt            |   1 
crates/vim/test_data/test_change_around_word/63text.txt            |  11 
crates/vim/test_data/test_change_around_word/64head.txt            |   1 
crates/vim/test_data/test_change_around_word/65mode.txt            |   1 
crates/vim/test_data/test_change_around_word/66text.txt            |  11 
crates/vim/test_data/test_change_around_word/67head.txt            |   1 
crates/vim/test_data/test_change_around_word/68mode.txt            |   1 
crates/vim/test_data/test_change_around_word/69text.txt            |  12 
crates/vim/test_data/test_change_around_word/6text.txt             |  11 
crates/vim/test_data/test_change_around_word/70head.txt            |   1 
crates/vim/test_data/test_change_around_word/71mode.txt            |   1 
crates/vim/test_data/test_change_around_word/72text.txt            |  12 
crates/vim/test_data/test_change_around_word/73head.txt            |   1 
crates/vim/test_data/test_change_around_word/74mode.txt            |   1 
crates/vim/test_data/test_change_around_word/75text.txt            |  11 
crates/vim/test_data/test_change_around_word/76head.txt            |   1 
crates/vim/test_data/test_change_around_word/77mode.txt            |   1 
crates/vim/test_data/test_change_around_word/78text.txt            |  12 
crates/vim/test_data/test_change_around_word/79head.txt            |   1 
crates/vim/test_data/test_change_around_word/7head.txt             |   1 
crates/vim/test_data/test_change_around_word/80mode.txt            |   1 
crates/vim/test_data/test_change_around_word/81text.txt            |  12 
crates/vim/test_data/test_change_around_word/82head.txt            |   1 
crates/vim/test_data/test_change_around_word/83mode.txt            |   1 
crates/vim/test_data/test_change_around_word/84text.txt            |  12 
crates/vim/test_data/test_change_around_word/85head.txt            |   1 
crates/vim/test_data/test_change_around_word/86mode.txt            |   1 
crates/vim/test_data/test_change_around_word/87text.txt            |  11 
crates/vim/test_data/test_change_around_word/88head.txt            |   1 
crates/vim/test_data/test_change_around_word/89mode.txt            |   1 
crates/vim/test_data/test_change_around_word/8mode.txt             |   1 
crates/vim/test_data/test_change_around_word/90text.txt            |  11 
crates/vim/test_data/test_change_around_word/91head.txt            |   1 
crates/vim/test_data/test_change_around_word/92mode.txt            |   1 
crates/vim/test_data/test_change_around_word/93text.txt            |  11 
crates/vim/test_data/test_change_around_word/94head.txt            |   1 
crates/vim/test_data/test_change_around_word/95mode.txt            |   1 
crates/vim/test_data/test_change_around_word/96text.txt            |  11 
crates/vim/test_data/test_change_around_word/97head.txt            |   1 
crates/vim/test_data/test_change_around_word/98mode.txt            |   1 
crates/vim/test_data/test_change_around_word/99text.txt            |  12 
crates/vim/test_data/test_change_around_word/9text.txt             |  12 
crates/vim/test_data/test_change_in_sentence/0text.txt             |   1 
crates/vim/test_data/test_change_in_sentence/10head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/11mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/12text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/13head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/14mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/15text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/16head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/17mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/18text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/19head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/1head.txt             |   1 
crates/vim/test_data/test_change_in_sentence/20mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/21text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/22head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/23mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/24text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/25head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/26mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/27text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/28head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/29mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/2mode.txt             |   1 
crates/vim/test_data/test_change_in_sentence/30text.txt            |   1 
crates/vim/test_data/test_change_in_sentence/31head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/32mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/33text.txt            |   2 
crates/vim/test_data/test_change_in_sentence/34head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/35mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/36text.txt            |   2 
crates/vim/test_data/test_change_in_sentence/37head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/38mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/39text.txt            |   2 
crates/vim/test_data/test_change_in_sentence/3text.txt             |   1 
crates/vim/test_data/test_change_in_sentence/40head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/41mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/42text.txt            |   2 
crates/vim/test_data/test_change_in_sentence/43head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/44mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/45text.txt            |   2 
crates/vim/test_data/test_change_in_sentence/46head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/47mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/48text.txt            |   4 
crates/vim/test_data/test_change_in_sentence/49head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/4head.txt             |   1 
crates/vim/test_data/test_change_in_sentence/50mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/51text.txt            |   3 
crates/vim/test_data/test_change_in_sentence/52head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/53mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/54text.txt            |   3 
crates/vim/test_data/test_change_in_sentence/55head.txt            |   1 
crates/vim/test_data/test_change_in_sentence/56mode.txt            |   1 
crates/vim/test_data/test_change_in_sentence/5mode.txt             |   1 
crates/vim/test_data/test_change_in_sentence/6text.txt             |   1 
crates/vim/test_data/test_change_in_sentence/7head.txt             |   1 
crates/vim/test_data/test_change_in_sentence/8mode.txt             |   1 
crates/vim/test_data/test_change_in_sentence/9text.txt             |   1 
crates/vim/test_data/test_change_in_word/0text.txt                 |  12 
crates/vim/test_data/test_change_in_word/100head.txt               |   1 
crates/vim/test_data/test_change_in_word/101mode.txt               |   1 
crates/vim/test_data/test_change_in_word/102text.txt               |  12 
crates/vim/test_data/test_change_in_word/103head.txt               |   1 
crates/vim/test_data/test_change_in_word/104mode.txt               |   1 
crates/vim/test_data/test_change_in_word/105text.txt               |  12 
crates/vim/test_data/test_change_in_word/106head.txt               |   1 
crates/vim/test_data/test_change_in_word/107mode.txt               |   1 
crates/vim/test_data/test_change_in_word/108text.txt               |  12 
crates/vim/test_data/test_change_in_word/109head.txt               |   1 
crates/vim/test_data/test_change_in_word/10head.txt                |   1 
crates/vim/test_data/test_change_in_word/110mode.txt               |   1 
crates/vim/test_data/test_change_in_word/111text.txt               |  12 
crates/vim/test_data/test_change_in_word/112head.txt               |   1 
crates/vim/test_data/test_change_in_word/113mode.txt               |   1 
crates/vim/test_data/test_change_in_word/114text.txt               |  12 
crates/vim/test_data/test_change_in_word/115head.txt               |   1 
crates/vim/test_data/test_change_in_word/116mode.txt               |   1 
crates/vim/test_data/test_change_in_word/117text.txt               |  12 
crates/vim/test_data/test_change_in_word/118head.txt               |   1 
crates/vim/test_data/test_change_in_word/119mode.txt               |   1 
crates/vim/test_data/test_change_in_word/11mode.txt                |   1 
crates/vim/test_data/test_change_in_word/120text.txt               |  12 
crates/vim/test_data/test_change_in_word/121head.txt               |   1 
crates/vim/test_data/test_change_in_word/122mode.txt               |   1 
crates/vim/test_data/test_change_in_word/123text.txt               |  12 
crates/vim/test_data/test_change_in_word/124head.txt               |   1 
crates/vim/test_data/test_change_in_word/125mode.txt               |   1 
crates/vim/test_data/test_change_in_word/126text.txt               |  12 
crates/vim/test_data/test_change_in_word/127head.txt               |   1 
crates/vim/test_data/test_change_in_word/128mode.txt               |   1 
crates/vim/test_data/test_change_in_word/129text.txt               |  12 
crates/vim/test_data/test_change_in_word/12text.txt                |  12 
crates/vim/test_data/test_change_in_word/130head.txt               |   1 
crates/vim/test_data/test_change_in_word/131mode.txt               |   1 
crates/vim/test_data/test_change_in_word/132text.txt               |  12 
crates/vim/test_data/test_change_in_word/133head.txt               |   1 
crates/vim/test_data/test_change_in_word/134mode.txt               |   1 
crates/vim/test_data/test_change_in_word/135text.txt               |  12 
crates/vim/test_data/test_change_in_word/136head.txt               |   1 
crates/vim/test_data/test_change_in_word/137mode.txt               |   1 
crates/vim/test_data/test_change_in_word/13head.txt                |   1 
crates/vim/test_data/test_change_in_word/14mode.txt                |   1 
crates/vim/test_data/test_change_in_word/15text.txt                |  12 
crates/vim/test_data/test_change_in_word/16head.txt                |   1 
crates/vim/test_data/test_change_in_word/17mode.txt                |   1 
crates/vim/test_data/test_change_in_word/18text.txt                |  12 
crates/vim/test_data/test_change_in_word/19head.txt                |   1 
crates/vim/test_data/test_change_in_word/1head.txt                 |   1 
crates/vim/test_data/test_change_in_word/20mode.txt                |   1 
crates/vim/test_data/test_change_in_word/21text.txt                |  12 
crates/vim/test_data/test_change_in_word/22head.txt                |   1 
crates/vim/test_data/test_change_in_word/23mode.txt                |   1 
crates/vim/test_data/test_change_in_word/24text.txt                |  12 
crates/vim/test_data/test_change_in_word/25head.txt                |   1 
crates/vim/test_data/test_change_in_word/26mode.txt                |   1 
crates/vim/test_data/test_change_in_word/27text.txt                |  12 
crates/vim/test_data/test_change_in_word/28head.txt                |   1 
crates/vim/test_data/test_change_in_word/29mode.txt                |   1 
crates/vim/test_data/test_change_in_word/2mode.txt                 |   1 
crates/vim/test_data/test_change_in_word/30text.txt                |  12 
crates/vim/test_data/test_change_in_word/31head.txt                |   1 
crates/vim/test_data/test_change_in_word/32mode.txt                |   1 
crates/vim/test_data/test_change_in_word/33text.txt                |  12 
crates/vim/test_data/test_change_in_word/34head.txt                |   1 
crates/vim/test_data/test_change_in_word/35mode.txt                |   1 
crates/vim/test_data/test_change_in_word/36text.txt                |  12 
crates/vim/test_data/test_change_in_word/37head.txt                |   1 
crates/vim/test_data/test_change_in_word/38mode.txt                |   1 
crates/vim/test_data/test_change_in_word/39text.txt                |  12 
crates/vim/test_data/test_change_in_word/3text.txt                 |  12 
crates/vim/test_data/test_change_in_word/40head.txt                |   1 
crates/vim/test_data/test_change_in_word/41mode.txt                |   1 
crates/vim/test_data/test_change_in_word/42text.txt                |  12 
crates/vim/test_data/test_change_in_word/43head.txt                |   1 
crates/vim/test_data/test_change_in_word/44mode.txt                |   1 
crates/vim/test_data/test_change_in_word/45text.txt                |  12 
crates/vim/test_data/test_change_in_word/46head.txt                |   1 
crates/vim/test_data/test_change_in_word/47mode.txt                |   1 
crates/vim/test_data/test_change_in_word/48text.txt                |  12 
crates/vim/test_data/test_change_in_word/49head.txt                |   1 
crates/vim/test_data/test_change_in_word/4head.txt                 |   1 
crates/vim/test_data/test_change_in_word/50mode.txt                |   1 
crates/vim/test_data/test_change_in_word/51text.txt                |  12 
crates/vim/test_data/test_change_in_word/52head.txt                |   1 
crates/vim/test_data/test_change_in_word/53mode.txt                |   1 
crates/vim/test_data/test_change_in_word/54text.txt                |  12 
crates/vim/test_data/test_change_in_word/55head.txt                |   1 
crates/vim/test_data/test_change_in_word/56mode.txt                |   1 
crates/vim/test_data/test_change_in_word/57text.txt                |  12 
crates/vim/test_data/test_change_in_word/58head.txt                |   1 
crates/vim/test_data/test_change_in_word/59mode.txt                |   1 
crates/vim/test_data/test_change_in_word/5mode.txt                 |   1 
crates/vim/test_data/test_change_in_word/60text.txt                |  12 
crates/vim/test_data/test_change_in_word/61head.txt                |   1 
crates/vim/test_data/test_change_in_word/62mode.txt                |   1 
crates/vim/test_data/test_change_in_word/63text.txt                |  12 
crates/vim/test_data/test_change_in_word/64head.txt                |   1 
crates/vim/test_data/test_change_in_word/65mode.txt                |   1 
crates/vim/test_data/test_change_in_word/66text.txt                |  12 
crates/vim/test_data/test_change_in_word/67head.txt                |   1 
crates/vim/test_data/test_change_in_word/68mode.txt                |   1 
crates/vim/test_data/test_change_in_word/69text.txt                |  12 
crates/vim/test_data/test_change_in_word/6text.txt                 |  12 
crates/vim/test_data/test_change_in_word/70head.txt                |   1 
crates/vim/test_data/test_change_in_word/71mode.txt                |   1 
crates/vim/test_data/test_change_in_word/72text.txt                |  12 
crates/vim/test_data/test_change_in_word/73head.txt                |   1 
crates/vim/test_data/test_change_in_word/74mode.txt                |   1 
crates/vim/test_data/test_change_in_word/75text.txt                |  12 
crates/vim/test_data/test_change_in_word/76head.txt                |   1 
crates/vim/test_data/test_change_in_word/77mode.txt                |   1 
crates/vim/test_data/test_change_in_word/78text.txt                |  12 
crates/vim/test_data/test_change_in_word/79head.txt                |   1 
crates/vim/test_data/test_change_in_word/7head.txt                 |   1 
crates/vim/test_data/test_change_in_word/80mode.txt                |   1 
crates/vim/test_data/test_change_in_word/81text.txt                |  12 
crates/vim/test_data/test_change_in_word/82head.txt                |   1 
crates/vim/test_data/test_change_in_word/83mode.txt                |   1 
crates/vim/test_data/test_change_in_word/84text.txt                |  12 
crates/vim/test_data/test_change_in_word/85head.txt                |   1 
crates/vim/test_data/test_change_in_word/86mode.txt                |   1 
crates/vim/test_data/test_change_in_word/87text.txt                |  12 
crates/vim/test_data/test_change_in_word/88head.txt                |   1 
crates/vim/test_data/test_change_in_word/89mode.txt                |   1 
crates/vim/test_data/test_change_in_word/8mode.txt                 |   1 
crates/vim/test_data/test_change_in_word/90text.txt                |  12 
crates/vim/test_data/test_change_in_word/91head.txt                |   1 
crates/vim/test_data/test_change_in_word/92mode.txt                |   1 
crates/vim/test_data/test_change_in_word/93text.txt                |  12 
crates/vim/test_data/test_change_in_word/94head.txt                |   1 
crates/vim/test_data/test_change_in_word/95mode.txt                |   1 
crates/vim/test_data/test_change_in_word/96text.txt                |  12 
crates/vim/test_data/test_change_in_word/97head.txt                |   1 
crates/vim/test_data/test_change_in_word/98mode.txt                |   1 
crates/vim/test_data/test_change_in_word/99text.txt                |  12 
crates/vim/test_data/test_change_in_word/9text.txt                 |  12 
crates/vim/test_data/test_delete_around_sentence/0text.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/10head.txt        |   1 
crates/vim/test_data/test_delete_around_sentence/1head.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/2mode.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/3text.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/4head.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/5mode.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/6text.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/7head.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/8mode.txt         |   1 
crates/vim/test_data/test_delete_around_sentence/9text.txt         |   1 
crates/vim/test_data/test_delete_around_word/0text.txt             |  12 
crates/vim/test_data/test_delete_around_word/100head.txt           |   1 
crates/vim/test_data/test_delete_around_word/101mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/102text.txt           |  12 
crates/vim/test_data/test_delete_around_word/103head.txt           |   1 
crates/vim/test_data/test_delete_around_word/104mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/105text.txt           |  12 
crates/vim/test_data/test_delete_around_word/106head.txt           |   1 
crates/vim/test_data/test_delete_around_word/107mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/108text.txt           |  12 
crates/vim/test_data/test_delete_around_word/109head.txt           |   1 
crates/vim/test_data/test_delete_around_word/10head.txt            |   1 
crates/vim/test_data/test_delete_around_word/110mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/111text.txt           |  12 
crates/vim/test_data/test_delete_around_word/112head.txt           |   1 
crates/vim/test_data/test_delete_around_word/113mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/114text.txt           |  12 
crates/vim/test_data/test_delete_around_word/115head.txt           |   1 
crates/vim/test_data/test_delete_around_word/116mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/117text.txt           |   9 
crates/vim/test_data/test_delete_around_word/118head.txt           |   1 
crates/vim/test_data/test_delete_around_word/119mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/11mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/120text.txt           |  10 
crates/vim/test_data/test_delete_around_word/121head.txt           |   1 
crates/vim/test_data/test_delete_around_word/122mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/123text.txt           |  11 
crates/vim/test_data/test_delete_around_word/124head.txt           |   1 
crates/vim/test_data/test_delete_around_word/125mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/126text.txt           |  12 
crates/vim/test_data/test_delete_around_word/127head.txt           |   1 
crates/vim/test_data/test_delete_around_word/128mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/129text.txt           |  12 
crates/vim/test_data/test_delete_around_word/12text.txt            |  12 
crates/vim/test_data/test_delete_around_word/130head.txt           |   1 
crates/vim/test_data/test_delete_around_word/131mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/132text.txt           |  11 
crates/vim/test_data/test_delete_around_word/133head.txt           |   1 
crates/vim/test_data/test_delete_around_word/134mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/135text.txt           |  11 
crates/vim/test_data/test_delete_around_word/136head.txt           |   1 
crates/vim/test_data/test_delete_around_word/137mode.txt           |   1 
crates/vim/test_data/test_delete_around_word/13head.txt            |   1 
crates/vim/test_data/test_delete_around_word/14mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/15text.txt            |  12 
crates/vim/test_data/test_delete_around_word/16head.txt            |   1 
crates/vim/test_data/test_delete_around_word/17mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/18text.txt            |  11 
crates/vim/test_data/test_delete_around_word/19head.txt            |   1 
crates/vim/test_data/test_delete_around_word/1head.txt             |   1 
crates/vim/test_data/test_delete_around_word/20mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/21text.txt            |  10 
crates/vim/test_data/test_delete_around_word/22head.txt            |   1 
crates/vim/test_data/test_delete_around_word/23mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/24text.txt            |  10 
crates/vim/test_data/test_delete_around_word/25head.txt            |   1 
crates/vim/test_data/test_delete_around_word/26mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/27text.txt            |  11 
crates/vim/test_data/test_delete_around_word/28head.txt            |   1 
crates/vim/test_data/test_delete_around_word/29mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/2mode.txt             |   1 
crates/vim/test_data/test_delete_around_word/30text.txt            |  12 
crates/vim/test_data/test_delete_around_word/31head.txt            |   1 
crates/vim/test_data/test_delete_around_word/32mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/33text.txt            |  12 
crates/vim/test_data/test_delete_around_word/34head.txt            |   1 
crates/vim/test_data/test_delete_around_word/35mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/36text.txt            |  12 
crates/vim/test_data/test_delete_around_word/37head.txt            |   1 
crates/vim/test_data/test_delete_around_word/38mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/39text.txt            |  12 
crates/vim/test_data/test_delete_around_word/3text.txt             |  12 
crates/vim/test_data/test_delete_around_word/40head.txt            |   1 
crates/vim/test_data/test_delete_around_word/41mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/42text.txt            |  12 
crates/vim/test_data/test_delete_around_word/43head.txt            |   1 
crates/vim/test_data/test_delete_around_word/44mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/45text.txt            |  12 
crates/vim/test_data/test_delete_around_word/46head.txt            |   1 
crates/vim/test_data/test_delete_around_word/47mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/48text.txt            |   9 
crates/vim/test_data/test_delete_around_word/49head.txt            |   1 
crates/vim/test_data/test_delete_around_word/4head.txt             |   1 
crates/vim/test_data/test_delete_around_word/50mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/51text.txt            |  10 
crates/vim/test_data/test_delete_around_word/52head.txt            |   1 
crates/vim/test_data/test_delete_around_word/53mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/54text.txt            |  11 
crates/vim/test_data/test_delete_around_word/55head.txt            |   1 
crates/vim/test_data/test_delete_around_word/56mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/57text.txt            |  12 
crates/vim/test_data/test_delete_around_word/58head.txt            |   1 
crates/vim/test_data/test_delete_around_word/59mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/5mode.txt             |   1 
crates/vim/test_data/test_delete_around_word/60text.txt            |  12 
crates/vim/test_data/test_delete_around_word/61head.txt            |   1 
crates/vim/test_data/test_delete_around_word/62mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/63text.txt            |  11 
crates/vim/test_data/test_delete_around_word/64head.txt            |   1 
crates/vim/test_data/test_delete_around_word/65mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/66text.txt            |  11 
crates/vim/test_data/test_delete_around_word/67head.txt            |   1 
crates/vim/test_data/test_delete_around_word/68mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/69text.txt            |  12 
crates/vim/test_data/test_delete_around_word/6text.txt             |  11 
crates/vim/test_data/test_delete_around_word/70head.txt            |   1 
crates/vim/test_data/test_delete_around_word/71mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/72text.txt            |  12 
crates/vim/test_data/test_delete_around_word/73head.txt            |   1 
crates/vim/test_data/test_delete_around_word/74mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/75text.txt            |  11 
crates/vim/test_data/test_delete_around_word/76head.txt            |   1 
crates/vim/test_data/test_delete_around_word/77mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/78text.txt            |  12 
crates/vim/test_data/test_delete_around_word/79head.txt            |   1 
crates/vim/test_data/test_delete_around_word/7head.txt             |   1 
crates/vim/test_data/test_delete_around_word/80mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/81text.txt            |  12 
crates/vim/test_data/test_delete_around_word/82head.txt            |   1 
crates/vim/test_data/test_delete_around_word/83mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/84text.txt            |  12 
crates/vim/test_data/test_delete_around_word/85head.txt            |   1 
crates/vim/test_data/test_delete_around_word/86mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/87text.txt            |  11 
crates/vim/test_data/test_delete_around_word/88head.txt            |   1 
crates/vim/test_data/test_delete_around_word/89mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/8mode.txt             |   1 
crates/vim/test_data/test_delete_around_word/90text.txt            |  10 
crates/vim/test_data/test_delete_around_word/91head.txt            |   1 
crates/vim/test_data/test_delete_around_word/92mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/93text.txt            |  10 
crates/vim/test_data/test_delete_around_word/94head.txt            |   1 
crates/vim/test_data/test_delete_around_word/95mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/96text.txt            |  11 
crates/vim/test_data/test_delete_around_word/97head.txt            |   1 
crates/vim/test_data/test_delete_around_word/98mode.txt            |   1 
crates/vim/test_data/test_delete_around_word/99text.txt            |  12 
crates/vim/test_data/test_delete_around_word/9text.txt             |  12 
crates/vim/test_data/test_delete_in_sentence/0text.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/10head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/11mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/12text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/13head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/14mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/15text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/16head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/17mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/18text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/19head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/1head.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/20mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/21text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/22head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/23mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/24text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/25head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/26mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/27text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/28head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/29mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/2mode.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/30text.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/31head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/32mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/33text.txt            |   2 
crates/vim/test_data/test_delete_in_sentence/34head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/35mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/36text.txt            |   2 
crates/vim/test_data/test_delete_in_sentence/37head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/38mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/39text.txt            |   2 
crates/vim/test_data/test_delete_in_sentence/3text.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/40head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/41mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/42text.txt            |   2 
crates/vim/test_data/test_delete_in_sentence/43head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/44mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/45text.txt            |   2 
crates/vim/test_data/test_delete_in_sentence/46head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/47mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/48text.txt            |   4 
crates/vim/test_data/test_delete_in_sentence/49head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/4head.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/50mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/51text.txt            |   3 
crates/vim/test_data/test_delete_in_sentence/52head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/53mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/54text.txt            |   3 
crates/vim/test_data/test_delete_in_sentence/55head.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/56mode.txt            |   1 
crates/vim/test_data/test_delete_in_sentence/5mode.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/6text.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/7head.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/8mode.txt             |   1 
crates/vim/test_data/test_delete_in_sentence/9text.txt             |   1 
crates/vim/test_data/test_delete_in_word/0text.txt                 |  12 
crates/vim/test_data/test_delete_in_word/100head.txt               |   1 
crates/vim/test_data/test_delete_in_word/101mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/102text.txt               |  12 
crates/vim/test_data/test_delete_in_word/103head.txt               |   1 
crates/vim/test_data/test_delete_in_word/104mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/105text.txt               |  12 
crates/vim/test_data/test_delete_in_word/106head.txt               |   1 
crates/vim/test_data/test_delete_in_word/107mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/108text.txt               |  12 
crates/vim/test_data/test_delete_in_word/109head.txt               |   1 
crates/vim/test_data/test_delete_in_word/10head.txt                |   1 
crates/vim/test_data/test_delete_in_word/110mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/111text.txt               |  12 
crates/vim/test_data/test_delete_in_word/112head.txt               |   1 
crates/vim/test_data/test_delete_in_word/113mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/114text.txt               |  12 
crates/vim/test_data/test_delete_in_word/115head.txt               |   1 
crates/vim/test_data/test_delete_in_word/116mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/117text.txt               |  12 
crates/vim/test_data/test_delete_in_word/118head.txt               |   1 
crates/vim/test_data/test_delete_in_word/119mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/11mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/120text.txt               |  12 
crates/vim/test_data/test_delete_in_word/121head.txt               |   1 
crates/vim/test_data/test_delete_in_word/122mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/123text.txt               |  12 
crates/vim/test_data/test_delete_in_word/124head.txt               |   1 
crates/vim/test_data/test_delete_in_word/125mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/126text.txt               |  12 
crates/vim/test_data/test_delete_in_word/127head.txt               |   1 
crates/vim/test_data/test_delete_in_word/128mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/129text.txt               |  12 
crates/vim/test_data/test_delete_in_word/12text.txt                |  12 
crates/vim/test_data/test_delete_in_word/130head.txt               |   1 
crates/vim/test_data/test_delete_in_word/131mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/132text.txt               |  12 
crates/vim/test_data/test_delete_in_word/133head.txt               |   1 
crates/vim/test_data/test_delete_in_word/134mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/135text.txt               |  12 
crates/vim/test_data/test_delete_in_word/136head.txt               |   1 
crates/vim/test_data/test_delete_in_word/137mode.txt               |   1 
crates/vim/test_data/test_delete_in_word/13head.txt                |   1 
crates/vim/test_data/test_delete_in_word/14mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/15text.txt                |  12 
crates/vim/test_data/test_delete_in_word/16head.txt                |   1 
crates/vim/test_data/test_delete_in_word/17mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/18text.txt                |  12 
crates/vim/test_data/test_delete_in_word/19head.txt                |   1 
crates/vim/test_data/test_delete_in_word/1head.txt                 |   1 
crates/vim/test_data/test_delete_in_word/20mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/21text.txt                |  12 
crates/vim/test_data/test_delete_in_word/22head.txt                |   1 
crates/vim/test_data/test_delete_in_word/23mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/24text.txt                |  12 
crates/vim/test_data/test_delete_in_word/25head.txt                |   1 
crates/vim/test_data/test_delete_in_word/26mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/27text.txt                |  12 
crates/vim/test_data/test_delete_in_word/28head.txt                |   1 
crates/vim/test_data/test_delete_in_word/29mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/2mode.txt                 |   1 
crates/vim/test_data/test_delete_in_word/30text.txt                |  12 
crates/vim/test_data/test_delete_in_word/31head.txt                |   1 
crates/vim/test_data/test_delete_in_word/32mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/33text.txt                |  12 
crates/vim/test_data/test_delete_in_word/34head.txt                |   1 
crates/vim/test_data/test_delete_in_word/35mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/36text.txt                |  12 
crates/vim/test_data/test_delete_in_word/37head.txt                |   1 
crates/vim/test_data/test_delete_in_word/38mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/39text.txt                |  12 
crates/vim/test_data/test_delete_in_word/3text.txt                 |  12 
crates/vim/test_data/test_delete_in_word/40head.txt                |   1 
crates/vim/test_data/test_delete_in_word/41mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/42text.txt                |  12 
crates/vim/test_data/test_delete_in_word/43head.txt                |   1 
crates/vim/test_data/test_delete_in_word/44mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/45text.txt                |  12 
crates/vim/test_data/test_delete_in_word/46head.txt                |   1 
crates/vim/test_data/test_delete_in_word/47mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/48text.txt                |  12 
crates/vim/test_data/test_delete_in_word/49head.txt                |   1 
crates/vim/test_data/test_delete_in_word/4head.txt                 |   1 
crates/vim/test_data/test_delete_in_word/50mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/51text.txt                |  12 
crates/vim/test_data/test_delete_in_word/52head.txt                |   1 
crates/vim/test_data/test_delete_in_word/53mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/54text.txt                |  12 
crates/vim/test_data/test_delete_in_word/55head.txt                |   1 
crates/vim/test_data/test_delete_in_word/56mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/57text.txt                |  12 
crates/vim/test_data/test_delete_in_word/58head.txt                |   1 
crates/vim/test_data/test_delete_in_word/59mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/5mode.txt                 |   1 
crates/vim/test_data/test_delete_in_word/60text.txt                |  12 
crates/vim/test_data/test_delete_in_word/61head.txt                |   1 
crates/vim/test_data/test_delete_in_word/62mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/63text.txt                |  12 
crates/vim/test_data/test_delete_in_word/64head.txt                |   1 
crates/vim/test_data/test_delete_in_word/65mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/66text.txt                |  12 
crates/vim/test_data/test_delete_in_word/67head.txt                |   1 
crates/vim/test_data/test_delete_in_word/68mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/69text.txt                |  12 
crates/vim/test_data/test_delete_in_word/6text.txt                 |  12 
crates/vim/test_data/test_delete_in_word/70head.txt                |   1 
crates/vim/test_data/test_delete_in_word/71mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/72text.txt                |  12 
crates/vim/test_data/test_delete_in_word/73head.txt                |   1 
crates/vim/test_data/test_delete_in_word/74mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/75text.txt                |  12 
crates/vim/test_data/test_delete_in_word/76head.txt                |   1 
crates/vim/test_data/test_delete_in_word/77mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/78text.txt                |  12 
crates/vim/test_data/test_delete_in_word/79head.txt                |   1 
crates/vim/test_data/test_delete_in_word/7head.txt                 |   1 
crates/vim/test_data/test_delete_in_word/80mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/81text.txt                |  12 
crates/vim/test_data/test_delete_in_word/82head.txt                |   1 
crates/vim/test_data/test_delete_in_word/83mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/84text.txt                |  12 
crates/vim/test_data/test_delete_in_word/85head.txt                |   1 
crates/vim/test_data/test_delete_in_word/86mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/87text.txt                |  12 
crates/vim/test_data/test_delete_in_word/88head.txt                |   1 
crates/vim/test_data/test_delete_in_word/89mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/8mode.txt                 |   1 
crates/vim/test_data/test_delete_in_word/90text.txt                |  12 
crates/vim/test_data/test_delete_in_word/91head.txt                |   1 
crates/vim/test_data/test_delete_in_word/92mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/93text.txt                |  12 
crates/vim/test_data/test_delete_in_word/94head.txt                |   1 
crates/vim/test_data/test_delete_in_word/95mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/96text.txt                |  12 
crates/vim/test_data/test_delete_in_word/97head.txt                |   1 
crates/vim/test_data/test_delete_in_word/98mode.txt                |   1 
crates/vim/test_data/test_delete_in_word/99text.txt                |  12 
crates/vim/test_data/test_delete_in_word/9text.txt                 |  12 
crates/vim/test_data/test_neovim/0text.txt                         |   1 
crates/vim/test_data/test_neovim/1head.txt                         |   1 
crates/vim/test_data/test_neovim/2mode.txt                         |   1 
crates/vim/test_data/test_word_text_object/0text.txt               |   9 
crates/vim/test_data/test_word_text_object/100head.txt             |   1 
crates/vim/test_data/test_word_text_object/101mode.txt             |   1 
crates/vim/test_data/test_word_text_object/102text.txt             |   9 
crates/vim/test_data/test_word_text_object/103head.txt             |   1 
crates/vim/test_data/test_word_text_object/104mode.txt             |   1 
crates/vim/test_data/test_word_text_object/105text.txt             |   9 
crates/vim/test_data/test_word_text_object/106head.txt             |   1 
crates/vim/test_data/test_word_text_object/107mode.txt             |   1 
crates/vim/test_data/test_word_text_object/108text.txt             |   9 
crates/vim/test_data/test_word_text_object/109head.txt             |   1 
crates/vim/test_data/test_word_text_object/10head.txt              |   1 
crates/vim/test_data/test_word_text_object/110mode.txt             |   1 
crates/vim/test_data/test_word_text_object/111text.txt             |   9 
crates/vim/test_data/test_word_text_object/112head.txt             |   1 
crates/vim/test_data/test_word_text_object/113mode.txt             |   1 
crates/vim/test_data/test_word_text_object/114text.txt             |   9 
crates/vim/test_data/test_word_text_object/115head.txt             |   1 
crates/vim/test_data/test_word_text_object/116mode.txt             |   1 
crates/vim/test_data/test_word_text_object/117text.txt             |   9 
crates/vim/test_data/test_word_text_object/118head.txt             |   1 
crates/vim/test_data/test_word_text_object/119mode.txt             |   1 
crates/vim/test_data/test_word_text_object/11mode.txt              |   1 
crates/vim/test_data/test_word_text_object/120text.txt             |   8 
crates/vim/test_data/test_word_text_object/121head.txt             |   1 
crates/vim/test_data/test_word_text_object/122mode.txt             |   1 
crates/vim/test_data/test_word_text_object/123text.txt             |   9 
crates/vim/test_data/test_word_text_object/124head.txt             |   1 
crates/vim/test_data/test_word_text_object/125mode.txt             |   1 
crates/vim/test_data/test_word_text_object/126text.txt             |   9 
crates/vim/test_data/test_word_text_object/127head.txt             |   1 
crates/vim/test_data/test_word_text_object/128mode.txt             |   1 
crates/vim/test_data/test_word_text_object/129text.txt             |   9 
crates/vim/test_data/test_word_text_object/12text.txt              |   9 
crates/vim/test_data/test_word_text_object/130head.txt             |   1 
crates/vim/test_data/test_word_text_object/131mode.txt             |   1 
crates/vim/test_data/test_word_text_object/132text.txt             |   8 
crates/vim/test_data/test_word_text_object/133head.txt             |   1 
crates/vim/test_data/test_word_text_object/134mode.txt             |   1 
crates/vim/test_data/test_word_text_object/135text.txt             |   8 
crates/vim/test_data/test_word_text_object/136head.txt             |   1 
crates/vim/test_data/test_word_text_object/137mode.txt             |   1 
crates/vim/test_data/test_word_text_object/138text.txt             |   8 
crates/vim/test_data/test_word_text_object/139head.txt             |   1 
crates/vim/test_data/test_word_text_object/13head.txt              |   1 
crates/vim/test_data/test_word_text_object/140mode.txt             |   1 
crates/vim/test_data/test_word_text_object/141text.txt             |   8 
crates/vim/test_data/test_word_text_object/142head.txt             |   1 
crates/vim/test_data/test_word_text_object/143mode.txt             |   1 
crates/vim/test_data/test_word_text_object/144text.txt             |   9 
crates/vim/test_data/test_word_text_object/145head.txt             |   1 
crates/vim/test_data/test_word_text_object/146mode.txt             |   1 
crates/vim/test_data/test_word_text_object/147text.txt             |   9 
crates/vim/test_data/test_word_text_object/148head.txt             |   1 
crates/vim/test_data/test_word_text_object/149mode.txt             |   1 
crates/vim/test_data/test_word_text_object/14mode.txt              |   1 
crates/vim/test_data/test_word_text_object/150text.txt             |   9 
crates/vim/test_data/test_word_text_object/151head.txt             |   1 
crates/vim/test_data/test_word_text_object/152mode.txt             |   1 
crates/vim/test_data/test_word_text_object/153text.txt             |   9 
crates/vim/test_data/test_word_text_object/154head.txt             |   1 
crates/vim/test_data/test_word_text_object/155mode.txt             |   1 
crates/vim/test_data/test_word_text_object/156text.txt             |   9 
crates/vim/test_data/test_word_text_object/157head.txt             |   1 
crates/vim/test_data/test_word_text_object/158mode.txt             |   1 
crates/vim/test_data/test_word_text_object/159text.txt             |   9 
crates/vim/test_data/test_word_text_object/15text.txt              |   9 
crates/vim/test_data/test_word_text_object/160head.txt             |   1 
crates/vim/test_data/test_word_text_object/161mode.txt             |   1 
crates/vim/test_data/test_word_text_object/162text.txt             |   8 
crates/vim/test_data/test_word_text_object/163head.txt             |   1 
crates/vim/test_data/test_word_text_object/164mode.txt             |   1 
crates/vim/test_data/test_word_text_object/165text.txt             |   9 
crates/vim/test_data/test_word_text_object/166head.txt             |   1 
crates/vim/test_data/test_word_text_object/167mode.txt             |   1 
crates/vim/test_data/test_word_text_object/168text.txt             |   9 
crates/vim/test_data/test_word_text_object/169head.txt             |   1 
crates/vim/test_data/test_word_text_object/16head.txt              |   1 
crates/vim/test_data/test_word_text_object/170mode.txt             |   1 
crates/vim/test_data/test_word_text_object/171text.txt             |   9 
crates/vim/test_data/test_word_text_object/172head.txt             |   1 
crates/vim/test_data/test_word_text_object/173mode.txt             |   1 
crates/vim/test_data/test_word_text_object/174text.txt             |   9 
crates/vim/test_data/test_word_text_object/175head.txt             |   1 
crates/vim/test_data/test_word_text_object/176mode.txt             |   1 
crates/vim/test_data/test_word_text_object/177text.txt             |   8 
crates/vim/test_data/test_word_text_object/178head.txt             |   1 
crates/vim/test_data/test_word_text_object/179mode.txt             |   1 
crates/vim/test_data/test_word_text_object/17mode.txt              |   1 
crates/vim/test_data/test_word_text_object/180text.txt             |   9 
crates/vim/test_data/test_word_text_object/181head.txt             |   1 
crates/vim/test_data/test_word_text_object/182mode.txt             |   1 
crates/vim/test_data/test_word_text_object/183text.txt             |   9 
crates/vim/test_data/test_word_text_object/184head.txt             |   1 
crates/vim/test_data/test_word_text_object/185mode.txt             |   1 
crates/vim/test_data/test_word_text_object/186text.txt             |   9 
crates/vim/test_data/test_word_text_object/187head.txt             |   1 
crates/vim/test_data/test_word_text_object/188mode.txt             |   1 
crates/vim/test_data/test_word_text_object/189text.txt             |   8 
crates/vim/test_data/test_word_text_object/18text.txt              |   9 
crates/vim/test_data/test_word_text_object/190head.txt             |   1 
crates/vim/test_data/test_word_text_object/191mode.txt             |   1 
crates/vim/test_data/test_word_text_object/192text.txt             |   8 
crates/vim/test_data/test_word_text_object/193head.txt             |   1 
crates/vim/test_data/test_word_text_object/194mode.txt             |   1 
crates/vim/test_data/test_word_text_object/195text.txt             |   8 
crates/vim/test_data/test_word_text_object/196head.txt             |   1 
crates/vim/test_data/test_word_text_object/197mode.txt             |   1 
crates/vim/test_data/test_word_text_object/198text.txt             |   8 
crates/vim/test_data/test_word_text_object/199head.txt             |   1 
crates/vim/test_data/test_word_text_object/19head.txt              |   1 
crates/vim/test_data/test_word_text_object/1head.txt               |   1 
crates/vim/test_data/test_word_text_object/200mode.txt             |   1 
crates/vim/test_data/test_word_text_object/201text.txt             |   9 
crates/vim/test_data/test_word_text_object/202head.txt             |   1 
crates/vim/test_data/test_word_text_object/203mode.txt             |   1 
crates/vim/test_data/test_word_text_object/204text.txt             |   9 
crates/vim/test_data/test_word_text_object/205head.txt             |   1 
crates/vim/test_data/test_word_text_object/206mode.txt             |   1 
crates/vim/test_data/test_word_text_object/207text.txt             |   9 
crates/vim/test_data/test_word_text_object/208head.txt             |   1 
crates/vim/test_data/test_word_text_object/209mode.txt             |   1 
crates/vim/test_data/test_word_text_object/20mode.txt              |   1 
crates/vim/test_data/test_word_text_object/210text.txt             |   9 
crates/vim/test_data/test_word_text_object/211head.txt             |   1 
crates/vim/test_data/test_word_text_object/212mode.txt             |   1 
crates/vim/test_data/test_word_text_object/213text.txt             |   9 
crates/vim/test_data/test_word_text_object/214head.txt             |   1 
crates/vim/test_data/test_word_text_object/215mode.txt             |   1 
crates/vim/test_data/test_word_text_object/216text.txt             |   9 
crates/vim/test_data/test_word_text_object/217head.txt             |   1 
crates/vim/test_data/test_word_text_object/218mode.txt             |   1 
crates/vim/test_data/test_word_text_object/219text.txt             |   8 
crates/vim/test_data/test_word_text_object/21text.txt              |   9 
crates/vim/test_data/test_word_text_object/220head.txt             |   1 
crates/vim/test_data/test_word_text_object/221mode.txt             |   1 
crates/vim/test_data/test_word_text_object/222text.txt             |   9 
crates/vim/test_data/test_word_text_object/223head.txt             |   1 
crates/vim/test_data/test_word_text_object/224mode.txt             |   1 
crates/vim/test_data/test_word_text_object/225text.txt             |   9 
crates/vim/test_data/test_word_text_object/226head.txt             |   1 
crates/vim/test_data/test_word_text_object/227mode.txt             |   1 
crates/vim/test_data/test_word_text_object/228text.txt             |   9 
crates/vim/test_data/test_word_text_object/229head.txt             |   1 
crates/vim/test_data/test_word_text_object/22head.txt              |   1 
crates/vim/test_data/test_word_text_object/230mode.txt             |   1 
crates/vim/test_data/test_word_text_object/231text.txt             |   9 
crates/vim/test_data/test_word_text_object/23mode.txt              |   1 
crates/vim/test_data/test_word_text_object/24text.txt              |   9 
crates/vim/test_data/test_word_text_object/25head.txt              |   1 
crates/vim/test_data/test_word_text_object/26mode.txt              |   1 
crates/vim/test_data/test_word_text_object/27text.txt              |   9 
crates/vim/test_data/test_word_text_object/28head.txt              |   1 
crates/vim/test_data/test_word_text_object/29mode.txt              |   1 
crates/vim/test_data/test_word_text_object/2mode.txt               |   1 
crates/vim/test_data/test_word_text_object/30text.txt              |   9 
crates/vim/test_data/test_word_text_object/31head.txt              |   1 
crates/vim/test_data/test_word_text_object/32mode.txt              |   1 
crates/vim/test_data/test_word_text_object/33text.txt              |   9 
crates/vim/test_data/test_word_text_object/34head.txt              |   1 
crates/vim/test_data/test_word_text_object/35mode.txt              |   1 
crates/vim/test_data/test_word_text_object/36text.txt              |   9 
crates/vim/test_data/test_word_text_object/37head.txt              |   1 
crates/vim/test_data/test_word_text_object/38mode.txt              |   1 
crates/vim/test_data/test_word_text_object/39text.txt              |   9 
crates/vim/test_data/test_word_text_object/3text.txt               |   9 
crates/vim/test_data/test_word_text_object/40head.txt              |   1 
crates/vim/test_data/test_word_text_object/41mode.txt              |   1 
crates/vim/test_data/test_word_text_object/42text.txt              |   9 
crates/vim/test_data/test_word_text_object/43head.txt              |   1 
crates/vim/test_data/test_word_text_object/44mode.txt              |   1 
crates/vim/test_data/test_word_text_object/45text.txt              |   9 
crates/vim/test_data/test_word_text_object/46head.txt              |   1 
crates/vim/test_data/test_word_text_object/47mode.txt              |   1 
crates/vim/test_data/test_word_text_object/48text.txt              |   9 
crates/vim/test_data/test_word_text_object/49head.txt              |   1 
crates/vim/test_data/test_word_text_object/4head.txt               |   1 
crates/vim/test_data/test_word_text_object/50mode.txt              |   1 
crates/vim/test_data/test_word_text_object/51text.txt              |   9 
crates/vim/test_data/test_word_text_object/52head.txt              |   1 
crates/vim/test_data/test_word_text_object/53mode.txt              |   1 
crates/vim/test_data/test_word_text_object/54text.txt              |   9 
crates/vim/test_data/test_word_text_object/55head.txt              |   1 
crates/vim/test_data/test_word_text_object/56mode.txt              |   1 
crates/vim/test_data/test_word_text_object/57text.txt              |   9 
crates/vim/test_data/test_word_text_object/58head.txt              |   1 
crates/vim/test_data/test_word_text_object/59mode.txt              |   1 
crates/vim/test_data/test_word_text_object/5mode.txt               |   1 
crates/vim/test_data/test_word_text_object/60text.txt              |   9 
crates/vim/test_data/test_word_text_object/61head.txt              |   1 
crates/vim/test_data/test_word_text_object/62mode.txt              |   1 
crates/vim/test_data/test_word_text_object/63text.txt              |   9 
crates/vim/test_data/test_word_text_object/64head.txt              |   1 
crates/vim/test_data/test_word_text_object/65mode.txt              |   1 
crates/vim/test_data/test_word_text_object/66text.txt              |   9 
crates/vim/test_data/test_word_text_object/67head.txt              |   1 
crates/vim/test_data/test_word_text_object/68mode.txt              |   1 
crates/vim/test_data/test_word_text_object/69text.txt              |   9 
crates/vim/test_data/test_word_text_object/6text.txt               |   9 
crates/vim/test_data/test_word_text_object/70head.txt              |   1 
crates/vim/test_data/test_word_text_object/71mode.txt              |   1 
crates/vim/test_data/test_word_text_object/72text.txt              |   9 
crates/vim/test_data/test_word_text_object/73head.txt              |   1 
crates/vim/test_data/test_word_text_object/74mode.txt              |   1 
crates/vim/test_data/test_word_text_object/75text.txt              |   9 
crates/vim/test_data/test_word_text_object/76head.txt              |   1 
crates/vim/test_data/test_word_text_object/77mode.txt              |   1 
crates/vim/test_data/test_word_text_object/78text.txt              |   9 
crates/vim/test_data/test_word_text_object/79head.txt              |   1 
crates/vim/test_data/test_word_text_object/7head.txt               |   1 
crates/vim/test_data/test_word_text_object/80mode.txt              |   1 
crates/vim/test_data/test_word_text_object/81text.txt              |   9 
crates/vim/test_data/test_word_text_object/82head.txt              |   1 
crates/vim/test_data/test_word_text_object/83mode.txt              |   1 
crates/vim/test_data/test_word_text_object/84text.txt              |   9 
crates/vim/test_data/test_word_text_object/85head.txt              |   1 
crates/vim/test_data/test_word_text_object/86mode.txt              |   1 
crates/vim/test_data/test_word_text_object/87text.txt              |   9 
crates/vim/test_data/test_word_text_object/88head.txt              |   1 
crates/vim/test_data/test_word_text_object/89mode.txt              |   1 
crates/vim/test_data/test_word_text_object/8mode.txt               |   1 
crates/vim/test_data/test_word_text_object/90text.txt              |   9 
crates/vim/test_data/test_word_text_object/91head.txt              |   1 
crates/vim/test_data/test_word_text_object/92mode.txt              |   1 
crates/vim/test_data/test_word_text_object/93text.txt              |   9 
crates/vim/test_data/test_word_text_object/94head.txt              |   1 
crates/vim/test_data/test_word_text_object/95mode.txt              |   1 
crates/vim/test_data/test_word_text_object/96text.txt              |   9 
crates/vim/test_data/test_word_text_object/97head.txt              |   1 
crates/vim/test_data/test_word_text_object/98mode.txt              |   1 
crates/vim/test_data/test_word_text_object/99text.txt              |   9 
crates/vim/test_data/test_word_text_object/9text.txt               |   9 
945 files changed, 5,198 insertions(+), 324 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8,7 +8,7 @@ version = "0.1.0"
 dependencies = [
  "auto_update",
  "editor",
- "futures",
+ "futures 0.3.24",
  "gpui",
  "language",
  "project",
@@ -52,9 +52,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.18"
+version = "0.7.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
 dependencies = [
  "memchr",
 ]
@@ -113,6 +113,15 @@ version = "0.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
 
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -124,9 +133,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.58"
+version = "1.0.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
 
 [[package]]
 name = "arrayref"
@@ -148,9 +157,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
 
 [[package]]
 name = "ascii"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
 
 [[package]]
 name = "assets"
@@ -174,20 +183,33 @@ dependencies = [
 
 [[package]]
 name = "async-channel"
-version = "1.6.1"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
+checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
 dependencies = [
  "concurrent-queue",
  "event-listener",
  "futures-core",
 ]
 
+[[package]]
+name = "async-compat"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b48b4ff0c2026db683dea961cd8ea874737f56cffca86fa84415eaddc51c00d"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "once_cell",
+ "pin-project-lite 0.2.9",
+ "tokio",
+]
+
 [[package]]
 name = "async-compression"
-version = "0.3.14"
+version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695"
+checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
 dependencies = [
  "flate2",
  "futures-core",
@@ -212,21 +234,23 @@ dependencies = [
 
 [[package]]
 name = "async-fs"
-version = "1.5.0"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
 dependencies = [
  "async-lock",
+ "autocfg 1.1.0",
  "blocking",
  "futures-lite",
 ]
 
 [[package]]
 name = "async-io"
-version = "1.7.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
+checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7"
 dependencies = [
+ "autocfg 1.1.0",
  "concurrent-queue",
  "futures-lite",
  "libc",
@@ -251,11 +275,12 @@ dependencies = [
 
 [[package]]
 name = "async-net"
-version = "1.6.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df"
+checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
 dependencies = [
  "async-io",
+ "autocfg 1.1.0",
  "blocking",
  "futures-lite",
 ]
@@ -265,17 +290,18 @@ name = "async-pipe"
 version = "0.1.3"
 source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553"
 dependencies = [
- "futures",
+ "futures 0.3.24",
  "log",
 ]
 
 [[package]]
 name = "async-process"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c"
+checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c"
 dependencies = [
  "async-io",
+ "autocfg 1.1.0",
  "blocking",
  "cfg-if 1.0.0",
  "event-listener",
@@ -338,9 +364,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.56"
+version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
+checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -435,15 +461,15 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "axum"
-version = "0.5.11"
+version = "0.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2cc6e8e8c993cb61a005fab8c1e5093a29199b7253b05a6883999312935c1ff"
+checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
 dependencies = [
  "async-trait",
  "axum-core",
  "base64",
  "bitflags",
- "bytes",
+ "bytes 1.2.1",
  "futures-util",
  "headers",
  "http",
@@ -470,26 +496,28 @@ dependencies = [
 
 [[package]]
 name = "axum-core"
-version = "0.2.6"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4d047478b986f14a13edad31a009e2e05cb241f9805d0d75e4cba4e129ad4d"
+checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b"
 dependencies = [
  "async-trait",
- "bytes",
+ "bytes 1.2.1",
  "futures-util",
  "http",
  "http-body",
  "mime",
+ "tower-layer",
+ "tower-service",
 ]
 
 [[package]]
 name = "axum-extra"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "277c75e6c814b061ae4947d02335d9659db9771b9950cca670002ae986372f44"
+checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
 dependencies = [
  "axum",
- "bytes",
+ "bytes 1.2.1",
  "futures-util",
  "http",
  "mime",
@@ -505,16 +533,16 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.65"
+version = "0.3.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
+checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
 dependencies = [
  "addr2line",
  "cc",
  "cfg-if 1.0.0",
  "libc",
- "miniz_oxide 0.5.3",
- "object",
+ "miniz_oxide 0.5.4",
+ "object 0.29.0",
  "rustc-demangle",
 ]
 
@@ -526,9 +554,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
 [[package]]
 name = "base64ct"
-version = "1.5.1"
+version = "1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851"
+checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474"
 
 [[package]]
 name = "bincode"
@@ -585,9 +613,9 @@ dependencies = [
 
 [[package]]
 name = "block-buffer"
-version = "0.10.2"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
 dependencies = [
  "generic-array",
 ]
@@ -645,15 +673,15 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.10.0"
+version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
 
 [[package]]
 name = "bytemuck"
-version = "1.10.0"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a"
+checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da"
 
 [[package]]
 name = "byteorder"
@@ -661,6 +689,16 @@ version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
+[[package]]
+name = "bytes"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
+dependencies = [
+ "byteorder",
+ "iovec",
+]
+
 [[package]]
 name = "bytes"
 version = "1.2.1"
@@ -758,12 +796,12 @@ dependencies = [
  "bindgen",
  "block",
  "byteorder",
- "bytes",
+ "bytes 1.2.1",
  "cocoa",
  "core-foundation",
  "core-graphics",
  "foreign-types",
- "futures",
+ "futures 0.3.24",
  "gpui",
  "hmac 0.12.1",
  "jwt",
@@ -774,7 +812,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "serde",
- "sha2 0.10.2",
+ "sha2 0.10.6",
  "simplelog",
 ]
 
@@ -825,21 +863,23 @@ dependencies = [
  "postage",
  "settings",
  "theme",
- "time 0.3.11",
+ "time 0.3.15",
  "util",
  "workspace",
 ]
 
 [[package]]
 name = "chrono"
-version = "0.4.19"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
 dependencies = [
- "libc",
+ "iana-time-zone",
+ "js-sys",
  "num-integer",
  "num-traits",
  "time 0.1.44",
+ "wasm-bindgen",
  "winapi 0.3.9",
 ]
 
@@ -860,9 +900,9 @@ dependencies = [
 
 [[package]]
 name = "clang-sys"
-version = "1.3.3"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b"
+checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
 dependencies = [
  "glob",
  "libc",
@@ -886,9 +926,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "3.2.8"
+version = "3.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
+checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
 dependencies = [
  "atty",
  "bitflags",
@@ -898,14 +938,14 @@ dependencies = [
  "once_cell",
  "strsim 0.10.0",
  "termcolor",
- "textwrap 0.15.0",
+ "textwrap 0.15.1",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "3.2.7"
+version = "3.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
 dependencies = [
  "heck 0.4.0",
  "proc-macro-error",
@@ -928,7 +968,7 @@ name = "cli"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "clap 3.2.8",
+ "clap 3.2.22",
  "core-foundation",
  "core-services",
  "dirs 3.0.2",
@@ -946,7 +986,7 @@ dependencies = [
  "async-tungstenite",
  "collections",
  "db",
- "futures",
+ "futures 0.3.24",
  "gpui",
  "image",
  "isahc",
@@ -961,11 +1001,11 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "thiserror",
- "time 0.3.11",
+ "time 0.3.15",
  "tiny_http",
  "url",
  "util",
- "uuid 1.1.2",
+ "uuid 1.2.1",
 ]
 
 [[package]]
@@ -1013,6 +1053,16 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
 [[package]]
 name = "collab"
 version = "0.1.0"
@@ -1023,14 +1073,14 @@ dependencies = [
  "axum",
  "axum-extra",
  "base64",
- "clap 3.2.8",
+ "clap 3.2.22",
  "client",
  "collections",
  "ctor",
  "editor",
  "env_logger",
  "envy",
- "futures",
+ "futures 0.3.24",
  "git",
  "gpui",
  "hyper",
@@ -1053,7 +1103,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.11",
+ "time 0.3.15",
  "tokio",
  "tokio-tungstenite",
  "toml",
@@ -1101,9 +1151,9 @@ dependencies = [
 
 [[package]]
 name = "concurrent-queue"
-version = "1.2.2"
+version = "1.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
+checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
 dependencies = [
  "cache-padded",
 ]
@@ -1116,7 +1166,7 @@ dependencies = [
  "client",
  "collections",
  "editor",
- "futures",
+ "futures 0.3.24",
  "fuzzy",
  "gpui",
  "language",
@@ -1140,7 +1190,7 @@ dependencies = [
  "client",
  "collections",
  "editor",
- "futures",
+ "futures 0.3.24",
  "fuzzy",
  "gpui",
  "language",
@@ -1236,27 +1286,27 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.2"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "cranelift-bforest"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7901fbba05decc537080b07cb3f1cadf53be7b7602ca8255786288a8692ae29a"
+checksum = "749d0d6022c9038dccf480bdde2a38d435937335bf2bb0f14e815d94517cdce8"
 dependencies = [
  "cranelift-entity",
 ]
 
 [[package]]
 name = "cranelift-codegen"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37ba1b45d243a4a28e12d26cd5f2507da74e77c45927d40de8b6ffbf088b46b5"
+checksum = "e94370cc7b37bf652ccd8bb8f09bd900997f7ccf97520edfc75554bb5c4abbea"
 dependencies = [
  "cranelift-bforest",
  "cranelift-codegen-meta",
@@ -1272,33 +1322,33 @@ dependencies = [
 
 [[package]]
 name = "cranelift-codegen-meta"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54cc30032171bf230ce22b99c07c3a1de1221cb5375bd6dbe6dbe77d0eed743c"
+checksum = "e0a3cea8fdab90e44018c5b9a1dfd460d8ee265ac354337150222a354628bdb6"
 dependencies = [
  "cranelift-codegen-shared",
 ]
 
 [[package]]
 name = "cranelift-codegen-shared"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a23f2672426d2bb4c9c3ef53e023076cfc4d8922f0eeaebaf372c92fae8b5c69"
+checksum = "5ac72f76f2698598951ab26d8c96eaa854810e693e7dd52523958b5909fde6b2"
 
 [[package]]
 name = "cranelift-entity"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "886c59a5e0de1f06dbb7da80db149c75de10d5e2caca07cdd9fef8a5918a6336"
+checksum = "09eaeacfcd2356fe0e66b295e8f9d59fdd1ac3ace53ba50de14d628ec902f72d"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "cranelift-frontend"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace74eeca11c439a9d4ed1a5cb9df31a54cd0f7fbddf82c8ce4ea8e9ad2a8fe0"
+checksum = "dba69c9980d5ffd62c18a2bde927855fcd7c8dc92f29feaf8636052662cbd99c"
 dependencies = [
  "cranelift-codegen",
  "log",
@@ -1308,15 +1358,15 @@ dependencies = [
 
 [[package]]
 name = "cranelift-isle"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db1ae52a5cc2cad0d86fdd3dcb16b7217d2f1e65ab4f5814aa4f014ad335fa43"
+checksum = "d2920dc1e05cac40304456ed3301fde2c09bd6a9b0210bcfa2f101398d628d5b"
 
 [[package]]
 name = "cranelift-native"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dadcfb7852900780d37102bce5698bcd401736403f07b52e714ff7a180e0e22f"
+checksum = "f04dfa45f9b2a6f587c564d6b63388e00cd6589d2df6ea2758cf79e1a13285e6"
 dependencies = [
  "cranelift-codegen",
  "libc",
@@ -1325,9 +1375,9 @@ dependencies = [
 
 [[package]]
 name = "cranelift-wasm"
-version = "0.85.1"
+version = "0.85.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c84e3410960389110b88f97776f39f6d2c8becdaa4cd59e390e6b76d9d0e7190"
+checksum = "31a46513ae6f26f3f267d8d75b5373d555fbbd1e68681f348d99df43f747ec54"
 dependencies = [
  "cranelift-codegen",
  "cranelift-entity",
@@ -1375,47 +1425,46 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.5"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c"
+checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.10",
+ "crossbeam-utils 0.8.12",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
+checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils 0.8.10",
+ "crossbeam-utils 0.8.12",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.9"
+version = "0.9.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d"
+checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
 dependencies = [
  "autocfg 1.1.0",
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.10",
+ "crossbeam-utils 0.8.12",
  "memoffset",
- "once_cell",
  "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2"
+checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.10",
+ "crossbeam-utils 0.8.12",
 ]
 
 [[package]]
@@ -1431,19 +1480,18 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.10"
+version = "0.8.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
+checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
 dependencies = [
  "cfg-if 1.0.0",
- "once_cell",
 ]
 
 [[package]]
 name = "crypto-common"
-version = "0.1.4"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
  "typenum",
@@ -1461,9 +1509,9 @@ dependencies = [
 
 [[package]]
 name = "ctor"
-version = "0.1.22"
+version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
+checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb"
 dependencies = [
  "quote",
  "syn",
@@ -1471,9 +1519,9 @@ dependencies = [
 
 [[package]]
 name = "curl"
-version = "0.4.43"
+version = "0.4.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
 dependencies = [
  "curl-sys",
  "libc",
@@ -1486,9 +1534,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.55+curl-7.83.1"
+version = "0.4.56+curl-7.83.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762"
+checksum = "6093e169dd4de29e468fa649fbae11cdcd5551c81fe5bf1b0677adad7ef3d26f"
 dependencies = [
  "cc",
  "libc",
@@ -1500,6 +1548,50 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "cxx"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "data-url"
 version = "0.1.1"
@@ -1534,13 +1626,13 @@ dependencies = [
 
 [[package]]
 name = "dhat"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47003dc9f6368a88e85956c3b2573a7e6872746a3e5d762a8885da3a136a0381"
+checksum = "0684eaa19a59be283a6f99369917b679bd4d1d06604b2eb2e2f87b4bbd67668d"
 dependencies = [
  "backtrace",
  "lazy_static",
- "parking_lot 0.11.2",
+ "parking_lot 0.12.1",
  "rustc-hash",
  "serde",
  "serde_json",
@@ -1579,11 +1671,11 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.10.3"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
 dependencies = [
- "block-buffer 0.10.2",
+ "block-buffer 0.10.3",
  "crypto-common",
  "subtle",
 ]
@@ -1649,10 +1741,13 @@ dependencies = [
 ]
 
 [[package]]
-name = "dotenv"
-version = "0.15.0"
+name = "dotenvy"
+version = "0.15.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314"
+dependencies = [
+ "dirs 4.0.0",
+]
 
 [[package]]
 name = "drag_and_drop"
@@ -1676,9 +1771,9 @@ dependencies = [
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.6"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "140206b78fb2bc3edbcfc9b5ccbd0b30699cfe8d348b8b31b330e47df5291a5a"
+checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
 
 [[package]]
 name = "easy-parallel"
@@ -1697,7 +1792,7 @@ dependencies = [
  "context_menu",
  "ctor",
  "env_logger",
- "futures",
+ "futures 0.3.24",
  "fuzzy",
  "git",
  "gpui",
@@ -1732,9 +1827,9 @@ dependencies = [
 
 [[package]]
 name = "either"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
 
 [[package]]
 name = "encoding_rs"
@@ -1747,9 +1842,9 @@ dependencies = [
 
 [[package]]
 name = "env_logger"
-version = "0.9.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
 dependencies = [
  "atty",
  "humantime",
@@ -1769,9 +1864,9 @@ dependencies = [
 
 [[package]]
 name = "erased-serde"
-version = "0.3.21"
+version = "0.3.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81d013529d5574a60caeda29e179e695125448e5de52e3874f7b4c1d7360e18e"
+checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11"
 dependencies = [
  "serde",
 ]
@@ -1818,9 +1913,9 @@ dependencies = [
 
 [[package]]
 name = "event-listener"
-version = "2.5.2"
+version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 
 [[package]]
 name = "expat-sys"
@@ -1840,9 +1935,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
 
 [[package]]
 name = "fastrand"
-version = "1.7.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
 dependencies = [
  "instant",
 ]
@@ -1890,7 +1985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
 dependencies = [
  "crc32fast",
- "miniz_oxide 0.5.3",
+ "miniz_oxide 0.5.4",
 ]
 
 [[package]]
@@ -1963,11 +2058,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
 name = "form_urlencoded"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
 dependencies = [
- "matches",
  "percent-encoding",
 ]
 
@@ -2046,9 +2140,15 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 
 [[package]]
 name = "futures"
-version = "0.3.21"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
+checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
+
+[[package]]
+name = "futures"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -2061,9 +2161,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -2071,15 +2171,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
+checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -2099,9 +2199,9 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.21"
+version = "0.3.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
 
 [[package]]
 name = "futures-lite"

assets/keymaps/vim.json 🔗

@@ -38,7 +38,23 @@
                 }
             ],
             "%": "vim::Matching",
-            "escape": "editor::Cancel"
+            "escape": "editor::Cancel",
+            "i": [
+                "vim::PushOperator",
+                {
+                    "Object": {
+                        "around": false
+                    }
+                }
+            ],
+            "a": [
+                "vim::PushOperator",
+                {
+                    "Object": {
+                        "around": true
+                    }
+                }
+            ]
         }
     },
     {
@@ -134,6 +150,20 @@
             "y": "vim::CurrentLine"
         }
     },
+    {
+        "context": "Editor && VimObject",
+        "bindings": {
+            "w": "vim::Word",
+            "shift-w": [
+                "vim::Word",
+                {
+                    "ignorePunctuation": true
+                }
+            ],
+            "s": "vim::Sentence",
+            "p": "vim::Paragraph"
+        }
+    },
     {
         "context": "Editor && vim_mode == visual",
         "bindings": {

crates/editor/src/display_map.rs 🔗

@@ -330,34 +330,91 @@ impl DisplaySnapshot {
         DisplayPoint(self.blocks_snapshot.max_point())
     }
 
+    /// Returns text chunks starting at the given display row until the end of the file
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         self.blocks_snapshot
             .chunks(display_row..self.max_point().row() + 1, false, None)
             .map(|h| h.text)
     }
 
+    // Returns text chunks starting at the end of the given display row in reverse until the start of the file
+    pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
+        (0..=display_row).into_iter().rev().flat_map(|row| {
+            self.blocks_snapshot
+                .chunks(row..row + 1, false, None)
+                .map(|h| h.text)
+                .collect::<Vec<_>>()
+                .into_iter()
+                .rev()
+        })
+    }
+
     pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
         self.blocks_snapshot
             .chunks(display_rows, language_aware, Some(&self.text_highlights))
     }
 
-    pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
-        let mut column = 0;
-        let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
-        while column < point.column() {
-            if let Some(c) = chars.next() {
-                column += c.len_utf8() as u32;
-            } else {
-                break;
-            }
-        }
-        chars
+    pub fn chars_at(
+        &self,
+        mut point: DisplayPoint,
+    ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+        point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
+        self.text_chunks(point.row())
+            .flat_map(str::chars)
+            .skip_while({
+                let mut column = 0;
+                move |char| {
+                    let at_point = column >= point.column();
+                    column += char.len_utf8() as u32;
+                    !at_point
+                }
+            })
+            .map(move |ch| {
+                let result = (ch, point);
+                if ch == '\n' {
+                    *point.row_mut() += 1;
+                    *point.column_mut() = 0;
+                } else {
+                    *point.column_mut() += ch.len_utf8() as u32;
+                }
+                result
+            })
+    }
+
+    pub fn reverse_chars_at(
+        &self,
+        mut point: DisplayPoint,
+    ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+        point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
+        self.reverse_text_chunks(point.row())
+            .flat_map(|chunk| chunk.chars().rev())
+            .skip_while({
+                let mut column = self.line_len(point.row());
+                if self.max_point().row() > point.row() {
+                    column += 1;
+                }
+
+                move |char| {
+                    let at_point = column <= point.column();
+                    column = column.saturating_sub(char.len_utf8() as u32);
+                    !at_point
+                }
+            })
+            .map(move |ch| {
+                if ch == '\n' {
+                    *point.row_mut() -= 1;
+                    *point.column_mut() = self.line_len(point.row());
+                } else {
+                    *point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
+                }
+                (ch, point)
+            })
     }
 
     pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
         let mut count = 0;
         let mut column = 0;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
+        for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
             if column >= target {
                 break;
             }
@@ -370,7 +427,7 @@ impl DisplaySnapshot {
     pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
         let mut column = 0;
 
-        for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
+        for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
             if c == '\n' || count >= char_count as usize {
                 break;
             }
@@ -454,7 +511,7 @@ impl DisplaySnapshot {
     pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
         let mut indent = 0;
         let mut is_blank = true;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
+        for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
             if c == ' ' {
                 indent += 1;
             } else {

crates/editor/src/editor.rs 🔗

@@ -4074,7 +4074,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
             s.move_cursors_with(|map, head, _| {
                 (
-                    movement::line_beginning(map, head, true),
+                    movement::indented_line_beginning(map, head, true),
                     SelectionGoal::None,
                 )
             });
@@ -4089,7 +4089,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
             s.move_heads_with(|map, head, _| {
                 (
-                    movement::line_beginning(map, head, action.stop_at_soft_wraps),
+                    movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
                     SelectionGoal::None,
                 )
             });

crates/editor/src/element.rs 🔗

@@ -752,7 +752,7 @@ impl EditorElement {
                                 .snapshot
                                 .chars_at(cursor_position)
                                 .next()
-                                .and_then(|character| {
+                                .and_then(|(character, _)| {
                                     let font_id =
                                         cursor_row_layout.font_for_index(cursor_column)?;
                                     let text = character.to_string();

crates/editor/src/movement.rs 🔗

@@ -101,6 +101,22 @@ pub fn line_beginning(
     map: &DisplaySnapshot,
     display_point: DisplayPoint,
     stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
+    let line_start = map.prev_line_boundary(point).1;
+
+    if stop_at_soft_boundaries && display_point != soft_line_start {
+        soft_line_start
+    } else {
+        line_start
+    }
+}
+
+pub fn indented_line_beginning(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    stop_at_soft_boundaries: bool,
 ) -> DisplayPoint {
     let point = display_point.to_point(map);
     let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
@@ -167,54 +183,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
     })
 }
 
-/// Scans for a boundary from the start of each line preceding the given end point until a boundary
-/// is found, indicated by the given predicate returning true. The predicate is called with the
-/// character to the left and right of the candidate boundary location, and will be called with `\n`
-/// characters indicating the start or end of a line. If the predicate returns true multiple times
-/// on a line, the *rightmost* boundary is returned.
+/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
+/// given predicate returning true. The predicate is called with the character to the left and right
+/// of the candidate boundary location, and will be called with `\n` characters indicating the start
+/// or end of a line.
 pub fn find_preceding_boundary(
     map: &DisplaySnapshot,
-    end: DisplayPoint,
+    from: DisplayPoint,
     mut is_boundary: impl FnMut(char, char) -> bool,
 ) -> DisplayPoint {
-    let mut point = end;
-    loop {
-        *point.column_mut() = 0;
-        if point.row() > 0 {
-            if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
-                *point.column_mut() = indent;
+    let mut start_column = 0;
+    let mut soft_wrap_row = from.row() + 1;
+
+    let mut prev = None;
+    for (ch, point) in map.reverse_chars_at(from) {
+        // Recompute soft_wrap_indent if the row has changed
+        if point.row() != soft_wrap_row {
+            soft_wrap_row = point.row();
+
+            if point.row() == 0 {
+                start_column = 0;
+            } else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
+                start_column = indent;
             }
         }
 
-        let mut boundary = None;
-        let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
-        for ch in map.chars_at(point) {
-            if point >= end {
-                break;
-            }
+        // If the current point is in the soft_wrap, skip comparing it
+        if point.column() < start_column {
+            continue;
+        }
 
-            if let Some(prev_ch) = prev_ch {
-                if is_boundary(prev_ch, ch) {
-                    boundary = Some(point);
-                }
+        if let Some((prev_ch, prev_point)) = prev {
+            if is_boundary(ch, prev_ch) {
+                return prev_point;
             }
+        }
 
-            if ch == '\n' {
-                break;
-            }
+        prev = Some((ch, point));
+    }
+    DisplayPoint::zero()
+}
 
-            prev_ch = Some(ch);
-            *point.column_mut() += ch.len_utf8() as u32;
+/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
+/// given predicate returning true. The predicate is called with the character to the left and right
+/// of the candidate boundary location, and will be called with `\n` characters indicating the start
+/// or end of a line. If no boundary is found, the start of the line is returned.
+pub fn find_preceding_boundary_in_line(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    let mut start_column = 0;
+    if from.row() > 0 {
+        if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
+            start_column = indent;
         }
+    }
 
-        if let Some(boundary) = boundary {
-            return boundary;
-        } else if point.row() == 0 {
-            return DisplayPoint::zero();
-        } else {
-            *point.row_mut() -= 1;
+    let mut prev = None;
+    for (ch, point) in map.reverse_chars_at(from) {
+        if let Some((prev_ch, prev_point)) = prev {
+            if is_boundary(ch, prev_ch) {
+                return prev_point;
+            }
         }
+
+        if ch == '\n' || point.column() < start_column {
+            break;
+        }
+
+        prev = Some((ch, point));
     }
+
+    prev.map(|(_, point)| point).unwrap_or(from)
 }
 
 /// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -223,26 +264,48 @@ pub fn find_preceding_boundary(
 /// or end of a line.
 pub fn find_boundary(
     map: &DisplaySnapshot,
-    mut point: DisplayPoint,
+    from: DisplayPoint,
     mut is_boundary: impl FnMut(char, char) -> bool,
 ) -> DisplayPoint {
     let mut prev_ch = None;
-    for ch in map.chars_at(point) {
+    for (ch, point) in map.chars_at(from) {
         if let Some(prev_ch) = prev_ch {
             if is_boundary(prev_ch, ch) {
-                break;
+                return map.clip_point(point, Bias::Right);
+            }
+        }
+
+        prev_ch = Some(ch);
+    }
+    map.clip_point(map.max_point(), Bias::Right)
+}
+
+/// Scans for a boundary following the given start point until a boundary is found, indicated by the
+/// given predicate returning true. The predicate is called with the character to the left and right
+/// of the candidate boundary location, and will be called with `\n` characters indicating the start
+/// or end of a line. If no boundary is found, the end of the line is returned
+pub fn find_boundary_in_line(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+    let mut prev = None;
+    for (ch, point) in map.chars_at(from) {
+        if let Some((prev_ch, _)) = prev {
+            if is_boundary(prev_ch, ch) {
+                return map.clip_point(point, Bias::Right);
             }
         }
 
+        prev = Some((ch, point));
+
         if ch == '\n' {
-            *point.row_mut() += 1;
-            *point.column_mut() = 0;
-        } else {
-            *point.column_mut() += ch.len_utf8() as u32;
+            break;
         }
-        prev_ch = Some(ch);
     }
-    map.clip_point(point, Bias::Right)
+
+    // Return the last position checked so that we give a point right before the newline or eof.
+    map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
 }
 
 pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {

crates/vim/Cargo.toml 🔗

@@ -7,7 +7,20 @@ edition = "2021"
 path = "src/vim.rs"
 doctest = false
 
+[features]
+neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
+
 [dependencies]
+serde = { version = "1.0", features = ["derive", "rc"] }
+itertools = "0.10"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
+
+async-compat = { version = "0.2.1", "optional" = true }
+async-trait = { version = "0.1", "optional" = true }
+nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
+tokio = { version = "1.15", "optional" = true }
+serde_json = { version = "1.0", features = ["preserve_order"] }
+
 assets = { path = "../assets" }
 collections = { path = "../collections" }
 command_palette = { path = "../command_palette" }
@@ -15,14 +28,12 @@ editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 search = { path = "../search" }
-serde = { version = "1.0", features = ["derive", "rc"] }
 settings = { path = "../settings" }
 workspace = { path = "../workspace" }
-itertools = "0.10"
-log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 
 [dev-dependencies]
 indoc = "1.0.4"
+
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }

crates/vim/src/insert.rs 🔗

@@ -26,7 +26,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
 
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, vim_test_context::VimTestContext};
+    use crate::{state::Mode, test_contexts::VimTestContext};
 
     #[gpui::test]
     async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {

crates/vim/src/motion.rs 🔗

@@ -206,7 +206,7 @@ impl Motion {
                 }
             }
 
-            selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
+            (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
         } else {
             // If the motion is exclusive and the end of the motion is in column 1, the
             // end of the motion is moved to the end of the previous line and the motion
@@ -239,12 +239,12 @@ fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     map.clip_point(point, Bias::Left)
 }
 
-fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
     *point.column_mut() += 1;
     map.clip_point(point, Bias::Right)
 }
 
-fn next_word_start(
+pub(crate) fn next_word_start(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     ignore_punctuation: bool,
@@ -255,7 +255,7 @@ fn next_word_start(
         let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
         let at_newline = right == '\n';
 
-        let found = (left_kind != right_kind && !right.is_whitespace())
+        let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
             || at_newline && crossed_newline
             || at_newline && left == '\n'; // Prevents skipping repeated empty lines
 
@@ -272,23 +272,28 @@ fn next_word_end(
     ignore_punctuation: bool,
 ) -> DisplayPoint {
     *point.column_mut() += 1;
+    dbg!(point);
     point = movement::find_boundary(map, point, |left, right| {
+        dbg!(left);
         let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
         let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
 
-        left_kind != right_kind && !left.is_whitespace()
+        left_kind != right_kind && left_kind != CharKind::Whitespace
     });
+
+    dbg!(point);
+
     // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
     // we have backtraced already
     if !map
         .chars_at(point)
         .nth(1)
-        .map(|c| c == '\n')
+        .map(|(c, _)| c == '\n')
         .unwrap_or(true)
     {
         *point.column_mut() = point.column().saturating_sub(1);
     }
-    map.clip_point(point, Bias::Left)
+    dbg!(map.clip_point(point, Bias::Left))
 }
 
 fn previous_word_start(
@@ -307,22 +312,21 @@ fn previous_word_start(
     point
 }
 
-fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
-    let mut column = 0;
-    for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
+fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
+    let mut last_point = DisplayPoint::new(from.row(), 0);
+    for (ch, point) in map.chars_at(last_point) {
         if ch == '\n' {
-            return point;
+            return from;
         }
 
+        last_point = point;
+
         if char_kind(ch) != CharKind::Whitespace {
             break;
         }
-
-        column += ch.len_utf8() as u32;
     }
 
-    *point.column_mut() = column;
-    map.clip_point(point, Bias::Left)
+    map.clip_point(last_point, Bias::Left)
 }
 
 fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {

crates/vim/src/normal.rs 🔗

@@ -6,6 +6,7 @@ use std::borrow::Cow;
 
 use crate::{
     motion::Motion,
+    object::Object,
     state::{Mode, Operator},
     Vim,
 };
@@ -16,7 +17,11 @@ use gpui::{actions, MutableAppContext, ViewContext};
 use language::{AutoindentMode, Point, SelectionGoal};
 use workspace::Workspace;
 
-use self::{change::change_over, delete::delete_over, yank::yank_over};
+use self::{
+    change::{change_motion, change_object},
+    delete::{delete_motion, delete_object},
+    yank::{yank_motion, yank_object},
+};
 
 actions!(
     vim,
@@ -43,22 +48,22 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(insert_line_below);
     cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
         Vim::update(cx, |vim, cx| {
-            delete_over(vim, Motion::Left, cx);
+            delete_motion(vim, Motion::Left, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
         Vim::update(cx, |vim, cx| {
-            delete_over(vim, Motion::Right, cx);
+            delete_motion(vim, Motion::Right, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
-            change_over(vim, Motion::EndOfLine, cx);
+            change_motion(vim, Motion::EndOfLine, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
-            delete_over(vim, Motion::EndOfLine, cx);
+            delete_motion(vim, Motion::EndOfLine, cx);
         })
     });
     cx.add_action(paste);
@@ -70,17 +75,36 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
     Vim::update(cx, |vim, cx| {
         match vim.state.operator_stack.pop() {
             None => move_cursor(vim, motion, cx),
-            Some(Operator::Namespace(_)) => {
-                // Can't do anything for a namespace operator. Ignoring
+            Some(Operator::Change) => change_motion(vim, motion, cx),
+            Some(Operator::Delete) => delete_motion(vim, motion, cx),
+            Some(Operator::Yank) => yank_motion(vim, motion, cx),
+            _ => {
+                // Can't do anything for text objects or namespace operators. Ignoring
             }
-            Some(Operator::Change) => change_over(vim, motion, cx),
-            Some(Operator::Delete) => delete_over(vim, motion, cx),
-            Some(Operator::Yank) => yank_over(vim, motion, cx),
         }
         vim.clear_operator(cx);
     });
 }
 
+pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
+    Vim::update(cx, |vim, cx| {
+        match vim.state.operator_stack.pop() {
+            Some(Operator::Object { around }) => match vim.state.operator_stack.pop() {
+                Some(Operator::Change) => change_object(vim, object, around, cx),
+                Some(Operator::Delete) => delete_object(vim, object, around, cx),
+                Some(Operator::Yank) => yank_object(vim, object, around, cx),
+                _ => {
+                    // Can't do anything for namespace operators. Ignoring
+                }
+            },
+            _ => {
+                // Can't do anything with change/delete/yank and text objects. Ignoring
+            }
+        }
+        vim.clear_operator(cx);
+    })
+}
+
 fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
@@ -304,7 +328,7 @@ mod test {
             Mode::{self, *},
             Namespace, Operator,
         },
-        vim_test_context::VimTestContext,
+        test_contexts::VimTestContext,
     };
 
     #[gpui::test]

crates/vim/src/normal/change.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
+use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
 use editor::{char_kind, movement, Autoscroll};
 use gpui::{impl_actions, MutableAppContext, ViewContext};
 use serde::Deserialize;
@@ -17,7 +17,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(change_word);
 }
 
-pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+pub fn change_motion(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             // We are swapping to insert mode anyway. Just set the line end clipping behavior now
@@ -34,6 +34,23 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     vim.switch_mode(Mode::Insert, false, cx)
 }
 
+pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.transact(cx, |editor, cx| {
+            // We are swapping to insert mode anyway. Just set the line end clipping behavior now
+            editor.set_clip_at_line_ends(false, cx);
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    object.expand_selection(map, selection, around);
+                });
+            });
+            copy_selections_content(editor, false, cx);
+            editor.insert("", cx);
+        });
+    });
+    vim.switch_mode(Mode::Insert, false, cx);
+}
+
 // From the docs https://vimhelp.org/change.txt.html#cw
 // Special case: When the cursor is in a word, "cw" and "cW" do not include the
 // white space after a word, they only change up to the end of the word. This is
@@ -78,7 +95,7 @@ fn change_word(
 mod test {
     use indoc::indoc;
 
-    use crate::{state::Mode, vim_test_context::VimTestContext};
+    use crate::{state::Mode, test_contexts::VimTestContext};
 
     #[gpui::test]
     async fn test_change_h(cx: &mut gpui::TestAppContext) {
@@ -170,8 +187,7 @@ mod test {
                 test"},
             indoc! {"
                 Test test
-                ˇ
-                test"},
+                ˇ"},
         );
 
         let mut cx = cx.binding(["c", "shift-e"]);
@@ -193,6 +209,7 @@ mod test {
                 Test ˇ
                 test"},
         );
+        println!("Marker");
         cx.assert(
             indoc! {"
                 Test test

crates/vim/src/normal/delete.rs 🔗

@@ -1,9 +1,9 @@
-use crate::{motion::Motion, utils::copy_selections_content, Vim};
-use collections::HashMap;
-use editor::{Autoscroll, Bias};
+use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
+use collections::{HashMap, HashSet};
+use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
 use gpui::MutableAppContext;
 
-pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+pub fn delete_motion(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -36,11 +36,67 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     });
 }
 
+pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.transact(cx, |editor, cx| {
+            editor.set_clip_at_line_ends(false, cx);
+            // Emulates behavior in vim where if we expanded backwards to include a newline
+            // the cursor gets set back to the start of the line
+            let mut should_move_to_start: HashSet<_> = Default::default();
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    object.expand_selection(map, selection, around);
+                    let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
+                    let contains_only_newlines = map
+                        .chars_at(selection.start)
+                        .take_while(|(_, p)| p < &selection.end)
+                        .all(|(char, _)| char == '\n')
+                        || offset_range.is_empty();
+                    let end_at_newline = map
+                        .chars_at(selection.end)
+                        .next()
+                        .map(|(c, _)| c == '\n')
+                        .unwrap_or(false);
+
+                    // If expanded range contains only newlines and
+                    // the object is around or sentence, expand to include a newline
+                    // at the end or start
+                    if (around || object == Object::Sentence) && contains_only_newlines {
+                        if end_at_newline {
+                            selection.end =
+                                (offset_range.end + '\n'.len_utf8()).to_display_point(map);
+                        } else if selection.start.row() > 0 {
+                            should_move_to_start.insert(selection.id);
+                            selection.start =
+                                (offset_range.start - '\n'.len_utf8()).to_display_point(map);
+                        }
+                    }
+                });
+            });
+            copy_selections_content(editor, false, cx);
+            editor.insert("", cx);
+
+            // Fixup cursor position after the deletion
+            editor.set_clip_at_line_ends(true, cx);
+            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                s.move_with(|map, selection| {
+                    let mut cursor = selection.head();
+                    if should_move_to_start.contains(&selection.id) {
+                        *cursor.column_mut() = 0;
+                    }
+                    cursor = map.clip_point(cursor, Bias::Left);
+                    selection.collapse_to(cursor, selection.goal)
+                });
+            });
+        });
+    });
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;
 
-    use crate::{state::Mode, vim_test_context::VimTestContext};
+    use crate::{state::Mode, test_contexts::VimTestContext};
 
     #[gpui::test]
     async fn test_delete_h(cx: &mut gpui::TestAppContext) {
@@ -140,8 +196,7 @@ mod test {
                 test"},
             indoc! {"
                 Test test
-                ˇ
-                test"},
+                ˇ"},
         );
 
         let mut cx = cx.binding(["d", "shift-e"]);

crates/vim/src/normal/yank.rs 🔗

@@ -1,8 +1,8 @@
-use crate::{motion::Motion, utils::copy_selections_content, Vim};
+use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
 use collections::HashMap;
 use gpui::MutableAppContext;
 
-pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+pub fn yank_motion(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);
@@ -24,3 +24,26 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
         });
     });
 }
+
+pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.transact(cx, |editor, cx| {
+            editor.set_clip_at_line_ends(false, cx);
+            let mut original_positions: HashMap<_, _> = Default::default();
+            editor.change_selections(None, cx, |s| {
+                s.move_with(|map, selection| {
+                    let original_position = (selection.head(), selection.goal);
+                    object.expand_selection(map, selection, around);
+                    original_positions.insert(selection.id, original_position);
+                });
+            });
+            copy_selections_content(editor, false, cx);
+            editor.change_selections(None, cx, |s| {
+                s.move_with(|_, selection| {
+                    let (head, goal) = original_positions.remove(&selection.id).unwrap();
+                    selection.collapse_to(head, goal);
+                });
+            });
+        });
+    });
+}

crates/vim/src/object.rs 🔗

@@ -0,0 +1,488 @@
+use std::ops::Range;
+
+use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
+use gpui::{actions, impl_actions, MutableAppContext};
+use language::Selection;
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{motion, normal::normal_object, state::Mode, visual::visual_object, Vim};
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum Object {
+    Word { ignore_punctuation: bool },
+    Sentence,
+    Paragraph,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Word {
+    #[serde(default)]
+    ignore_punctuation: bool,
+}
+
+actions!(vim, [Sentence, Paragraph]);
+impl_actions!(vim, [Word]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(
+        |_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
+            object(Object::Word { ignore_punctuation }, cx)
+        },
+    );
+    cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
+    cx.add_action(|_: &mut Workspace, _: &Paragraph, cx: _| object(Object::Paragraph, cx));
+}
+
+fn object(object: Object, cx: &mut MutableAppContext) {
+    match Vim::read(cx).state.mode {
+        Mode::Normal => normal_object(object, cx),
+        Mode::Visual { .. } => visual_object(object, cx),
+        Mode::Insert => {
+            // Shouldn't execute a text object in insert mode. Ignoring
+        }
+    }
+}
+
+impl Object {
+    pub fn object_range(
+        self,
+        map: &DisplaySnapshot,
+        relative_to: DisplayPoint,
+        around: bool,
+    ) -> Range<DisplayPoint> {
+        match self {
+            Object::Word { ignore_punctuation } => {
+                if around {
+                    around_word(map, relative_to, ignore_punctuation)
+                } else {
+                    in_word(map, relative_to, ignore_punctuation)
+                }
+            }
+            Object::Sentence => sentence(map, relative_to, around),
+            _ => relative_to..relative_to,
+        }
+    }
+
+    pub fn expand_selection(
+        self,
+        map: &DisplaySnapshot,
+        selection: &mut Selection<DisplayPoint>,
+        around: bool,
+    ) {
+        let range = self.object_range(map, selection.head(), around);
+        selection.start = range.start;
+        selection.end = range.end;
+    }
+}
+
+/// Return a range that surrounds the word relative_to is in
+/// If relative_to is at the start of a word, return the word.
+/// If relative_to is between words, return the space between
+fn in_word(
+    map: &DisplaySnapshot,
+    relative_to: DisplayPoint,
+    ignore_punctuation: bool,
+) -> Range<DisplayPoint> {
+    // Use motion::right so that we consider the character under the cursor when looking for the start
+    let start = movement::find_preceding_boundary_in_line(
+        map,
+        motion::right(map, relative_to),
+        |left, right| {
+            char_kind(left).coerce_punctuation(ignore_punctuation)
+                != char_kind(right).coerce_punctuation(ignore_punctuation)
+        },
+    );
+    let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
+        char_kind(left).coerce_punctuation(ignore_punctuation)
+            != char_kind(right).coerce_punctuation(ignore_punctuation)
+    });
+
+    start..end
+}
+
+/// Return a range that surrounds the word and following whitespace
+/// relative_to is in.
+/// If relative_to is at the start of a word, return the word and following whitespace.
+/// If relative_to is between words, return the whitespace back and the following word
+
+/// if in word
+///   delete that word
+///   if there is whitespace following the word, delete that as well
+///   otherwise, delete any preceding whitespace
+/// otherwise
+///   delete whitespace around cursor
+///   delete word following the cursor
+fn around_word(
+    map: &DisplaySnapshot,
+    relative_to: DisplayPoint,
+    ignore_punctuation: bool,
+) -> Range<DisplayPoint> {
+    let in_word = map
+        .chars_at(relative_to)
+        .next()
+        .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
+        .unwrap_or(false);
+
+    if in_word {
+        around_containing_word(map, relative_to, ignore_punctuation)
+    } else {
+        around_next_word(map, relative_to, ignore_punctuation)
+    }
+}
+
+fn around_containing_word(
+    map: &DisplaySnapshot,
+    relative_to: DisplayPoint,
+    ignore_punctuation: bool,
+) -> Range<DisplayPoint> {
+    expand_to_include_whitespace(map, in_word(map, relative_to, ignore_punctuation), true)
+}
+
+fn around_next_word(
+    map: &DisplaySnapshot,
+    relative_to: DisplayPoint,
+    ignore_punctuation: bool,
+) -> Range<DisplayPoint> {
+    // Get the start of the word
+    let start = movement::find_preceding_boundary_in_line(
+        map,
+        motion::right(map, relative_to),
+        |left, right| {
+            char_kind(left).coerce_punctuation(ignore_punctuation)
+                != char_kind(right).coerce_punctuation(ignore_punctuation)
+        },
+    );
+
+    let mut word_found = false;
+    let end = movement::find_boundary(map, relative_to, |left, right| {
+        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+        let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
+
+        if right_kind != CharKind::Whitespace {
+            word_found = true;
+        }
+
+        found
+    });
+
+    start..end
+}
+
+// /// Return the range containing a sentence.
+// fn sentence(map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool) -> Range<DisplayPoint> {
+//     let mut previous_end = relative_to;
+//     let mut start = None;
+
+//     // Seek backwards to find a period or double newline. Record the last non whitespace character as the
+//     // possible start of the sentence. Alternatively if two newlines are found right after each other, return that.
+//     let mut rev_chars = map.reverse_chars_at(relative_to).peekable();
+//     while let Some((char, point)) = rev_chars.next() {
+//         dbg!(char, point);
+//         if char == '.' {
+//             break;
+//         }
+
+//         if char == '\n'
+//             && (rev_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) || start.is_none())
+//         {
+//             break;
+//         }
+
+//         if !char.is_whitespace() {
+//             start = Some(point);
+//         }
+
+//         previous_end = point;
+//     }
+
+//     let mut end = relative_to;
+//     let mut chars = map.chars_at(relative_to).peekable();
+//     while let Some((char, point)) = chars.next() {
+//         if !char.is_whitespace() {
+//             if start.is_none() {
+//                 start = Some(point);
+//             }
+
+//             // Set the end to the point after the current non whitespace character
+//             end = point;
+//             *end.column_mut() += char.len_utf8() as u32;
+//         }
+
+//         if char == '.' {
+//             break;
+//         }
+
+//         if char == '\n' {
+//             if start.is_none() {
+//                 if let Some((_, next_point)) = chars.peek() {
+//                     end = *next_point;
+//                 }
+//                 break;
+
+//             if chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
+//                 break;
+//             }
+//         }
+//     }
+
+//     start.unwrap_or(previous_end)..end
+// }
+
+fn sentence(map: &DisplaySnapshot, relative_to: DisplayPoint, around: bool) -> Range<DisplayPoint> {
+    let mut start = None;
+    let mut previous_end = relative_to;
+
+    for (char, point) in map.reverse_chars_at(relative_to) {
+        if is_sentence_end(map, point) {
+            break;
+        }
+
+        if is_possible_sentence_start(char) {
+            start = Some(point);
+        }
+
+        previous_end = point;
+    }
+
+    // Handle case where cursor was before the sentence start
+    let mut chars = map.chars_at(relative_to).peekable();
+    if start.is_none() {
+        if let Some((char, point)) = chars.peek() {
+            if is_possible_sentence_start(*char) {
+                start = Some(*point);
+            }
+        }
+    }
+
+    let mut end = relative_to;
+    for (char, point) in chars {
+        if start.is_some() {
+            if !char.is_whitespace() {
+                end = point;
+                *end.column_mut() += char.len_utf8() as u32;
+                end = map.clip_point(end, Bias::Left);
+            }
+
+            if is_sentence_end(map, point) {
+                break;
+            }
+        } else if is_possible_sentence_start(char) {
+            if around {
+                start = Some(point);
+            } else {
+                end = point;
+                break;
+            }
+        }
+    }
+
+    let mut range = start.unwrap_or(previous_end)..end;
+    if around {
+        range = expand_to_include_whitespace(map, range, false);
+    }
+
+    range
+}
+
+fn is_possible_sentence_start(character: char) -> bool {
+    !character.is_whitespace() && character != '.'
+}
+
+const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
+const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
+const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
+fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
+    let mut chars = map.chars_at(point).peekable();
+
+    if let Some((char, _)) = chars.next() {
+        if char == '\n' && chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
+            return true;
+        }
+
+        if !SENTENCE_END_PUNCTUATION.contains(&char) {
+            return false;
+        }
+    } else {
+        return false;
+    }
+
+    for (char, _) in chars {
+        if SENTENCE_END_WHITESPACE.contains(&char) {
+            return true;
+        }
+
+        if !SENTENCE_END_FILLERS.contains(&char) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
+/// whitespace to the end first and falls back to the start if there was none.
+fn expand_to_include_whitespace(
+    map: &DisplaySnapshot,
+    mut range: Range<DisplayPoint>,
+    stop_at_newline: bool,
+) -> Range<DisplayPoint> {
+    let mut whitespace_included = false;
+    for (char, point) in map.chars_at(range.end) {
+        range.end = point;
+
+        if char == '\n' && stop_at_newline {
+            break;
+        }
+
+        if char.is_whitespace() {
+            whitespace_included = true;
+        } else {
+            break;
+        }
+    }
+
+    if !whitespace_included {
+        for (char, point) in map.reverse_chars_at(range.start) {
+            if char == '\n' && stop_at_newline {
+                break;
+            }
+
+            if !char.is_whitespace() {
+                break;
+            }
+
+            range.start = point;
+        }
+    }
+
+    range
+}
+
+#[cfg(test)]
+mod test {
+    use indoc::indoc;
+
+    use crate::test_contexts::NeovimBackedTestContext;
+
+    const WORD_LOCATIONS: &'static str = indoc! {"
+        The quick ˇbrowˇnˇ   
+        fox ˇjuˇmpsˇ over
+        the lazy dogˇ  
+        ˇ
+        ˇ
+        ˇ
+        Thˇeˇ-ˇquˇickˇ ˇbrownˇ 
+        ˇ  
+        ˇ  
+        ˇ  fox-jumpˇs over
+        the lazy dogˇ 
+        ˇ
+        "};
+
+    #[gpui::test]
+    async fn test_change_in_word(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_change_in_word", cx)
+            .await
+            .binding(["c", "i", "w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+        let mut cx = cx.consume().binding(["c", "i", "shift-w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+    }
+
+    #[gpui::test]
+    async fn test_delete_in_word(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_delete_in_word", cx)
+            .await
+            .binding(["d", "i", "w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+        let mut cx = cx.consume().binding(["d", "i", "shift-w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+    }
+
+    #[gpui::test]
+    async fn test_change_around_word(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_change_around_word", cx)
+            .await
+            .binding(["c", "a", "w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+        let mut cx = cx.consume().binding(["c", "a", "shift-w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+    }
+
+    #[gpui::test]
+    async fn test_delete_around_word(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_delete_around_word", cx)
+            .await
+            .binding(["d", "a", "w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+        let mut cx = cx.consume().binding(["d", "a", "shift-w"]);
+        cx.assert_all(WORD_LOCATIONS).await;
+    }
+
+    const SENTENCE_EXAMPLES: &[&'static str] = &[
+        "ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
+        indoc! {"
+            ˇThe quick ˇbrownˇ   
+            fox jumps over
+            the lazy doˇgˇ.ˇ ˇThe quick ˇ
+            brown fox jumps over
+        "},
+        // Double newlines are broken currently
+        // indoc! {"
+        //     The quick brown fox jumps.
+        //     Over the lazy dog
+        //     ˇ
+        //     ˇ
+        //     ˇ  fox-jumpˇs over
+        //     the lazy dog.ˇ
+        //     ˇ
+        // "},
+        r#"The quick brown.)]'" Brown fox jumps."#,
+    ];
+
+    #[gpui::test]
+    async fn test_change_in_sentence(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_change_in_sentence", cx)
+            .await
+            .binding(["c", "i", "s"]);
+        for sentence_example in SENTENCE_EXAMPLES {
+            cx.assert_all(sentence_example).await;
+        }
+    }
+
+    #[gpui::test]
+    async fn test_delete_in_sentence(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_delete_in_sentence", cx)
+            .await
+            .binding(["d", "i", "s"]);
+        for sentence_example in SENTENCE_EXAMPLES {
+            cx.assert_all(sentence_example).await;
+        }
+    }
+
+    #[gpui::test]
+    #[ignore] // End cursor position is incorrect
+    async fn test_change_around_sentence(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_change_around_sentence", cx)
+            .await
+            .binding(["c", "a", "s"]);
+        for sentence_example in SENTENCE_EXAMPLES {
+            cx.assert_all(sentence_example).await;
+        }
+    }
+
+    #[gpui::test]
+    #[ignore] // End cursor position is incorrect
+    async fn test_delete_around_sentence(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_delete_around_sentence", cx)
+            .await
+            .binding(["d", "a", "s"]);
+        for sentence_example in SENTENCE_EXAMPLES {
+            cx.assert_all(sentence_example).await;
+        }
+    }
+}

crates/vim/src/state.rs 🔗

@@ -1,8 +1,8 @@
 use editor::CursorShape;
 use gpui::keymap::Context;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
 pub enum Mode {
     Normal,
     Insert,
@@ -26,6 +26,7 @@ pub enum Operator {
     Change,
     Delete,
     Yank,
+    Object { around: bool },
 }
 
 #[derive(Default)]
@@ -77,7 +78,12 @@ impl VimState {
             context.set.insert("VimControl".to_string());
         }
 
-        Operator::set_context(self.operator_stack.last(), &mut context);
+        let active_operator = self.operator_stack.last();
+        if matches!(active_operator, Some(Operator::Object { .. })) {
+            context.set.insert("VimObject".to_string());
+        }
+
+        Operator::set_context(active_operator, &mut context);
 
         context
     }
@@ -87,6 +93,8 @@ impl Operator {
     pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
         let operator_context = match operator {
             Some(Operator::Namespace(Namespace::G)) => "g",
+            Some(Operator::Object { around: false }) => "i",
+            Some(Operator::Object { around: true }) => "a",
             Some(Operator::Change) => "c",
             Some(Operator::Delete) => "d",
             Some(Operator::Yank) => "y",

crates/vim/src/test_contexts.rs 🔗

@@ -0,0 +1,9 @@
+mod neovim_backed_binding_test_context;
+mod neovim_backed_test_context;
+mod vim_binding_test_context;
+mod vim_test_context;
+
+pub use neovim_backed_binding_test_context::*;
+pub use neovim_backed_test_context::*;
+pub use vim_binding_test_context::*;
+pub use vim_test_context::*;

crates/vim/src/test_contexts/neovim_backed_binding_test_context.rs 🔗

@@ -0,0 +1,56 @@
+use std::ops::{Deref, DerefMut};
+
+use util::test::marked_text_offsets;
+
+use super::NeovimBackedTestContext;
+
+pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
+    cx: NeovimBackedTestContext<'a>,
+    keystrokes_under_test: [&'static str; COUNT],
+}
+
+impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
+    pub fn new(
+        keystrokes_under_test: [&'static str; COUNT],
+        cx: NeovimBackedTestContext<'a>,
+    ) -> Self {
+        Self {
+            cx,
+            keystrokes_under_test,
+        }
+    }
+
+    pub fn consume(self) -> NeovimBackedTestContext<'a> {
+        self.cx
+    }
+
+    pub async fn assert(&mut self, initial_state: &str) {
+        self.cx
+            .assert_binding_matches(self.keystrokes_under_test, initial_state)
+            .await
+    }
+
+    pub async fn assert_all(&mut self, marked_positions: &str) {
+        let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
+
+        for cursor_offset in cursor_offsets.iter() {
+            let mut marked_text = unmarked_text.clone();
+            marked_text.insert(*cursor_offset, 'ˇ');
+            self.assert(&marked_text).await;
+        }
+    }
+}
+
+impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
+    type Target = NeovimBackedTestContext<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.cx
+    }
+}
+
+impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

crates/vim/src/test_contexts/neovim_backed_test_context.rs 🔗

@@ -0,0 +1,374 @@
+use std::{
+    ops::{Deref, DerefMut},
+    path::PathBuf,
+};
+
+use editor::DisplayPoint;
+use gpui::keymap::Keystroke;
+
+#[cfg(feature = "neovim")]
+use async_compat::Compat;
+#[cfg(feature = "neovim")]
+use async_trait::async_trait;
+#[cfg(feature = "neovim")]
+use nvim_rs::{
+    create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
+};
+#[cfg(feature = "neovim")]
+use tokio::{
+    process::{Child, ChildStdin, Command},
+    task::JoinHandle,
+};
+
+use crate::state::Mode;
+
+use super::{NeovimBackedBindingTestContext, VimTestContext};
+
+pub struct NeovimBackedTestContext<'a> {
+    cx: VimTestContext<'a>,
+    test_case_id: &'static str,
+    data_counter: usize,
+    #[cfg(feature = "neovim")]
+    nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
+    #[cfg(feature = "neovim")]
+    _join_handle: JoinHandle<Result<(), Box<LoopError>>>,
+    #[cfg(feature = "neovim")]
+    _child: Child,
+}
+
+impl<'a> NeovimBackedTestContext<'a> {
+    pub async fn new(
+        test_case_id: &'static str,
+        cx: &'a mut gpui::TestAppContext,
+    ) -> NeovimBackedTestContext<'a> {
+        let cx = VimTestContext::new(cx, true).await;
+
+        #[cfg(feature = "neovim")]
+        let handler = NvimHandler {};
+        #[cfg(feature = "neovim")]
+        let (nvim, join_handle, child) = Compat::new(async {
+            let (nvim, join_handle, child) = new_child_cmd(
+                &mut Command::new("nvim").arg("--embed").arg("--clean"),
+                handler,
+            )
+            .await
+            .expect("Could not connect to neovim process");
+
+            nvim.ui_attach(100, 100, &UiAttachOptions::default())
+                .await
+                .expect("Could not attach to ui");
+
+            (nvim, join_handle, child)
+        })
+        .await;
+
+        let result = Self {
+            cx,
+            test_case_id,
+            data_counter: 0,
+            #[cfg(feature = "neovim")]
+            nvim,
+            #[cfg(feature = "neovim")]
+            _join_handle: join_handle,
+            #[cfg(feature = "neovim")]
+            _child: child,
+        };
+
+        #[cfg(feature = "neovim")]
+        {
+            result.clear_test_data()
+        }
+
+        result
+    }
+
+    pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) {
+        let keystroke = Keystroke::parse(keystroke_text).unwrap();
+
+        #[cfg(feature = "neovim")]
+        {
+            let special = keystroke.shift
+                || keystroke.ctrl
+                || keystroke.alt
+                || keystroke.cmd
+                || keystroke.key.len() > 1;
+            let start = if special { "<" } else { "" };
+            let shift = if keystroke.shift { "S-" } else { "" };
+            let ctrl = if keystroke.ctrl { "C-" } else { "" };
+            let alt = if keystroke.alt { "M-" } else { "" };
+            let cmd = if keystroke.cmd { "D-" } else { "" };
+            let end = if special { ">" } else { "" };
+
+            let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
+
+            self.nvim
+                .input(&key)
+                .await
+                .expect("Could not input keystroke");
+        }
+
+        let window_id = self.window_id;
+        self.cx.dispatch_keystroke(window_id, keystroke, false);
+    }
+
+    pub async fn simulate_shared_keystrokes<const COUNT: usize>(
+        &mut self,
+        keystroke_texts: [&str; COUNT],
+    ) {
+        for keystroke_text in keystroke_texts.into_iter() {
+            self.simulate_shared_keystroke(keystroke_text).await;
+        }
+    }
+
+    pub async fn set_shared_state(&mut self, marked_text: &str) {
+        self.set_state(marked_text, Mode::Normal);
+
+        #[cfg(feature = "neovim")]
+        {
+            let cursor_point =
+                self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
+            let nvim_buffer = self
+                .nvim
+                .get_current_buf()
+                .await
+                .expect("Could not get neovim buffer");
+            let mut lines = self
+                .buffer_text()
+                .lines()
+                .map(|line| line.to_string())
+                .collect::<Vec<_>>();
+
+            if lines.len() > 1 {
+                // Add final newline which is missing from buffer_text
+                lines.push("".to_string());
+            }
+
+            nvim_buffer
+                .set_lines(0, -1, false, lines)
+                .await
+                .expect("Could not set nvim buffer text");
+
+            self.nvim
+                .input("<escape>")
+                .await
+                .expect("Could not send escape to nvim");
+            self.nvim
+                .input("<escape>")
+                .await
+                .expect("Could not send escape to nvim");
+
+            let nvim_window = self
+                .nvim
+                .get_current_win()
+                .await
+                .expect("Could not get neovim window");
+            nvim_window
+                .set_cursor((
+                    cursor_point.head().row as i64 + 1,
+                    cursor_point.head().column as i64,
+                ))
+                .await
+                .expect("Could not set nvim cursor position");
+        }
+    }
+
+    pub async fn assert_state_matches(&mut self) {
+        assert_eq!(self.neovim_text().await, self.buffer_text());
+
+        let zed_head = self.update_editor(|editor, cx| editor.selections.newest_display(cx).head());
+        assert_eq!(self.neovim_head().await, zed_head);
+
+        if let Some(neovim_mode) = self.neovim_mode().await {
+            assert_eq!(neovim_mode, self.mode());
+        }
+    }
+
+    #[cfg(feature = "neovim")]
+    pub async fn neovim_text(&mut self) -> String {
+        let nvim_buffer = self
+            .nvim
+            .get_current_buf()
+            .await
+            .expect("Could not get neovim buffer");
+        let text = nvim_buffer
+            .get_lines(0, -1, false)
+            .await
+            .expect("Could not get buffer text")
+            .join("\n");
+
+        self.write_test_data(text.clone(), "text");
+        text
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    pub async fn neovim_text(&mut self) -> String {
+        self.read_test_data("text")
+    }
+
+    #[cfg(feature = "neovim")]
+    pub async fn neovim_head(&mut self) -> DisplayPoint {
+        let nvim_row: u32 = self
+            .nvim
+            .command_output("echo line('.')")
+            .await
+            .unwrap()
+            .parse::<u32>()
+            .unwrap()
+            - 1; // Neovim rows start at 1
+        let nvim_column: u32 = self
+            .nvim
+            .command_output("echo col('.')")
+            .await
+            .unwrap()
+            .parse::<u32>()
+            .unwrap()
+            - 1; // Neovim columns start at 1
+
+        let serialized = format!("{},{}", nvim_row.to_string(), nvim_column.to_string());
+        self.write_test_data(serialized, "head");
+
+        DisplayPoint::new(nvim_row, nvim_column)
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    pub async fn neovim_head(&mut self) -> DisplayPoint {
+        let serialized = self.read_test_data("head");
+        let mut components = serialized.split(',');
+        let nvim_row = components.next().unwrap().parse::<u32>().unwrap();
+        let nvim_column = components.next().unwrap().parse::<u32>().unwrap();
+
+        DisplayPoint::new(nvim_row, nvim_column)
+    }
+
+    #[cfg(feature = "neovim")]
+    pub async fn neovim_mode(&mut self) -> Option<Mode> {
+        let nvim_mode_text = self
+            .nvim
+            .get_mode()
+            .await
+            .expect("Could not get mode")
+            .into_iter()
+            .find_map(|(key, value)| {
+                if key.as_str() == Some("mode") {
+                    Some(value.as_str().unwrap().to_owned())
+                } else {
+                    None
+                }
+            })
+            .expect("Could not find mode value");
+
+        let mode = match nvim_mode_text.as_ref() {
+            "i" => Some(Mode::Insert),
+            "n" => Some(Mode::Normal),
+            "v" => Some(Mode::Visual { line: false }),
+            "V" => Some(Mode::Visual { line: true }),
+            _ => None,
+        };
+
+        let serialized = serde_json::to_string(&mode).expect("Could not serialize mode");
+
+        self.write_test_data(serialized, "mode");
+
+        mode
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    pub async fn neovim_mode(&mut self) -> Option<Mode> {
+        let serialized = self.read_test_data("mode");
+        serde_json::from_str(&serialized).expect("Could not deserialize test data")
+    }
+
+    fn test_data_directory(&self) -> PathBuf {
+        let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        data_path.push("test_data");
+        data_path.push(self.test_case_id);
+        data_path
+    }
+
+    fn next_data_path(&mut self, kind: &str) -> PathBuf {
+        let mut data_path = self.test_data_directory();
+        data_path.push(format!("{}{}.txt", self.data_counter, kind));
+        self.data_counter += 1;
+        data_path
+    }
+
+    #[cfg(not(feature = "neovim"))]
+    fn read_test_data(&mut self, kind: &str) -> String {
+        let path = self.next_data_path(kind);
+        std::fs::read_to_string(path).expect(
+            "Could not read test data. Is it generated? Try running test with '--features neovim'",
+        )
+    }
+
+    #[cfg(feature = "neovim")]
+    fn write_test_data(&mut self, data: String, kind: &str) {
+        let path = self.next_data_path(kind);
+        std::fs::create_dir_all(path.parent().unwrap())
+            .expect("Could not create test data directory");
+        std::fs::write(path, data).expect("Could not write out test data");
+    }
+
+    #[cfg(feature = "neovim")]
+    fn clear_test_data(&self) {
+        // If the path does not exist, no biggy, we will create it
+        std::fs::remove_dir_all(self.test_data_directory()).ok();
+    }
+
+    pub async fn assert_binding_matches<const COUNT: usize>(
+        &mut self,
+        keystrokes: [&str; COUNT],
+        initial_state: &str,
+    ) {
+        dbg!(keystrokes, initial_state);
+        self.set_shared_state(initial_state).await;
+        self.simulate_shared_keystrokes(keystrokes).await;
+        self.assert_state_matches().await;
+    }
+
+    pub fn binding<const COUNT: usize>(
+        self,
+        keystrokes: [&'static str; COUNT],
+    ) -> NeovimBackedBindingTestContext<'a, COUNT> {
+        NeovimBackedBindingTestContext::new(keystrokes, self)
+    }
+}
+
+#[derive(Clone)]
+struct NvimHandler {}
+
+#[cfg(feature = "neovim")]
+#[async_trait]
+impl Handler for NvimHandler {
+    type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
+
+    async fn handle_request(
+        &self,
+        _event_name: String,
+        _arguments: Vec<Value>,
+        _neovim: Neovim<Self::Writer>,
+    ) -> Result<Value, Value> {
+        unimplemented!();
+    }
+
+    async fn handle_notify(
+        &self,
+        _event_name: String,
+        _arguments: Vec<Value>,
+        _neovim: Neovim<Self::Writer>,
+    ) {
+    }
+}
+
+impl<'a> Deref for NeovimBackedTestContext<'a> {
+    type Target = VimTestContext<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.cx
+    }
+}
+
+impl<'a> DerefMut for NeovimBackedTestContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

crates/vim/src/test_contexts/vim_binding_test_context.rs 🔗

@@ -0,0 +1,69 @@
+use std::ops::{Deref, DerefMut};
+
+use crate::*;
+
+use super::VimTestContext;
+
+pub struct VimBindingTestContext<'a, const COUNT: usize> {
+    cx: VimTestContext<'a>,
+    keystrokes_under_test: [&'static str; COUNT],
+    mode_before: Mode,
+    mode_after: Mode,
+}
+
+impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
+    pub fn new(
+        keystrokes_under_test: [&'static str; COUNT],
+        mode_before: Mode,
+        mode_after: Mode,
+        cx: VimTestContext<'a>,
+    ) -> Self {
+        Self {
+            cx,
+            keystrokes_under_test,
+            mode_before,
+            mode_after,
+        }
+    }
+
+    pub fn binding<const NEW_COUNT: usize>(
+        self,
+        keystrokes_under_test: [&'static str; NEW_COUNT],
+    ) -> VimBindingTestContext<'a, NEW_COUNT> {
+        VimBindingTestContext {
+            keystrokes_under_test,
+            cx: self.cx,
+            mode_before: self.mode_before,
+            mode_after: self.mode_after,
+        }
+    }
+
+    pub fn mode_after(mut self, mode_after: Mode) -> Self {
+        self.mode_after = mode_after;
+        self
+    }
+
+    pub fn assert(&mut self, initial_state: &str, state_after: &str) {
+        self.cx.assert_binding(
+            self.keystrokes_under_test,
+            initial_state,
+            self.mode_before,
+            state_after,
+            self.mode_after,
+        )
+    }
+}
+
+impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
+    type Target = VimTestContext<'a>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.cx
+    }
+}
+
+impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.cx
+    }
+}

crates/vim/src/vim_test_context.rs → crates/vim/src/test_contexts/vim_test_context.rs 🔗

@@ -8,6 +8,8 @@ use workspace::{pane, AppState, WorkspaceHandle};
 
 use crate::{state::Operator, *};
 
+use super::VimBindingTestContext;
+
 pub struct VimTestContext<'a> {
     cx: EditorTestContext<'a>,
     workspace: ViewHandle<Workspace>,
@@ -168,67 +170,3 @@ impl<'a> DerefMut for VimTestContext<'a> {
         &mut self.cx
     }
 }
-
-pub struct VimBindingTestContext<'a, const COUNT: usize> {
-    cx: VimTestContext<'a>,
-    keystrokes_under_test: [&'static str; COUNT],
-    mode_before: Mode,
-    mode_after: Mode,
-}
-
-impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
-    pub fn new(
-        keystrokes_under_test: [&'static str; COUNT],
-        mode_before: Mode,
-        mode_after: Mode,
-        cx: VimTestContext<'a>,
-    ) -> Self {
-        Self {
-            cx,
-            keystrokes_under_test,
-            mode_before,
-            mode_after,
-        }
-    }
-
-    pub fn binding<const NEW_COUNT: usize>(
-        self,
-        keystrokes_under_test: [&'static str; NEW_COUNT],
-    ) -> VimBindingTestContext<'a, NEW_COUNT> {
-        VimBindingTestContext {
-            keystrokes_under_test,
-            cx: self.cx,
-            mode_before: self.mode_before,
-            mode_after: self.mode_after,
-        }
-    }
-
-    pub fn mode_after(mut self, mode_after: Mode) -> Self {
-        self.mode_after = mode_after;
-        self
-    }
-
-    pub fn assert(&mut self, initial_state: &str, state_after: &str) {
-        self.cx.assert_binding(
-            self.keystrokes_under_test,
-            initial_state,
-            self.mode_before,
-            state_after,
-            self.mode_after,
-        )
-    }
-}
-
-impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
-    type Target = VimTestContext<'a>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.cx
-    }
-}
-
-impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.cx
-    }
-}

crates/vim/src/vim.rs 🔗

@@ -1,10 +1,11 @@
 #[cfg(test)]
-mod vim_test_context;
+mod test_contexts;
 
 mod editor_events;
 mod insert;
 mod motion;
 mod normal;
+mod object;
 mod state;
 mod utils;
 mod visual;
@@ -32,6 +33,7 @@ pub fn init(cx: &mut MutableAppContext) {
     normal::init(cx);
     visual::init(cx);
     insert::init(cx);
+    object::init(cx);
     motion::init(cx);
 
     // Vim Actions
@@ -144,7 +146,8 @@ impl Vim {
     }
 
     fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
-        let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
+        let popped_operator = self.state.operator_stack.pop()
+            .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
         self.sync_vim_settings(cx);
         popped_operator
     }
@@ -210,7 +213,10 @@ mod test {
     use indoc::indoc;
     use search::BufferSearchBar;
 
-    use crate::{state::Mode, vim_test_context::VimTestContext};
+    use crate::{
+        state::Mode,
+        test_contexts::{NeovimBackedTestContext, VimTestContext},
+    };
 
     #[gpui::test]
     async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
@@ -219,6 +225,19 @@ mod test {
         cx.assert_editor_state("hjklˇ");
     }
 
+    #[gpui::test]
+    async fn test_neovim(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new("test_neovim", cx).await;
+
+        cx.simulate_shared_keystroke("i").await;
+        cx.simulate_shared_keystrokes([
+            "shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
+        ])
+        .await;
+        cx.assert_state_matches().await;
+        cx.assert_editor_state("ˇtest");
+    }
+
     #[gpui::test]
     async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;

crates/vim/src/visual.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{actions, MutableAppContext, ViewContext};
 use language::{AutoindentMode, SelectionGoal};
 use workspace::Workspace;
 
-use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
+use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
 
 actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
 
@@ -43,6 +43,8 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
     });
 }
 
+pub fn visual_object(_object: Object, _cx: &mut MutableAppContext) {}
+
 pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
@@ -274,7 +276,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
 mod test {
     use indoc::indoc;
 
-    use crate::{state::Mode, vim_test_context::VimTestContext};
+    use crate::{state::Mode, test_contexts::VimTestContext};
 
     #[gpui::test]
     async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {