diff --git a/Cargo.lock b/Cargo.lock index 010e7763e4..a9072004c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9079,6 +9079,7 @@ dependencies = [ "nvim-rs", "parking_lot 0.11.2", "project", + "regex", "search", "serde", "serde_derive", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1da6f0ef8c..32acb90d69 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -104,8 +104,6 @@ "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", "ctrl-q": "vim::ToggleVisualBlock", - "*": "vim::MoveToNext", - "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "ctrl-f": "vim::PageDown", "pagedown": "vim::PageDown", @@ -329,6 +327,8 @@ "backwards": true } ], + "*": "vim::MoveToNext", + "#": "vim::MoveToPrev", ";": "vim::RepeatFind", ",": [ "vim::RepeatFind", @@ -421,6 +421,18 @@ "shift-r": "vim::SubstituteLine", "c": "vim::Substitute", "~": "vim::ChangeCase", + "*": [ + "vim::MoveToNext", + { + "partialWord": true + } + ], + "#": [ + "vim::MoveToPrev", + { + "partialWord": true + } + ], "ctrl-a": "vim::Increment", "ctrl-x": "vim::Decrement", "g ctrl-a": [ diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 9570258529..ef3fd2a4c7 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -23,6 +23,7 @@ async-trait = { workspace = true, "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.workspace = true +regex.workspace = true collections = { path = "../collections" } command_palette = { path = "../command_palette" } diff --git a/crates/vim/README.md b/crates/vim/README.md new file mode 100644 index 0000000000..547ca686fb --- /dev/null +++ b/crates/vim/README.md @@ -0,0 +1,36 @@ +This contains the code for Zed's Vim emulation mode. + +Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar! + +The backlog is maintained in the `#vim` channel notes. + +## Testing against Neovim + +If you are making a change to make Zed's behaviour more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`. + +For example, the following test checks that Zed and Neovim have the same behaviour when running `*` in visual mode: + +```rust +#[gpui::test] +async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; + cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await; + cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await; +} +``` + +To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data), +but while developing this test you'll need to run it with the neovim flag enabled: + +``` +cargo test -p vim --features neovim test_visual_star_hash +``` + +This will run your keystrokes against a headless neovim and cache the results in the test_data directory. + + +## Testing zed-only behaviour + +Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent. diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index f85e3d9ba9..31fda7788f 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -91,7 +91,6 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { let search = search_bar.update(cx, |search_bar, cx| { - let mut options = SearchOptions::CASE_SENSITIVE; - options.set(SearchOptions::WHOLE_WORD, whole_word); - if search_bar.show(cx) { - search_bar - .query_suggestion(cx) - .map(|query| search_bar.search(&query, Some(options), cx)) - } else { - None + let options = SearchOptions::CASE_SENSITIVE; + if !search_bar.show(cx) { + return None; } + let Some(query) = search_bar.query_suggestion(cx) else { + return None; + }; + let mut query = regex::escape(&query); + if whole_word { + query = format!(r"\b{}\b", query); + } + search_bar.activate_search_mode(SearchMode::Regex, cx); + Some(search_bar.search(&query, Some(options), cx)) }); if let Some(search) = search { @@ -350,7 +353,10 @@ mod test { use editor::DisplayPoint; use search::BufferSearchBar; - use crate::{state::Mode, test::VimTestContext}; + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; #[gpui::test] async fn test_move_to_next(cx: &mut gpui::TestAppContext) { @@ -474,4 +480,13 @@ mod test { cx.simulate_keystrokes(["shift-enter"]); cx.assert_editor_state("«oneˇ» one one one"); } + + #[gpui::test] + async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; + cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await; + cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await; + } } diff --git a/crates/vim/test_data/test_visual_star_hash.json b/crates/vim/test_data/test_visual_star_hash.json new file mode 100644 index 0000000000..d6523c4a45 --- /dev/null +++ b/crates/vim/test_data/test_visual_star_hash.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} +{"Key":"v"} +{"Key":"3"} +{"Key":"l"} +{"Key":"*"} +{"Get":{"state":"a.c. abcd ˇa.c. abcd","mode":"Normal"}}