Merge pull request #2258 from zed-industries/lsp-file-change-notifications

Max Brunsfeld created

Implement `DidChangedWatchedFiles` LSP feature

Change summary

Cargo.lock                                                          |   7 
Cargo.toml                                                          |   1 
crates/call/Cargo.toml                                              |   2 
crates/client/Cargo.toml                                            |   2 
crates/collab/src/tests/integration_tests.rs                        |  12 
crates/collab_ui/Cargo.toml                                         |   2 
crates/diagnostics/Cargo.toml                                       |   2 
crates/editor/Cargo.toml                                            |   2 
crates/feedback/Cargo.toml                                          |   2 
crates/file_finder/Cargo.toml                                       |   2 
crates/fs/src/fs.rs                                                 |  36 
crates/go_to_line/Cargo.toml                                        |   2 
crates/gpui/Cargo.toml                                              |   2 
crates/language/Cargo.toml                                          |   2 
crates/language/src/buffer.rs                                       |   1 
crates/language/src/buffer_tests.rs                                 |   1 
crates/live_kit_client/Cargo.toml                                   |   2 
crates/lsp/Cargo.toml                                               |   2 
crates/lsp/src/lsp.rs                                               |   3 
crates/outline/Cargo.toml                                           |   2 
crates/project/Cargo.toml                                           |   5 
crates/project/src/lsp_glob_set.rs                                  | 121 
crates/project/src/project.rs                                       | 108 
crates/project/src/project_tests.rs                                 | 120 
crates/project/src/worktree.rs                                      | 765 
crates/project_panel/Cargo.toml                                     |   2 
crates/project_symbols/Cargo.toml                                   |   2 
crates/recent_projects/Cargo.toml                                   |   2 
crates/search/Cargo.toml                                            |   2 
crates/settings/Cargo.toml                                          |   2 
crates/text/Cargo.toml                                              |   2 
crates/theme_selector/Cargo.toml                                    |   3 
crates/vim/src/test/neovim_backed_test_context.rs                   |   8 
crates/vim/src/test/neovim_connection.rs                            | 252 
crates/vim/test_data/neovim_backed_test_context_works.json          |   4 
crates/vim/test_data/test_a.json                                    |   7 
crates/vim/test_data/test_b.json                                    |   0 
crates/vim/test_data/test_backspace.json                            |  10 
crates/vim/test_data/test_capital_f_and_capital_t.json              |   0 
crates/vim/test_data/test_cc.json                                   |  25 
crates/vim/test_data/test_change_0.json                             |   9 
crates/vim/test_data/test_change_b.json                             |  25 
crates/vim/test_data/test_change_backspace.json                     |  17 
crates/vim/test_data/test_change_e.json                             |  25 
crates/vim/test_data/test_change_end_of_document.json               |  17 
crates/vim/test_data/test_change_end_of_line.json                   |   9 
crates/vim/test_data/test_change_gg.json                            |  21 
crates/vim/test_data/test_change_h.json                             |  17 
crates/vim/test_data/test_change_j.json                             |  17 
crates/vim/test_data/test_change_k.json                             |  17 
crates/vim/test_data/test_change_l.json                             |   9 
crates/vim/test_data/test_change_sentence_object.json               |   0 
crates/vim/test_data/test_change_surrounding_character_objects.json |   0 
crates/vim/test_data/test_change_w.json                             |  29 
crates/vim/test_data/test_change_word_object.json                   |   0 
crates/vim/test_data/test_dd.json                                   |  25 
crates/vim/test_data/test_delete_0.json                             |   9 
crates/vim/test_data/test_delete_b.json                             |  25 
crates/vim/test_data/test_delete_e.json                             |  21 
crates/vim/test_data/test_delete_end_of_document.json               |  17 
crates/vim/test_data/test_delete_end_of_line.json                   |   9 
crates/vim/test_data/test_delete_gg.json                            |  21 
crates/vim/test_data/test_delete_h.json                             |  17 
crates/vim/test_data/test_delete_j.json                             |  17 
crates/vim/test_data/test_delete_k.json                             |  17 
crates/vim/test_data/test_delete_l.json                             |  17 
crates/vim/test_data/test_delete_left.json                          |  16 
crates/vim/test_data/test_delete_sentence_object.json               |   0 
crates/vim/test_data/test_delete_surrounding_character_objects.json |   0 
crates/vim/test_data/test_delete_to_end_of_line.json                |   7 
crates/vim/test_data/test_delete_w.json                             |  21 
crates/vim/test_data/test_delete_word_object.json                   |   0 
crates/vim/test_data/test_e.json                                    |   0 
crates/vim/test_data/test_end_of_document.json                      |  16 
crates/vim/test_data/test_enter.json                                |  12 
crates/vim/test_data/test_enter_visual_mode.json                    |   0 
crates/vim/test_data/test_f_and_t.json                              |   0 
crates/vim/test_data/test_gg.json                                   |  22 
crates/vim/test_data/test_h.json                                    |  10 
crates/vim/test_data/test_h_through_unicode.json                    |  13 
crates/vim/test_data/test_insert_end_of_line.json                   |  10 
crates/vim/test_data/test_insert_first_non_whitespace.json          |  16 
crates/vim/test_data/test_insert_line_above.json                    |  19 
crates/vim/test_data/test_j.json                                    |  13 
crates/vim/test_data/test_jump_to_end.json                          |  15 
crates/vim/test_data/test_jump_to_first_non_whitespace.json         |  19 
crates/vim/test_data/test_jump_to_line_boundaries.json              |   0 
crates/vim/test_data/test_k.json                                    |  16 
crates/vim/test_data/test_l.json                                    |  16 
crates/vim/test_data/test_neovim.json                               |  17 
crates/vim/test_data/test_o.json                                    |  19 
crates/vim/test_data/test_p.json                                    |  14 
crates/vim/test_data/test_percent.json                              |   0 
crates/vim/test_data/test_repeated_cb.json                          |   0 
crates/vim/test_data/test_repeated_ce.json                          |   0 
crates/vim/test_data/test_repeated_cj.json                          |   0 
crates/vim/test_data/test_repeated_cl.json                          |   0 
crates/vim/test_data/test_repeated_word.json                        |   0 
crates/vim/test_data/test_visual_change.json                        |  42 
crates/vim/test_data/test_visual_delete.json                        |  45 
crates/vim/test_data/test_visual_line_change.json                   |  36 
crates/vim/test_data/test_visual_line_delete.json                   |  32 
crates/vim/test_data/test_visual_sentence_object.json               |   1 
crates/vim/test_data/test_visual_word_object.json                   |   0 
crates/vim/test_data/test_w.json                                    |   0 
crates/vim/test_data/test_x.json                                    |  13 
crates/workspace/Cargo.toml                                         |   2 
crates/zed/Cargo.toml                                               |   2 
108 files changed, 1,782 insertions(+), 616 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2591,9 +2591,9 @@ dependencies = [
 
 [[package]]
 name = "glob"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "globset"
@@ -4625,12 +4625,15 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "ctor",
  "db",
+ "env_logger",
  "fs",
  "fsevent",
  "futures 0.3.25",
  "fuzzy",
  "git",
+ "glob",
  "gpui",
  "ignore",
  "language",

Cargo.toml 🔗

@@ -71,6 +71,7 @@ serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
+postage = { version = "0.4.1", features = ["futures-traits"] }
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

crates/call/Cargo.toml 🔗

@@ -34,7 +34,7 @@ util = { path = "../util" }
 anyhow = "1.0.38"
 async-broadcast = "0.4"
 futures = "0.3"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }

crates/client/Cargo.toml 🔗

@@ -27,7 +27,7 @@ isahc = "1.7"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = "0.8.3"
 smol = "1.2.5"
 thiserror = "1.0.29"

crates/collab/src/tests/integration_tests.rs 🔗

@@ -1744,10 +1744,6 @@ async fn test_project_reconnect(
             vec![
                 "a.txt",
                 "b.txt",
-                "subdir1",
-                "subdir1/c.txt",
-                "subdir1/d.txt",
-                "subdir1/e.txt",
                 "subdir2",
                 "subdir2/f.txt",
                 "subdir2/g.txt",
@@ -1780,10 +1776,6 @@ async fn test_project_reconnect(
             vec![
                 "a.txt",
                 "b.txt",
-                "subdir1",
-                "subdir1/c.txt",
-                "subdir1/d.txt",
-                "subdir1/e.txt",
                 "subdir2",
                 "subdir2/f.txt",
                 "subdir2/g.txt",
@@ -1875,10 +1867,6 @@ async fn test_project_reconnect(
             vec![
                 "a.txt",
                 "b.txt",
-                "subdir1",
-                "subdir1/c.txt",
-                "subdir1/d.txt",
-                "subdir1/e.txt",
                 "subdir2",
                 "subdir2/f.txt",
                 "subdir2/g.txt",

crates/collab_ui/Cargo.toml 🔗

@@ -42,7 +42,7 @@ workspace = { path = "../workspace" }
 anyhow = "1.0"
 futures = "0.3"
 log = "0.4"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 

crates/diagnostics/Cargo.toml 🔗

@@ -20,7 +20,7 @@ settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 
 [dev-dependencies]
 unindent = "0.1"

crates/editor/Cargo.toml 🔗

@@ -51,7 +51,7 @@ lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 ordered-float = "2.1.1"
 parking_lot = "0.11"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 serde = { workspace = true }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }

crates/feedback/Cargo.toml 🔗

@@ -21,7 +21,7 @@ gpui = { path = "../gpui" }
 human_bytes = "0.4.1"
 isahc = "1.7"
 lazy_static = "1.4.0"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 project = { path = "../project" }
 search = { path = "../search" }
 serde = { version = "1.0", features = ["derive", "rc"] }

crates/file_finder/Cargo.toml 🔗

@@ -19,7 +19,7 @@ settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/fs/src/fs.rs 🔗

@@ -380,6 +380,8 @@ struct FakeFsState {
     next_inode: u64,
     next_mtime: SystemTime,
     event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
+    events_paused: bool,
+    buffered_events: Vec<fsevent::Event>,
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -483,15 +485,21 @@ impl FakeFsState {
         I: IntoIterator<Item = T>,
         T: Into<PathBuf>,
     {
-        let events = paths
-            .into_iter()
-            .map(|path| fsevent::Event {
+        self.buffered_events
+            .extend(paths.into_iter().map(|path| fsevent::Event {
                 event_id: 0,
                 flags: fsevent::StreamFlags::empty(),
                 path: path.into(),
-            })
-            .collect::<Vec<_>>();
+            }));
+
+        if !self.events_paused {
+            self.flush_events(self.buffered_events.len());
+        }
+    }
 
+    fn flush_events(&mut self, mut count: usize) {
+        count = count.min(self.buffered_events.len());
+        let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
         self.event_txs.retain(|tx| {
             let _ = tx.try_send(events.clone());
             !tx.is_closed()
@@ -514,6 +522,8 @@ impl FakeFs {
                 next_mtime: SystemTime::UNIX_EPOCH,
                 next_inode: 1,
                 event_txs: Default::default(),
+                buffered_events: Vec::new(),
+                events_paused: false,
             }),
         })
     }
@@ -567,6 +577,18 @@ impl FakeFs {
         state.emit_event(&[path]);
     }
 
+    pub async fn pause_events(&self) {
+        self.state.lock().await.events_paused = true;
+    }
+
+    pub async fn buffered_event_count(&self) -> usize {
+        self.state.lock().await.buffered_events.len()
+    }
+
+    pub async fn flush_events(&self, count: usize) {
+        self.state.lock().await.flush_events(count);
+    }
+
     #[must_use]
     pub fn insert_tree<'a>(
         &'a self,
@@ -868,7 +890,7 @@ impl Fs for FakeFs {
             .ok_or_else(|| anyhow!("cannot remove the root"))?;
         let base_name = path.file_name().unwrap();
 
-        let state = self.state.lock().await;
+        let mut state = self.state.lock().await;
         let parent_entry = state.read_path(parent_path).await?;
         let mut parent_entry = parent_entry.lock().await;
         let entry = parent_entry
@@ -892,7 +914,7 @@ impl Fs for FakeFs {
                 e.remove();
             }
         }
-
+        state.emit_event(&[path]);
         Ok(())
     }
 

crates/go_to_line/Cargo.toml 🔗

@@ -15,4 +15,4 @@ menu = { path = "../menu" }
 settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }

crates/gpui/Cargo.toml 🔗

@@ -36,7 +36,7 @@ parking = "2.0.0"
 parking_lot = "0.11.1"
 pathfinder_color = "0.5"
 pathfinder_geometry = "0.5"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = "0.8.3"
 resvg = "0.14"
 schemars = "0.8"

crates/language/Cargo.toml 🔗

@@ -43,7 +43,7 @@ futures = "0.3"
 lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 regex = "1.5"
 serde = { version = "1.0", features = ["derive", "rc"] }

crates/language/src/buffer.rs 🔗

@@ -1366,6 +1366,7 @@ impl Buffer {
     where
         T: Into<Arc<str>>,
     {
+        self.autoindent_requests.clear();
         self.edit([(0..self.len(), text)], None, cx)
     }
 

crates/language/src/buffer_tests.rs 🔗

@@ -809,7 +809,6 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
         }"}],
     );
 
-    eprintln!("-----------------------");
     // Regression test: even though the parent node of the parentheses (the for loop) does
     // intersect the given range, the parentheses themselves do not contain the range, so
     // they should not be returned. Only the curly braces contain the range.

crates/live_kit_client/Cargo.toml 🔗

@@ -35,7 +35,7 @@ core-graphics = "0.22.3"
 futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 
 async-trait = { version = "0.1", optional = true }
 lazy_static = { version = "1.4", optional = true }

crates/lsp/Cargo.toml 🔗

@@ -21,7 +21,7 @@ futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 lsp-types = "0.91"
 parking_lot = "0.11"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["raw_value"] }

crates/lsp/src/lsp.rs 🔗

@@ -319,6 +319,9 @@ impl LanguageServer {
             capabilities: ClientCapabilities {
                 workspace: Some(WorkspaceClientCapabilities {
                     configuration: Some(true),
+                    did_change_watched_files: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: Some(true),
+                    }),
                     did_change_configuration: Some(DynamicRegistrationClientCapabilities {
                         dynamic_registration: Some(true),
                     }),

crates/outline/Cargo.toml 🔗

@@ -18,5 +18,5 @@ settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2"

crates/project/Cargo.toml 🔗

@@ -27,6 +27,7 @@ fs = { path = "../fs" }
 fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 git = { path = "../git" }
+glob = { version = "0.3.1" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
@@ -44,7 +45,7 @@ ignore = "0.4"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 pulldown-cmark = { version = "0.9.1", default-features = false }
 rand = "0.8.3"
 regex = "1.5"
@@ -58,6 +59,8 @@ thiserror = "1.0.29"
 toml = "0.5"
 
 [dev-dependencies]
+ctor = "0.1"
+env_logger = "0.9"
 pretty_assertions = "1.3.0"
 client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }

crates/project/src/lsp_glob_set.rs 🔗

@@ -0,0 +1,121 @@
+use anyhow::{anyhow, Result};
+use std::path::Path;
+
+#[derive(Default)]
+pub struct LspGlobSet {
+    patterns: Vec<glob::Pattern>,
+}
+
+impl LspGlobSet {
+    pub fn clear(&mut self) {
+        self.patterns.clear();
+    }
+
+    /// Add a pattern to the glob set.
+    ///
+    /// LSP's glob syntax supports bash-style brace expansion. For example,
+    /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
+    /// This is not a part of the standard libc glob syntax, and isn't supported
+    /// by the `glob` crate. So we pre-process the glob patterns, producing a
+    /// separate glob `Pattern` object for each part of a brace expansion.
+    pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
+        // Find all of the ranges of `pattern` that contain matched curly braces.
+        let mut expansion_ranges = Vec::new();
+        let mut expansion_start_ix = None;
+        for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
+            match c {
+                "{" => {
+                    if expansion_start_ix.is_some() {
+                        return Err(anyhow!("nested braces in glob patterns aren't supported"));
+                    }
+                    expansion_start_ix = Some(ix);
+                }
+                "}" => {
+                    if let Some(start_ix) = expansion_start_ix {
+                        expansion_ranges.push(start_ix..ix + 1);
+                    }
+                    expansion_start_ix = None;
+                }
+                _ => {}
+            }
+        }
+
+        // Starting with a single pattern, process each brace expansion by cloning
+        // the pattern once per element of the expansion.
+        let mut unexpanded_patterns = vec![];
+        let mut expanded_patterns = vec![pattern.to_string()];
+
+        for outer_range in expansion_ranges.into_iter().rev() {
+            let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
+            std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
+            for unexpanded_pattern in unexpanded_patterns.drain(..) {
+                for part in unexpanded_pattern[inner_range.clone()].split(',') {
+                    let mut expanded_pattern = unexpanded_pattern.clone();
+                    expanded_pattern.replace_range(outer_range.clone(), part);
+                    expanded_patterns.push(expanded_pattern);
+                }
+            }
+        }
+
+        // Parse the final glob patterns and add them to the set.
+        for pattern in expanded_patterns {
+            let pattern = glob::Pattern::new(&pattern)?;
+            self.patterns.push(pattern);
+        }
+
+        Ok(())
+    }
+
+    pub fn matches(&self, path: &Path) -> bool {
+        self.patterns
+            .iter()
+            .any(|pattern| pattern.matches_path(path))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_glob_set() {
+        let mut watch = LspGlobSet::default();
+        watch.add_pattern("/a/**/*.rs").unwrap();
+        watch.add_pattern("/a/**/Cargo.toml").unwrap();
+
+        assert!(watch.matches("/a/b.rs".as_ref()));
+        assert!(watch.matches("/a/b/c.rs".as_ref()));
+
+        assert!(!watch.matches("/b/c.rs".as_ref()));
+        assert!(!watch.matches("/a/b.ts".as_ref()));
+    }
+
+    #[test]
+    fn test_brace_expansion() {
+        let mut watch = LspGlobSet::default();
+        watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
+
+        assert!(watch.matches("/a/one.js".as_ref()));
+        assert!(watch.matches("/a/two.ts".as_ref()));
+        assert!(watch.matches("/a/three.tsx".as_ref()));
+
+        assert!(!watch.matches("/a/one.j".as_ref()));
+        assert!(!watch.matches("/a/two.s".as_ref()));
+        assert!(!watch.matches("/a/three.t".as_ref()));
+        assert!(!watch.matches("/a/four.t".as_ref()));
+        assert!(!watch.matches("/a/five.xt".as_ref()));
+    }
+
+    #[test]
+    fn test_multiple_brace_expansion() {
+        let mut watch = LspGlobSet::default();
+        watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
+
+        assert!(watch.matches("/a/one.bic".as_ref()));
+        assert!(watch.matches("/a/two.dole".as_ref()));
+        assert!(watch.matches("/a/three.deeee".as_ref()));
+
+        assert!(!watch.matches("/a/four.bic".as_ref()));
+        assert!(!watch.matches("/a/one.be".as_ref()));
+    }
+}

crates/project/src/project.rs 🔗

@@ -1,5 +1,6 @@
 mod ignore;
 mod lsp_command;
+mod lsp_glob_set;
 pub mod search;
 pub mod terminals;
 pub mod worktree;
@@ -33,10 +34,11 @@ use language::{
     Transaction, Unclipped,
 };
 use lsp::{
-    DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
-    MarkedString,
+    DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
+    DocumentHighlightKind, LanguageServer, LanguageString, MarkedString,
 };
 use lsp_command::*;
+use lsp_glob_set::LspGlobSet;
 use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
@@ -188,6 +190,7 @@ pub enum LanguageServerState {
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
         server: Arc<LanguageServer>,
+        watched_paths: LspGlobSet,
         simulate_disk_based_diagnostics_completion: Option<Task<()>>,
     },
 }
@@ -2046,8 +2049,26 @@ impl Project {
                             })
                             .detach();
                         language_server
-                            .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
-                                Ok(())
+                            .on_request::<lsp::request::RegisterCapability, _, _>({
+                                let this = this.downgrade();
+                                move |params, mut cx| async move {
+                                    let this = this
+                                        .upgrade(&cx)
+                                        .ok_or_else(|| anyhow!("project dropped"))?;
+                                    for reg in params.registrations {
+                                        if reg.method == "workspace/didChangeWatchedFiles" {
+                                            if let Some(options) = reg.register_options {
+                                                let options = serde_json::from_value(options)?;
+                                                this.update(&mut cx, |this, cx| {
+                                                    this.on_lsp_did_change_watched_files(
+                                                        server_id, options, cx,
+                                                    );
+                                                });
+                                            }
+                                        }
+                                    }
+                                    Ok(())
+                                }
                             })
                             .detach();
 
@@ -2117,6 +2138,7 @@ impl Project {
                                 LanguageServerState::Running {
                                     adapter: adapter.clone(),
                                     language,
+                                    watched_paths: Default::default(),
                                     server: language_server.clone(),
                                     simulate_disk_based_diagnostics_completion: None,
                                 },
@@ -2509,6 +2531,23 @@ impl Project {
         }
     }
 
+    fn on_lsp_did_change_watched_files(
+        &mut self,
+        language_server_id: usize,
+        params: DidChangeWatchedFilesRegistrationOptions,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(LanguageServerState::Running { watched_paths, .. }) =
+            self.language_servers.get_mut(&language_server_id)
+        {
+            watched_paths.clear();
+            for watcher in params.watchers {
+                watched_paths.add_pattern(&watcher.glob_pattern).log_err();
+            }
+            cx.notify();
+        }
+    }
+
     async fn on_lsp_workspace_edit(
         this: WeakModelHandle<Self>,
         params: lsp::ApplyWorkspaceEditParams,
@@ -4465,7 +4504,10 @@ impl Project {
         cx.observe(worktree, |_, _, cx| cx.notify()).detach();
         if worktree.read(cx).is_local() {
             cx.subscribe(worktree, |this, worktree, event, cx| match event {
-                worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
+                worktree::Event::UpdatedEntries(changes) => {
+                    this.update_local_worktree_buffers(&worktree, cx);
+                    this.update_local_worktree_language_servers(&worktree, changes, cx);
+                }
                 worktree::Event::UpdatedGitRepositories(updated_repos) => {
                     this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
                 }
@@ -4496,7 +4538,7 @@ impl Project {
 
     fn update_local_worktree_buffers(
         &mut self,
-        worktree_handle: ModelHandle<Worktree>,
+        worktree_handle: &ModelHandle<Worktree>,
         cx: &mut ModelContext<Self>,
     ) {
         let snapshot = worktree_handle.read(cx).snapshot();
@@ -4506,7 +4548,7 @@ impl Project {
             if let Some(buffer) = buffer.upgrade(cx) {
                 buffer.update(cx, |buffer, cx| {
                     if let Some(old_file) = File::from_dyn(buffer.file()) {
-                        if old_file.worktree != worktree_handle {
+                        if old_file.worktree != *worktree_handle {
                             return;
                         }
 
@@ -4578,6 +4620,58 @@ impl Project {
         }
     }
 
+    fn update_local_worktree_language_servers(
+        &mut self,
+        worktree_handle: &ModelHandle<Worktree>,
+        changes: &HashMap<Arc<Path>, PathChange>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let worktree_id = worktree_handle.read(cx).id();
+        let abs_path = worktree_handle.read(cx).abs_path();
+        for ((server_worktree_id, _), server_id) in &self.language_server_ids {
+            if *server_worktree_id == worktree_id {
+                if let Some(server) = self.language_servers.get(server_id) {
+                    if let LanguageServerState::Running {
+                        server,
+                        watched_paths,
+                        ..
+                    } = server
+                    {
+                        let params = lsp::DidChangeWatchedFilesParams {
+                            changes: changes
+                                .iter()
+                                .filter_map(|(path, change)| {
+                                    let path = abs_path.join(path);
+                                    if watched_paths.matches(&path) {
+                                        Some(lsp::FileEvent {
+                                            uri: lsp::Url::from_file_path(path).unwrap(),
+                                            typ: match change {
+                                                PathChange::Added => lsp::FileChangeType::CREATED,
+                                                PathChange::Removed => lsp::FileChangeType::DELETED,
+                                                PathChange::Updated
+                                                | PathChange::AddedOrUpdated => {
+                                                    lsp::FileChangeType::CHANGED
+                                                }
+                                            },
+                                        })
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect(),
+                        };
+
+                        if !params.changes.is_empty() {
+                            server
+                                .notify::<lsp::notification::DidChangeWatchedFiles>(params)
+                                .log_err();
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     fn update_local_worktree_buffers_git_repos(
         &mut self,
         worktree: ModelHandle<Worktree>,

crates/project/src/project_tests.rs 🔗

@@ -8,12 +8,21 @@ use language::{
     OffsetRangeExt, Point, ToPoint,
 };
 use lsp::Url;
+use parking_lot::Mutex;
 use pretty_assertions::assert_eq;
 use serde_json::json;
 use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
 use unindent::Unindent as _;
 use util::{assert_set_eq, test::temp_tree};
 
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}
+
 #[gpui::test]
 async fn test_symlinks(cx: &mut gpui::TestAppContext) {
     let dir = temp_tree(json!({
@@ -438,6 +447,111 @@ async fn test_managing_language_servers(
     );
 }
 
+#[gpui::test]
+async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
+    cx.foreground().forbid_parking();
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            name: "the-language-server",
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            "a.rs": "",
+            "b.rs": "",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    project.update(cx, |project, _| {
+        project.languages.add(Arc::new(language));
+    });
+    cx.foreground().run_until_parked();
+
+    // Start the language server by opening a buffer with a compatible file extension.
+    let _buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/the-root/a.rs", cx)
+        })
+        .await
+        .unwrap();
+
+    // Keep track of the FS events reported to the language server.
+    let fake_server = fake_servers.next().await.unwrap();
+    let file_changes = Arc::new(Mutex::new(Vec::new()));
+    fake_server
+        .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
+            registrations: vec![lsp::Registration {
+                id: Default::default(),
+                method: "workspace/didChangeWatchedFiles".to_string(),
+                register_options: serde_json::to_value(
+                    lsp::DidChangeWatchedFilesRegistrationOptions {
+                        watchers: vec![lsp::FileSystemWatcher {
+                            glob_pattern: "*.{rs,c}".to_string(),
+                            kind: None,
+                        }],
+                    },
+                )
+                .ok(),
+            }],
+        })
+        .await
+        .unwrap();
+    fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
+        let file_changes = file_changes.clone();
+        move |params, _| {
+            let mut file_changes = file_changes.lock();
+            file_changes.extend(params.changes);
+            file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
+        }
+    });
+
+    cx.foreground().run_until_parked();
+    assert_eq!(file_changes.lock().len(), 0);
+
+    // Perform some file system mutations, two of which match the watched patterns,
+    // and one of which does not.
+    fs.create_file("/the-root/c.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.create_file("/the-root/d.txt".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+
+    // The language server receives events for the FS mutations that match its watch patterns.
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        &*file_changes.lock(),
+        &[
+            lsp::FileEvent {
+                uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
+                typ: lsp::FileChangeType::DELETED,
+            },
+            lsp::FileEvent {
+                uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
+                typ: lsp::FileChangeType::CREATED,
+            },
+        ]
+    );
+}
+
 #[gpui::test]
 async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     cx.foreground().forbid_parking();
@@ -1585,7 +1699,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
             buffer.text(),
             "
                 use a::{b, c};
-                
+
                 fn f() {
                     b();
                     c();
@@ -1603,7 +1717,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
     let text = "
         use a::b;
         use a::c;
-        
+
         fn f() {
             b();
             c();
@@ -1688,7 +1802,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
             buffer.text(),
             "
                 use a::{b, c};
-                
+
                 fn f() {
                     b();
                     c();

crates/project/src/worktree.rs 🔗

@@ -1,18 +1,18 @@
-use super::{ignore::IgnoreStack, DiagnosticSummary};
-use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
+use crate::{
+    copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+};
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context, Result};
 use client::{proto, Client};
 use clock::ReplicaId;
 use collections::{HashMap, VecDeque};
-use fs::LineEnding;
-use fs::{repository::GitRepository, Fs};
+use fs::{repository::GitRepository, Fs, LineEnding};
 use futures::{
     channel::{
         mpsc::{self, UnboundedSender},
         oneshot,
     },
-    Stream, StreamExt,
+    select_biased, Stream, StreamExt,
 };
 use fuzzy::CharBag;
 use git::{DOT_GIT, GITIGNORE};
@@ -20,20 +20,19 @@ use gpui::{
     executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
     Task,
 };
-use language::File as _;
 use language::{
     proto::{
         deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
         serialize_version,
     },
-    Buffer, DiagnosticEntry, PointUtf16, Rope, RopeFingerprint, Unclipped,
+    Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
 };
 use parking_lot::Mutex;
 use postage::{
+    barrier,
     prelude::{Sink as _, Stream as _},
     watch,
 };
-
 use smol::channel::{self, Sender};
 use std::{
     any::Any,
@@ -45,18 +44,19 @@ use std::{
     mem,
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
-    sync::{atomic::AtomicUsize, Arc},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
     task::Poll,
     time::{Duration, SystemTime},
 };
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::paths::HOME;
-use util::{ResultExt, TryFutureExt};
+use util::{paths::HOME, ResultExt, TryFutureExt};
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
 pub struct WorktreeId(usize);
 
-#[allow(clippy::large_enum_variant)]
 pub enum Worktree {
     Local(LocalWorktree),
     Remote(RemoteWorktree),
@@ -64,10 +64,9 @@ pub enum Worktree {
 
 pub struct LocalWorktree {
     snapshot: LocalSnapshot,
-    background_snapshot: Arc<Mutex<LocalSnapshot>>,
-    last_scan_state_rx: watch::Receiver<ScanState>,
-    _background_scanner_task: Option<Task<()>>,
-    poll_task: Option<Task<()>>,
+    path_changes_tx: channel::Sender<(Vec<PathBuf>, barrier::Sender)>,
+    is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
+    _background_scanner_task: Task<()>,
     share: Option<ShareState>,
     diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
@@ -77,8 +76,8 @@ pub struct LocalWorktree {
 }
 
 pub struct RemoteWorktree {
-    pub snapshot: Snapshot,
-    pub(crate) background_snapshot: Arc<Mutex<Snapshot>>,
+    snapshot: Snapshot,
+    background_snapshot: Arc<Mutex<Snapshot>>,
     project_id: u64,
     client: Arc<Client>,
     updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
@@ -118,11 +117,11 @@ impl std::fmt::Debug for GitRepositoryEntry {
         f.debug_struct("GitRepositoryEntry")
             .field("content_path", &self.content_path)
             .field("git_dir_path", &self.git_dir_path)
-            .field("libgit_repository", &"LibGitRepository")
             .finish()
     }
 }
 
+#[derive(Debug)]
 pub struct LocalSnapshot {
     ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
     git_repositories: Vec<GitRepositoryEntry>,
@@ -157,14 +156,22 @@ impl DerefMut for LocalSnapshot {
     }
 }
 
-#[derive(Clone, Debug)]
 enum ScanState {
-    Idle,
     /// The worktree is performing its initial scan of the filesystem.
-    Initializing,
+    Initializing {
+        snapshot: LocalSnapshot,
+        barrier: Option<barrier::Sender>,
+    },
+    Initialized {
+        snapshot: LocalSnapshot,
+    },
     /// The worktree is updating in response to filesystem events.
     Updating,
-    Err(Arc<anyhow::Error>),
+    Updated {
+        snapshot: LocalSnapshot,
+        changes: HashMap<Arc<Path>, PathChange>,
+        barrier: Option<barrier::Sender>,
+    },
 }
 
 struct ShareState {
@@ -175,7 +182,7 @@ struct ShareState {
 }
 
 pub enum Event {
-    UpdatedEntries,
+    UpdatedEntries(HashMap<Arc<Path>, PathChange>),
     UpdatedGitRepositories(Vec<GitRepositoryEntry>),
 }
 
@@ -192,21 +199,87 @@ impl Worktree {
         next_entry_id: Arc<AtomicUsize>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
-        let (tree, scan_states_tx) =
-            LocalWorktree::create(client, path, visible, fs.clone(), next_entry_id, cx).await?;
-        tree.update(cx, |tree, cx| {
-            let tree = tree.as_local_mut().unwrap();
-            let abs_path = tree.abs_path().clone();
-            let background_snapshot = tree.background_snapshot.clone();
-            let background = cx.background().clone();
-            tree._background_scanner_task = Some(cx.background().spawn(async move {
-                let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
-                let scanner =
-                    BackgroundScanner::new(background_snapshot, scan_states_tx, fs, background);
-                scanner.run(events).await;
-            }));
-        });
-        Ok(tree)
+        // After determining whether the root entry is a file or a directory, populate the
+        // snapshot's "root name", which will be used for the purpose of fuzzy matching.
+        let abs_path = path.into();
+        let metadata = fs
+            .metadata(&abs_path)
+            .await
+            .context("failed to stat worktree path")?;
+
+        Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
+            let root_name = abs_path
+                .file_name()
+                .map_or(String::new(), |f| f.to_string_lossy().to_string());
+
+            let mut snapshot = LocalSnapshot {
+                ignores_by_parent_abs_path: Default::default(),
+                git_repositories: Default::default(),
+                removed_entry_ids: Default::default(),
+                next_entry_id,
+                snapshot: Snapshot {
+                    id: WorktreeId::from_usize(cx.model_id()),
+                    abs_path: abs_path.clone(),
+                    root_name: root_name.clone(),
+                    root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
+                    entries_by_path: Default::default(),
+                    entries_by_id: Default::default(),
+                    scan_id: 0,
+                    completed_scan_id: 0,
+                },
+            };
+
+            if let Some(metadata) = metadata {
+                snapshot.insert_entry(
+                    Entry::new(
+                        Arc::from(Path::new("")),
+                        &metadata,
+                        &snapshot.next_entry_id,
+                        snapshot.root_char_bag,
+                    ),
+                    fs.as_ref(),
+                );
+            }
+
+            let (path_changes_tx, path_changes_rx) = channel::unbounded();
+            let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+
+            cx.spawn_weak(|this, mut cx| async move {
+                while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
+                    this.update(&mut cx, |this, cx| {
+                        this.as_local_mut()
+                            .unwrap()
+                            .background_scanner_updated(state, cx);
+                    });
+                }
+            })
+            .detach();
+
+            let background_scanner_task = cx.background().spawn({
+                let fs = fs.clone();
+                let snapshot = snapshot.clone();
+                let background = cx.background().clone();
+                async move {
+                    let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+                    BackgroundScanner::new(snapshot, scan_states_tx, fs, background)
+                        .run(events, path_changes_rx)
+                        .await;
+                }
+            });
+
+            Worktree::Local(LocalWorktree {
+                snapshot,
+                is_scanning: watch::channel_with(true),
+                share: None,
+                path_changes_tx,
+                _background_scanner_task: background_scanner_task,
+                diagnostics: Default::default(),
+                diagnostic_summaries: Default::default(),
+                client,
+                fs,
+                visible,
+            })
+        }))
     }
 
     pub fn remote(
@@ -216,64 +289,50 @@ impl Worktree {
         client: Arc<Client>,
         cx: &mut MutableAppContext,
     ) -> ModelHandle<Self> {
-        let remote_id = worktree.id;
-        let root_char_bag: CharBag = worktree
-            .root_name
-            .chars()
-            .map(|c| c.to_ascii_lowercase())
-            .collect();
-        let root_name = worktree.root_name.clone();
-        let visible = worktree.visible;
-
-        let abs_path = PathBuf::from(worktree.abs_path);
-        let snapshot = Snapshot {
-            id: WorktreeId(remote_id as usize),
-            abs_path: Arc::from(abs_path.deref()),
-            root_name,
-            root_char_bag,
-            entries_by_path: Default::default(),
-            entries_by_id: Default::default(),
-            scan_id: 0,
-            completed_scan_id: 0,
-        };
-
-        let (updates_tx, mut updates_rx) = mpsc::unbounded();
-        let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
-        let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
-        let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
-            Worktree::Remote(RemoteWorktree {
-                project_id: project_remote_id,
-                replica_id,
-                snapshot: snapshot.clone(),
-                background_snapshot: background_snapshot.clone(),
-                updates_tx: Some(updates_tx),
-                snapshot_subscriptions: Default::default(),
-                client: client.clone(),
-                diagnostic_summaries: Default::default(),
-                visible,
-                disconnected: false,
-            })
-        });
+        cx.add_model(|cx: &mut ModelContext<Self>| {
+            let snapshot = Snapshot {
+                id: WorktreeId(worktree.id as usize),
+                abs_path: Arc::from(PathBuf::from(worktree.abs_path)),
+                root_name: worktree.root_name.clone(),
+                root_char_bag: worktree
+                    .root_name
+                    .chars()
+                    .map(|c| c.to_ascii_lowercase())
+                    .collect(),
+                entries_by_path: Default::default(),
+                entries_by_id: Default::default(),
+                scan_id: 0,
+                completed_scan_id: 0,
+            };
 
-        cx.background()
-            .spawn(async move {
-                while let Some(update) = updates_rx.next().await {
-                    if let Err(error) = background_snapshot.lock().apply_remote_update(update) {
-                        log::error!("error applying worktree update: {}", error);
+            let (updates_tx, mut updates_rx) = mpsc::unbounded();
+            let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
+            let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
+
+            cx.background()
+                .spawn({
+                    let background_snapshot = background_snapshot.clone();
+                    async move {
+                        while let Some(update) = updates_rx.next().await {
+                            if let Err(error) =
+                                background_snapshot.lock().apply_remote_update(update)
+                            {
+                                log::error!("error applying worktree update: {}", error);
+                            }
+                            snapshot_updated_tx.send(()).await.ok();
+                        }
                     }
-                    snapshot_updated_tx.send(()).await.ok();
-                }
-            })
-            .detach();
+                })
+                .detach();
 
-        cx.spawn(|mut cx| {
-            let this = worktree_handle.downgrade();
-            async move {
+            cx.spawn_weak(|this, mut cx| async move {
                 while (snapshot_updated_rx.recv().await).is_some() {
                     if let Some(this) = this.upgrade(&cx) {
                         this.update(&mut cx, |this, cx| {
-                            this.poll_snapshot(cx);
                             let this = this.as_remote_mut().unwrap();
+                            this.snapshot = this.background_snapshot.lock().clone();
+                            cx.emit(Event::UpdatedEntries(Default::default()));
+                            cx.notify();
                             while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
                                 if this.observed_snapshot(*scan_id) {
                                     let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
@@ -287,11 +346,22 @@ impl Worktree {
                         break;
                     }
                 }
-            }
-        })
-        .detach();
+            })
+            .detach();
 
-        worktree_handle
+            Worktree::Remote(RemoteWorktree {
+                project_id: project_remote_id,
+                replica_id,
+                snapshot: snapshot.clone(),
+                background_snapshot,
+                updates_tx: Some(updates_tx),
+                snapshot_subscriptions: Default::default(),
+                client: client.clone(),
+                diagnostic_summaries: Default::default(),
+                visible: worktree.visible,
+                disconnected: false,
+            })
+        })
     }
 
     pub fn as_local(&self) -> Option<&LocalWorktree> {
@@ -380,13 +450,6 @@ impl Worktree {
         .map(|(path, summary)| (path.0.clone(), *summary))
     }
 
-    fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
-        match self {
-            Self::Local(worktree) => worktree.poll_snapshot(false, cx),
-            Self::Remote(worktree) => worktree.poll_snapshot(cx),
-        };
-    }
-
     pub fn abs_path(&self) -> Arc<Path> {
         match self {
             Worktree::Local(worktree) => worktree.abs_path.clone(),
@@ -396,90 +459,6 @@ impl Worktree {
 }
 
 impl LocalWorktree {
-    async fn create(
-        client: Arc<Client>,
-        path: impl Into<Arc<Path>>,
-        visible: bool,
-        fs: Arc<dyn Fs>,
-        next_entry_id: Arc<AtomicUsize>,
-        cx: &mut AsyncAppContext,
-    ) -> Result<(ModelHandle<Worktree>, UnboundedSender<ScanState>)> {
-        let abs_path = path.into();
-        let path: Arc<Path> = Arc::from(Path::new(""));
-
-        // After determining whether the root entry is a file or a directory, populate the
-        // snapshot's "root name", which will be used for the purpose of fuzzy matching.
-        let root_name = abs_path
-            .file_name()
-            .map_or(String::new(), |f| f.to_string_lossy().to_string());
-        let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
-        let metadata = fs
-            .metadata(&abs_path)
-            .await
-            .context("failed to stat worktree path")?;
-
-        let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
-        let (mut last_scan_state_tx, last_scan_state_rx) =
-            watch::channel_with(ScanState::Initializing);
-        let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
-            let mut snapshot = LocalSnapshot {
-                ignores_by_parent_abs_path: Default::default(),
-                git_repositories: Default::default(),
-                removed_entry_ids: Default::default(),
-                next_entry_id,
-                snapshot: Snapshot {
-                    id: WorktreeId::from_usize(cx.model_id()),
-                    abs_path,
-                    root_name: root_name.clone(),
-                    root_char_bag,
-                    entries_by_path: Default::default(),
-                    entries_by_id: Default::default(),
-                    scan_id: 0,
-                    completed_scan_id: 0,
-                },
-            };
-            if let Some(metadata) = metadata {
-                let entry = Entry::new(
-                    path,
-                    &metadata,
-                    &snapshot.next_entry_id,
-                    snapshot.root_char_bag,
-                );
-                snapshot.insert_entry(entry, fs.as_ref());
-            }
-
-            let tree = Self {
-                snapshot: snapshot.clone(),
-                background_snapshot: Arc::new(Mutex::new(snapshot)),
-                last_scan_state_rx,
-                _background_scanner_task: None,
-                share: None,
-                poll_task: None,
-                diagnostics: Default::default(),
-                diagnostic_summaries: Default::default(),
-                client,
-                fs,
-                visible,
-            };
-
-            cx.spawn_weak(|this, mut cx| async move {
-                while let Some(scan_state) = scan_states_rx.next().await {
-                    if let Some(this) = this.upgrade(&cx) {
-                        last_scan_state_tx.blocking_send(scan_state).ok();
-                        this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
-                    } else {
-                        break;
-                    }
-                }
-            })
-            .detach();
-
-            Worktree::Local(tree)
-        });
-
-        Ok((tree, scan_states_tx))
-    }
-
     pub fn contains_abs_path(&self, path: &Path) -> bool {
         path.starts_with(&self.abs_path)
     }
@@ -557,68 +536,54 @@ impl LocalWorktree {
         Ok(updated)
     }
 
-    fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
-        self.poll_task.take();
-
-        match self.scan_state() {
-            ScanState::Idle => {
-                let new_snapshot = self.background_snapshot.lock().clone();
-                let updated_repos = Self::changed_repos(
-                    &self.snapshot.git_repositories,
-                    &new_snapshot.git_repositories,
-                );
-                self.snapshot = new_snapshot;
-
-                if let Some(share) = self.share.as_mut() {
-                    *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
-                }
-
-                cx.emit(Event::UpdatedEntries);
-
-                if !updated_repos.is_empty() {
-                    cx.emit(Event::UpdatedGitRepositories(updated_repos));
-                }
+    fn background_scanner_updated(
+        &mut self,
+        scan_state: ScanState,
+        cx: &mut ModelContext<Worktree>,
+    ) {
+        match scan_state {
+            ScanState::Initializing { snapshot, barrier } => {
+                *self.is_scanning.0.borrow_mut() = true;
+                self.set_snapshot(snapshot, cx);
+                drop(barrier);
             }
-
-            ScanState::Initializing => {
-                let is_fake_fs = self.fs.is_fake();
-
-                let new_snapshot = self.background_snapshot.lock().clone();
-                let updated_repos = Self::changed_repos(
-                    &self.snapshot.git_repositories,
-                    &new_snapshot.git_repositories,
-                );
-                self.snapshot = new_snapshot;
-
-                self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
-                    if is_fake_fs {
-                        #[cfg(any(test, feature = "test-support"))]
-                        cx.background().simulate_random_delay().await;
-                    } else {
-                        smol::Timer::after(Duration::from_millis(100)).await;
-                    }
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
-                    }
-                }));
-
-                cx.emit(Event::UpdatedEntries);
-
-                if !updated_repos.is_empty() {
-                    cx.emit(Event::UpdatedGitRepositories(updated_repos));
-                }
+            ScanState::Initialized { snapshot } => {
+                *self.is_scanning.0.borrow_mut() = false;
+                self.set_snapshot(snapshot, cx);
             }
-
-            _ => {
-                if force {
-                    self.snapshot = self.background_snapshot.lock().clone();
-                }
+            ScanState::Updating => {
+                *self.is_scanning.0.borrow_mut() = true;
+            }
+            ScanState::Updated {
+                snapshot,
+                changes,
+                barrier,
+            } => {
+                *self.is_scanning.0.borrow_mut() = false;
+                cx.emit(Event::UpdatedEntries(changes));
+                self.set_snapshot(snapshot, cx);
+                drop(barrier);
             }
         }
-
         cx.notify();
     }
 
+    fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
+        let updated_repos = Self::changed_repos(
+            &self.snapshot.git_repositories,
+            &new_snapshot.git_repositories,
+        );
+        self.snapshot = new_snapshot;
+
+        if let Some(share) = self.share.as_mut() {
+            *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
+        }
+
+        if !updated_repos.is_empty() {
+            cx.emit(Event::UpdatedGitRepositories(updated_repos));
+        }
+    }
+
     fn changed_repos(
         old_repos: &[GitRepositoryEntry],
         new_repos: &[GitRepositoryEntry],
@@ -648,19 +613,19 @@ impl LocalWorktree {
     }
 
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
-        let mut scan_state_rx = self.last_scan_state_rx.clone();
+        let mut is_scanning_rx = self.is_scanning.1.clone();
         async move {
-            let mut scan_state = Some(scan_state_rx.borrow().clone());
-            while let Some(ScanState::Initializing | ScanState::Updating) = scan_state {
-                scan_state = scan_state_rx.recv().await;
+            let mut is_scanning = is_scanning_rx.borrow().clone();
+            while is_scanning {
+                if let Some(value) = is_scanning_rx.recv().await {
+                    is_scanning = value;
+                } else {
+                    break;
+                }
             }
         }
     }
 
-    fn scan_state(&self) -> ScanState {
-        self.last_scan_state_rx.borrow().clone()
-    }
-
     pub fn snapshot(&self) -> LocalSnapshot {
         self.snapshot.clone()
     }
@@ -704,9 +669,7 @@ impl LocalWorktree {
             // Eagerly populate the snapshot with an updated entry for the loaded file
             let entry = this
                 .update(&mut cx, |this, cx| {
-                    this.as_local()
-                        .unwrap()
-                        .refresh_entry(path, abs_path, None, cx)
+                    this.as_local().unwrap().refresh_entry(path, None, cx)
                 })
                 .await?;
 
@@ -797,15 +760,25 @@ impl LocalWorktree {
         is_dir: bool,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
-        self.write_entry_internal(
-            path,
+        let path = path.into();
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let write = cx.background().spawn(async move {
             if is_dir {
-                None
+                fs.create_dir(&abs_path).await
             } else {
-                Some(Default::default())
-            },
-            cx,
-        )
+                fs.save(&abs_path, &Default::default(), Default::default())
+                    .await
+            }
+        });
+
+        cx.spawn(|this, mut cx| async move {
+            write.await?;
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut().unwrap().refresh_entry(path, None, cx)
+            })
+            .await
+        })
     }
 
     pub fn write_file(
@@ -815,7 +788,20 @@ impl LocalWorktree {
         line_ending: LineEnding,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
-        self.write_entry_internal(path, Some((text, line_ending)), cx)
+        let path = path.into();
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let write = cx
+            .background()
+            .spawn(async move { fs.save(&abs_path, &text, line_ending).await });
+
+        cx.spawn(|this, mut cx| async move {
+            write.await?;
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut().unwrap().refresh_entry(path, None, cx)
+            })
+            .await
+        })
     }
 
     pub fn delete_entry(
@@ -824,36 +810,40 @@ impl LocalWorktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Option<Task<Result<()>>> {
         let entry = self.entry_for_id(entry_id)?.clone();
-        let abs_path = self.absolutize(&entry.path);
-        let delete = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_path = abs_path;
-            async move {
-                if entry.is_file() {
-                    fs.remove_file(&abs_path, Default::default()).await
-                } else {
-                    fs.remove_dir(
-                        &abs_path,
-                        RemoveOptions {
-                            recursive: true,
-                            ignore_if_not_exists: false,
-                        },
-                    )
-                    .await
-                }
+        let abs_path = self.abs_path.clone();
+        let fs = self.fs.clone();
+
+        let delete = cx.background().spawn(async move {
+            let mut abs_path = fs.canonicalize(&abs_path).await?;
+            if entry.path.file_name().is_some() {
+                abs_path = abs_path.join(&entry.path);
+            }
+            if entry.is_file() {
+                fs.remove_file(&abs_path, Default::default()).await?;
+            } else {
+                fs.remove_dir(
+                    &abs_path,
+                    RemoveOptions {
+                        recursive: true,
+                        ignore_if_not_exists: false,
+                    },
+                )
+                .await?;
             }
+            anyhow::Ok(abs_path)
         });
 
         Some(cx.spawn(|this, mut cx| async move {
-            delete.await?;
-            this.update(&mut cx, |this, cx| {
-                let this = this.as_local_mut().unwrap();
-                {
-                    let mut snapshot = this.background_snapshot.lock();
-                    snapshot.delete_entry(entry_id);
-                }
-                this.poll_snapshot(true, cx);
+            let abs_path = delete.await?;
+            let (tx, mut rx) = barrier::channel();
+            this.update(&mut cx, |this, _| {
+                this.as_local_mut()
+                    .unwrap()
+                    .path_changes_tx
+                    .try_send((vec![abs_path], tx))
+                    .unwrap();
             });
+            rx.recv().await;
             Ok(())
         }))
     }
@@ -867,29 +857,21 @@ impl LocalWorktree {
         let old_path = self.entry_for_id(entry_id)?.path.clone();
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
-        let abs_new_path = self.absolutize(new_path.as_ref());
-        let rename = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_new_path = abs_new_path.clone();
-            async move {
-                fs.rename(&abs_old_path, &abs_new_path, Default::default())
-                    .await
-            }
+        let abs_new_path = self.absolutize(&new_path);
+        let fs = self.fs.clone();
+        let rename = cx.background().spawn(async move {
+            fs.rename(&abs_old_path, &abs_new_path, Default::default())
+                .await
         });
 
         Some(cx.spawn(|this, mut cx| async move {
             rename.await?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut().unwrap().refresh_entry(
-                        new_path.clone(),
-                        abs_new_path,
-                        Some(old_path),
-                        cx,
-                    )
-                })
-                .await?;
-            Ok(entry)
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entry(new_path.clone(), Some(old_path), cx)
+            })
+            .await
         }))
     }
 
@@ -903,111 +885,63 @@ impl LocalWorktree {
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
-        let copy = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_new_path = abs_new_path.clone();
-            async move {
-                copy_recursive(
-                    fs.as_ref(),
-                    &abs_old_path,
-                    &abs_new_path,
-                    Default::default(),
-                )
-                .await
-            }
+        let fs = self.fs.clone();
+        let copy = cx.background().spawn(async move {
+            copy_recursive(
+                fs.as_ref(),
+                &abs_old_path,
+                &abs_new_path,
+                Default::default(),
+            )
+            .await
         });
 
         Some(cx.spawn(|this, mut cx| async move {
             copy.await?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut().unwrap().refresh_entry(
-                        new_path.clone(),
-                        abs_new_path,
-                        None,
-                        cx,
-                    )
-                })
-                .await?;
-            Ok(entry)
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entry(new_path.clone(), None, cx)
+            })
+            .await
         }))
     }
 
-    fn write_entry_internal(
-        &self,
-        path: impl Into<Arc<Path>>,
-        text_if_file: Option<(Rope, LineEnding)>,
-        cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
-        let path = path.into();
-        let abs_path = self.absolutize(&path);
-        let write = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_path = abs_path.clone();
-            async move {
-                if let Some((text, line_ending)) = text_if_file {
-                    fs.save(&abs_path, &text, line_ending).await
-                } else {
-                    fs.create_dir(&abs_path).await
-                }
-            }
-        });
-
-        cx.spawn(|this, mut cx| async move {
-            write.await?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut()
-                        .unwrap()
-                        .refresh_entry(path, abs_path, None, cx)
-                })
-                .await?;
-            Ok(entry)
-        })
-    }
-
     fn refresh_entry(
         &self,
         path: Arc<Path>,
-        abs_path: PathBuf,
         old_path: Option<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
         let fs = self.fs.clone();
-        let root_char_bag;
-        let next_entry_id;
-        {
-            let snapshot = self.background_snapshot.lock();
-            root_char_bag = snapshot.root_char_bag;
-            next_entry_id = snapshot.next_entry_id.clone();
-        }
-        cx.spawn_weak(|this, mut cx| async move {
-            let metadata = fs
-                .metadata(&abs_path)
-                .await?
-                .ok_or_else(|| anyhow!("could not read saved file metadata"))?;
-            let this = this
-                .upgrade(&cx)
-                .ok_or_else(|| anyhow!("worktree was dropped"))?;
-            this.update(&mut cx, |this, cx| {
-                let this = this.as_local_mut().unwrap();
-                let inserted_entry;
-                {
-                    let mut snapshot = this.background_snapshot.lock();
-                    let mut entry = Entry::new(path, &metadata, &next_entry_id, root_char_bag);
-                    entry.is_ignored = snapshot
-                        .ignore_stack_for_abs_path(&abs_path, entry.is_dir())
-                        .is_abs_path_ignored(&abs_path, entry.is_dir());
-                    if let Some(old_path) = old_path {
-                        snapshot.remove_path(&old_path);
-                    }
-                    snapshot.scan_started();
-                    inserted_entry = snapshot.insert_entry(entry, fs.as_ref());
-                    snapshot.scan_completed();
-                }
-                this.poll_snapshot(true, cx);
-                Ok(inserted_entry)
-            })
+        let abs_root_path = self.abs_path.clone();
+        let path_changes_tx = self.path_changes_tx.clone();
+        cx.spawn_weak(move |this, mut cx| async move {
+            let abs_path = fs.canonicalize(&abs_root_path).await?;
+            let mut paths = Vec::with_capacity(2);
+            paths.push(if path.file_name().is_some() {
+                abs_path.join(&path)
+            } else {
+                abs_path.clone()
+            });
+            if let Some(old_path) = old_path {
+                paths.push(if old_path.file_name().is_some() {
+                    abs_path.join(&old_path)
+                } else {
+                    abs_path.clone()
+                });
+            }
+
+            let (tx, mut rx) = barrier::channel();
+            path_changes_tx.try_send((paths, tx)).unwrap();
+            rx.recv().await;
+            this.upgrade(&cx)
+                .ok_or_else(|| anyhow!("worktree was dropped"))?
+                .update(&mut cx, |this, _| {
+                    this.entry_for_path(path)
+                        .cloned()
+                        .ok_or_else(|| anyhow!("failed to read path after update"))
+                })
         })
     }
 
@@ -1109,12 +1043,6 @@ impl RemoteWorktree {
         self.snapshot.clone()
     }
 
-    fn poll_snapshot(&mut self, cx: &mut ModelContext<Worktree>) {
-        self.snapshot = self.background_snapshot.lock().clone();
-        cx.emit(Event::UpdatedEntries);
-        cx.notify();
-    }
-
     pub fn disconnected_from_host(&mut self) {
         self.updates_tx.take();
         self.snapshot_subscriptions.clear();
@@ -1274,28 +1202,25 @@ impl Snapshot {
         Ok(entry)
     }
 
-    fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
-        if let Some(removed_entry) = self.entries_by_id.remove(&entry_id, &()) {
-            self.entries_by_path = {
-                let mut cursor = self.entries_by_path.cursor();
-                let mut new_entries_by_path =
-                    cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
-                while let Some(entry) = cursor.item() {
-                    if entry.path.starts_with(&removed_entry.path) {
-                        self.entries_by_id.remove(&entry.id, &());
-                        cursor.next(&());
-                    } else {
-                        break;
-                    }
+    fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option<Arc<Path>> {
+        let removed_entry = self.entries_by_id.remove(&entry_id, &())?;
+        self.entries_by_path = {
+            let mut cursor = self.entries_by_path.cursor();
+            let mut new_entries_by_path =
+                cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &());
+            while let Some(entry) = cursor.item() {
+                if entry.path.starts_with(&removed_entry.path) {
+                    self.entries_by_id.remove(&entry.id, &());
+                    cursor.next(&());
+                } else {
+                    break;
                 }
-                new_entries_by_path.push_tree(cursor.suffix(&()), &());
-                new_entries_by_path
-            };
+            }
+            new_entries_by_path.push_tree(cursor.suffix(&()), &());
+            new_entries_by_path
+        };
 
-            true
-        } else {
-            false
-        }
+        Some(removed_entry.path)
     }
 
     pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {

crates/project_panel/Cargo.toml 🔗

@@ -19,7 +19,7 @@ settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 futures = "0.3"
 unicase = "2.6"
 

crates/project_symbols/Cargo.toml 🔗

@@ -20,7 +20,7 @@ workspace = { path = "../workspace" }
 util = { path = "../util" }
 anyhow = "1.0.38"
 ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2"
 
 [dev-dependencies]

crates/recent_projects/Cargo.toml 🔗

@@ -19,5 +19,5 @@ settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2"

crates/search/Cargo.toml 🔗

@@ -22,7 +22,7 @@ workspace = { path = "../workspace" }
 anyhow = "1.0"
 futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 smallvec = { version = "1.6", features = ["union"] }

crates/settings/Cargo.toml 🔗

@@ -22,7 +22,7 @@ futures = "0.3"
 theme = { path = "../theme" }
 util = { path = "../util" }
 json_comments = "0.2"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 schemars = "0.8"
 serde = { workspace = true }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }

crates/text/Cargo.toml 🔗

@@ -22,7 +22,7 @@ digest = { version = "0.9", features = ["std"] }
 lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 smallvec = { version = "1.6", features = ["union"] }
 util = { path = "../util" }

crates/theme_selector/Cargo.toml 🔗

@@ -19,6 +19,5 @@ workspace = { path = "../workspace" }
 util = { path = "../util" }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2.5"
-

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

@@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut};
 
 use collections::{HashMap, HashSet};
 use gpui::ContextHandle;
-use language::{OffsetRangeExt, Point};
+use language::OffsetRangeExt;
 use util::test::marked_text_offsets;
 
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@@ -108,11 +108,7 @@ impl<'a> NeovimBackedTestContext<'a> {
 
     pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
         let context_handle = self.set_state(marked_text, Mode::Normal);
-
-        let selection = self.editor(|editor, cx| editor.selections.newest::<Point>(cx));
-        let text = self.buffer_text();
-        self.neovim.set_state(selection, &text).await;
-
+        self.neovim.set_state(marked_text).await;
         context_handle
     }
 

crates/vim/src/test/neovim_connection.rs 🔗

@@ -9,7 +9,7 @@ use async_trait::async_trait;
 #[cfg(feature = "neovim")]
 use gpui::keymap_matcher::Keystroke;
 
-use language::{Point, Selection};
+use language::Point;
 
 #[cfg(feature = "neovim")]
 use lazy_static::lazy_static;
@@ -36,11 +36,11 @@ lazy_static! {
     static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
 pub enum NeovimData {
-    Text(String),
-    Selection { start: (u32, u32), end: (u32, u32) },
-    Mode(Option<Mode>),
+    Put { state: String },
+    Key(String),
+    Get { state: String, mode: Option<Mode> },
 }
 
 pub struct NeovimConnection {
@@ -117,18 +117,30 @@ impl NeovimConnection {
 
         let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
 
+        self.data
+            .push_back(NeovimData::Key(keystroke_text.to_string()));
         self.nvim
             .input(&key)
             .await
             .expect("Could not input keystroke");
     }
 
-    // If not running with a live neovim connection, this is a no-op
     #[cfg(not(feature = "neovim"))]
-    pub async fn send_keystroke(&mut self, _keystroke_text: &str) {}
+    pub async fn send_keystroke(&mut self, keystroke_text: &str) {
+        if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
+            self.data.pop_front();
+        }
+        assert_eq!(
+            self.data.pop_front(),
+            Some(NeovimData::Key(keystroke_text.to_string())),
+            "operation does not match recorded script. re-record with --features=neovim"
+        );
+    }
 
     #[cfg(feature = "neovim")]
-    pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
+    pub async fn set_state(&mut self, marked_text: &str) {
+        let (text, selection) = parse_state(&marked_text);
+
         let nvim_buffer = self
             .nvim
             .get_current_buf()
@@ -162,18 +174,41 @@ impl NeovimConnection {
         if !selection.is_empty() {
             panic!("Setting neovim state with non empty selection not yet supported");
         }
-        let cursor = selection.head();
+        let cursor = selection.start;
         nvim_window
             .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
             .await
             .expect("Could not set nvim cursor position");
+
+        if let Some(NeovimData::Get { mode, state }) = self.data.back() {
+            if *mode == Some(Mode::Normal) && *state == marked_text {
+                return;
+            }
+        }
+        self.data.push_back(NeovimData::Put {
+            state: marked_text.to_string(),
+        })
     }
 
     #[cfg(not(feature = "neovim"))]
-    pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
+    pub async fn set_state(&mut self, marked_text: &str) {
+        if let Some(NeovimData::Get { mode, state: text }) = self.data.front() {
+            if *mode == Some(Mode::Normal) && *text == marked_text {
+                return;
+            }
+            self.data.pop_front();
+        }
+        assert_eq!(
+            self.data.pop_front(),
+            Some(NeovimData::Put {
+                state: marked_text.to_string()
+            }),
+            "operation does not match recorded script. re-record with --features=neovim"
+        );
+    }
 
     #[cfg(feature = "neovim")]
-    pub async fn text(&mut self) -> String {
+    pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
         let nvim_buffer = self
             .nvim
             .get_current_buf()
@@ -185,22 +220,6 @@ impl NeovimConnection {
             .expect("Could not get buffer text")
             .join("\n");
 
-        self.data.push_back(NeovimData::Text(text.clone()));
-
-        text
-    }
-
-    #[cfg(not(feature = "neovim"))]
-    pub async fn text(&mut self) -> String {
-        if let Some(NeovimData::Text(text)) = self.data.pop_front() {
-            text
-        } else {
-            panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
-        }
-    }
-
-    #[cfg(feature = "neovim")]
-    pub async fn selection(&mut self) -> Range<Point> {
         let cursor_row: u32 = self
             .nvim
             .command_output("echo line('.')")
@@ -218,7 +237,30 @@ impl NeovimConnection {
             .unwrap()
             - 1; // Neovim columns start at 1
 
-        let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
+        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 (start, end) = if let Some(Mode::Visual { .. }) = mode {
             self.nvim
                 .input("<escape>")
                 .await
@@ -243,72 +285,54 @@ impl NeovimConnection {
 
             if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
                 (
-                    (end_row as u32 - 1, end_col as u32),
-                    (start_row as u32 - 1, start_col as u32),
+                    Point::new(end_row as u32 - 1, end_col as u32),
+                    Point::new(start_row as u32 - 1, start_col as u32),
                 )
             } else {
                 (
-                    (start_row as u32 - 1, start_col as u32),
-                    (end_row as u32 - 1, end_col as u32),
+                    Point::new(start_row as u32 - 1, start_col as u32),
+                    Point::new(end_row as u32 - 1, end_col as u32),
                 )
             }
         } else {
-            ((cursor_row, cursor_col), (cursor_row, cursor_col))
+            (
+                Point::new(cursor_row, cursor_col),
+                Point::new(cursor_row, cursor_col),
+            )
         };
 
-        self.data.push_back(NeovimData::Selection { start, end });
+        let state = NeovimData::Get {
+            mode,
+            state: encode_range(&text, start..end),
+        };
+
+        if self.data.back() != Some(&state) {
+            self.data.push_back(state.clone());
+        }
 
-        Point::new(start.0, start.1)..Point::new(end.0, end.1)
+        (mode, text, start..end)
     }
 
     #[cfg(not(feature = "neovim"))]
-    pub async fn selection(&mut self) -> Range<Point> {
-        // Selection code fetches the mode. This emulates that.
-        let _mode = self.mode().await;
-        if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
-            Point::new(start.0, start.1)..Point::new(end.0, end.1)
+    pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
+        if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
+            let (text, range) = parse_state(text);
+            (*mode, text, range)
         } else {
-            panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
+            panic!("operation does not match recorded script. re-record with --features=neovim");
         }
     }
 
-    #[cfg(feature = "neovim")]
-    pub async fn 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,
-        };
-
-        self.data.push_back(NeovimData::Mode(mode.clone()));
-
-        mode
+    pub async fn selection(&mut self) -> Range<Point> {
+        self.state().await.2
     }
 
-    #[cfg(not(feature = "neovim"))]
     pub async fn mode(&mut self) -> Option<Mode> {
-        if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
-            mode
-        } else {
-            panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
-        }
+        self.state().await.0
+    }
+
+    pub async fn text(&mut self) -> String {
+        self.state().await.1
     }
 
     fn test_data_path(test_case_id: &str) -> PathBuf {
@@ -325,8 +349,27 @@ impl NeovimConnection {
             "Could not read test data. Is it generated? Try running test with '--features neovim'",
         );
 
-        serde_json::from_str(&json)
-            .expect("Test data corrupted. Try regenerating it with '--features neovim'")
+        let mut result = VecDeque::new();
+        for line in json.lines() {
+            result.push_back(
+                serde_json::from_str(line)
+                    .expect("invalid test data. regenerate it with '--features neovim'"),
+            );
+        }
+        result
+    }
+
+    #[cfg(feature = "neovim")]
+    fn write_test_data(test_case_id: &str, data: &VecDeque<NeovimData>) {
+        let path = Self::test_data_path(test_case_id);
+        let mut json = Vec::new();
+        for entry in data {
+            serde_json::to_writer(&mut json, entry).unwrap();
+            json.push(b'\n');
+        }
+        std::fs::create_dir_all(path.parent().unwrap())
+            .expect("could not create test data directory");
+        std::fs::write(path, json).expect("could not write out test data");
     }
 }
 
@@ -349,11 +392,7 @@ impl DerefMut for NeovimConnection {
 #[cfg(feature = "neovim")]
 impl Drop for NeovimConnection {
     fn drop(&mut self) {
-        let path = Self::test_data_path(&self.test_case_id);
-        std::fs::create_dir_all(path.parent().unwrap())
-            .expect("Could not create test data directory");
-        let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
-        std::fs::write(path, json).expect("Could not write out test data");
+        Self::write_test_data(&self.test_case_id, &self.data);
     }
 }
 
@@ -383,3 +422,52 @@ impl Handler for NvimHandler {
     ) {
     }
 }
+
+fn parse_state(marked_text: &str) -> (String, Range<Point>) {
+    let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
+    let byte_range = ranges[0].clone();
+    let mut point_range = Point::zero()..Point::zero();
+    let mut ix = 0;
+    let mut position = Point::zero();
+    for c in text.chars().chain(['\0']) {
+        if ix == byte_range.start {
+            point_range.start = position;
+        }
+        if ix == byte_range.end {
+            point_range.end = position;
+        }
+        let len_utf8 = c.len_utf8();
+        ix += len_utf8;
+        if c == '\n' {
+            position.row += 1;
+            position.column = 0;
+        } else {
+            position.column += len_utf8 as u32;
+        }
+    }
+    (text, point_range)
+}
+
+#[cfg(feature = "neovim")]
+fn encode_range(text: &str, range: Range<Point>) -> String {
+    let mut byte_range = 0..0;
+    let mut ix = 0;
+    let mut position = Point::zero();
+    for c in text.chars().chain(['\0']) {
+        if position == range.start {
+            byte_range.start = ix;
+        }
+        if position == range.end {
+            byte_range.end = ix;
+        }
+        let len_utf8 = c.len_utf8();
+        ix += len_utf8;
+        if c == '\n' {
+            position.row += 1;
+            position.column = 0;
+        } else {
+            position.column += len_utf8 as u32;
+        }
+    }
+    util::test::generate_marked_text(text, &[byte_range], true)
+}

crates/vim/test_data/neovim_backed_test_context_works.json 🔗

@@ -1 +1,3 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"This is a test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"This is a tesˇt"}}
+{"Get":{"state":"This is a tesˇt","mode":"Normal"}}

crates/vim/test_data/test_a.json 🔗

@@ -1 +1,6 @@
-[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"a"}
+{"Get":{"state":"The quˇick","mode":"Insert"}}
+{"Put":{"state":"The quicˇk"}}
+{"Key":"a"}
+{"Get":{"state":"The quickˇ","mode":"Insert"}}

crates/vim/test_data/test_backspace.json 🔗

@@ -1 +1,9 @@
-[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}

crates/vim/test_data/test_cc.json 🔗

@@ -1 +1,24 @@
-[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The quˇick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_change_0.json 🔗

@@ -1 +1,8 @@
-[{"Text":"uick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"c"}
+{"Key":"0"}
+{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"0"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_change_b.json 🔗

@@ -1 +1,24 @@
-[{"Text":"st Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test1 test3"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Test \ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test \n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test test"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"ˇst Test","mode":"Insert"}}
+{"Put":{"state":"Test ˇtest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"ˇtest","mode":"Insert"}}
+{"Put":{"state":"Test1 test2 ˇtest3"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇtest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test-test ˇtest"}}
+{"Key":"c"}
+{"Key":"shift-b"}
+{"Get":{"state":"Test ˇtest","mode":"Insert"}}

crates/vim/test_data/test_change_backspace.json 🔗

@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"Tˇst","mode":"Insert"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"ˇest","mode":"Insert"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"ˇTest","mode":"Insert"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"Testˇtest","mode":"Insert"}}

crates/vim/test_data/test_change_e.json 🔗

@@ -1 +1,24 @@
-[{"Text":"Te Test"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Teˇ Test","mode":"Insert"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Tˇ test","mode":"Insert"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test tesˇ","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test test\nˇ","mode":"Insert"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"c"}
+{"Key":"shift-e"}
+{"Get":{"state":"Test teˇ test","mode":"Insert"}}

crates/vim/test_data/test_change_end_of_document.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}

crates/vim/test_data/test_change_end_of_line.json 🔗

@@ -1 +1,8 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"c"}
+{"Key":"$"}
+{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"$"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_change_gg.json 🔗

@@ -1 +1,20 @@
-[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}

crates/vim/test_data/test_change_h.json 🔗

@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"Tˇst","mode":"Insert"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"ˇest","mode":"Insert"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"ˇTest","mode":"Insert"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"Test\nˇtest","mode":"Insert"}}

crates/vim/test_data/test_change_j.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\nˇ"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}

crates/vim/test_data/test_change_k.json 🔗

@@ -1 +1,16 @@
-[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}}

crates/vim/test_data/test_change_l.json 🔗

@@ -1 +1,8 @@
-[{"Text":"Tet"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"Tes"},{"Mode":"Insert"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"l"}
+{"Get":{"state":"Teˇt","mode":"Insert"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"c"}
+{"Key":"l"}
+{"Get":{"state":"Tesˇ","mode":"Insert"}}

crates/vim/test_data/test_change_w.json 🔗

@@ -1 +1,28 @@
-[{"Text":"Te"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Teˇ","mode":"Insert"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Tˇ test","mode":"Insert"}}
+{"Put":{"state":"Testˇ  test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Testˇtest","mode":"Insert"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"c"}
+{"Key":"shift-w"}
+{"Get":{"state":"Test teˇ test","mode":"Insert"}}

crates/vim/test_data/test_dd.json 🔗

@@ -1 +1,24 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"brownˇ fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\nbrown ˇfox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\nˇbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_0.json 🔗

@@ -1 +1,8 @@
-[{"Text":"uick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"d"}
+{"Key":"0"}
+{"Get":{"state":"ˇuick\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"0"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_b.json 🔗

@@ -1 +1,24 @@
-[{"Text":"st Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test1 test3"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test \ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test \n\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test test"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"ˇst Test","mode":"Normal"}}
+{"Put":{"state":"Test ˇtest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"ˇtest","mode":"Normal"}}
+{"Put":{"state":"Test1 test2 ˇtest3"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Test1 ˇtest3","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇtest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Testˇ \ntest","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Testˇ \n\ntest","mode":"Normal"}}
+{"Put":{"state":"Test test-test ˇtest"}}
+{"Key":"d"}
+{"Key":"shift-b"}
+{"Get":{"state":"Test ˇtest","mode":"Normal"}}

crates/vim/test_data/test_delete_e.json 🔗

@@ -1 +1,20 @@
-[{"Text":"Te Test"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"T test"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test te test"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Teˇ Test","mode":"Normal"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Tˇ test","mode":"Normal"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Test teˇs","mode":"Normal"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"d"}
+{"Key":"shift-e"}
+{"Get":{"state":"Test teˇ test","mode":"Normal"}}

crates/vim/test_data/test_delete_end_of_document.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The qˇuick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The qˇuick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumpsˇ over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\nˇjumps over","mode":"Normal"}}

crates/vim/test_data/test_delete_end_of_line.json 🔗

@@ -1 +1,8 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_gg.json 🔗

@@ -1 +1,20 @@
-[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"jumpsˇ over\nthe lazy","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"brownˇ fox\njumps over\nthe lazy","mode":"Normal"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇbrown fox\njumps over\nthe lazy","mode":"Normal"}}

crates/vim/test_data/test_delete_h.json 🔗

@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"Tˇst","mode":"Normal"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"ˇTest","mode":"Normal"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"Test\nˇtest","mode":"Normal"}}

crates/vim/test_data/test_delete_j.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quˇick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"jumpsˇ over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\nˇ"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}

crates/vim/test_data/test_delete_k.json 🔗

@@ -1 +1,16 @@
-[{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"jumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"The quˇick","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"ˇbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"ˇbrown fox\njumps over","mode":"Normal"}}

crates/vim/test_data/test_delete_l.json 🔗

@@ -1 +1,16 @@
-[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇs","mode":"Normal"}}
+{"Put":{"state":"Tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇs\ntest","mode":"Normal"}}

crates/vim/test_data/test_delete_left.json 🔗

@@ -1 +1,15 @@
-[{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"ˇTest","mode":"Normal"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Tˇst","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Test\nˇtest","mode":"Normal"}}

crates/vim/test_data/test_delete_to_end_of_line.json 🔗

@@ -1 +1,6 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"shift-d"}
+{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"shift-d"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_w.json 🔗

@@ -1 +1,20 @@
-[{"Text":"Te"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Ttest"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test tetest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Tˇe","mode":"Normal"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Tˇtest","mode":"Normal"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇs\ntest","mode":"Normal"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"d"}
+{"Key":"shift-w"}
+{"Get":{"state":"Test teˇtest","mode":"Normal"}}

crates/vim/test_data/test_end_of_document.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,11],"end":[3,11]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,11],"end":[3,11]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}}
+{"Key":"2"}
+{"Key":"shift-g"}
+{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}}

crates/vim/test_data/test_enter.json 🔗

@@ -1 +1,11 @@
-[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick brown\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick broˇwn\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}

crates/vim/test_data/test_gg.json 🔗

@@ -1 +1,21 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The quicˇk\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}}
+{"Key":"2"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}}

crates/vim/test_data/test_h.json 🔗

@@ -1 +1,9 @@
-[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"h"}
+{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"h"}
+{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"h"}
+{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}}

crates/vim/test_data/test_h_through_unicode.json 🔗

@@ -1 +1,12 @@
-[{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
+{"Put":{"state":"Testˇ├──┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Tesˇt├──┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├ˇ──┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Testˇ├──┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├──ˇ┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Test├─ˇ─┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├──┐ˇTest"}}
+{"Key":"h"}
+{"Get":{"state":"Test├──ˇ┐Test","mode":"Normal"}}

crates/vim/test_data/test_insert_end_of_line.json 🔗

@@ -1 +1,9 @@
-[{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ\nThe quick\nbrown fox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"ˇ\nThe quick\nbrown fox ","mode":"Insert"}}
+{"Put":{"state":"\nThe qˇuick\nbrown fox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"\nThe quickˇ\nbrown fox ","mode":"Insert"}}
+{"Put":{"state":"\nThe quick\nbrown ˇfox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"\nThe quick\nbrown fox ˇ","mode":"Insert"}}

crates/vim/test_data/test_insert_first_non_whitespace.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇThe quick","mode":"Insert"}}
+{"Put":{"state":" The qˇuick"}}
+{"Key":"shift-i"}
+{"Get":{"state":" ˇThe quick","mode":"Insert"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"ˇ\nThe quick"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}}

crates/vim/test_data/test_insert_line_above.json 🔗

@@ -1 +1,18 @@
-[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\n","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\nThe quick\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_j.json 🔗

@@ -1 +1,12 @@
-[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick brown\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox jˇumps","mode":"Normal"}}
+{"Put":{"state":"The quick broˇwn\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox jumpˇs","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nˇfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}

crates/vim/test_data/test_jump_to_end.json 🔗

@@ -1 +1,14 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,16],"end":[3,16]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick\n\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The ˇquick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg","mode":"Normal"}}
+{"Put":{"state":"The quiˇck\n\nbrown"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrowˇn","mode":"Normal"}}
+{"Put":{"state":"The quiˇck\n\n"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nˇ","mode":"Normal"}}

crates/vim/test_data/test_jump_to_first_non_whitespace.json 🔗

@@ -1 +1,18 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"    \nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"^"}
+{"Get":{"state":"ˇThe quick","mode":"Normal"}}
+{"Put":{"state":" The qˇuick"}}
+{"Key":"^"}
+{"Get":{"state":" ˇThe quick","mode":"Normal"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"^"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"^"}
+{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"ˇ\nThe quick"}}
+{"Key":"^"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Normal"}}
+{"Put":{"state":"   ˇ \nThe quick"}}
+{"Key":"^"}
+{"Get":{"state":"   ˇ \nThe quick","mode":"Normal"}}

crates/vim/test_data/test_k.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fˇox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"The quiˇck\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox jumˇps"}}
+{"Key":"k"}
+{"Get":{"state":"The quicˇk\nbrown fox jumps","mode":"Normal"}}

crates/vim/test_data/test_l.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,1],"end":[1,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"Tˇhe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quˇick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quicˇk\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quick\nbˇrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrowˇn"}}
+{"Key":"l"}
+{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}}

crates/vim/test_data/test_neovim.json 🔗

@@ -1 +1,16 @@
-[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Key":"i"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Key":"shift-T"}
+{"Key":"e"}
+{"Key":"s"}
+{"Key":"t"}
+{"Key":" "}
+{"Key":"t"}
+{"Key":"e"}
+{"Key":"s"}
+{"Key":"t"}
+{"Key":"escape"}
+{"Key":"0"}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"ˇtest","mode":"Normal"}}

crates/vim/test_data/test_o.json 🔗

@@ -1 +1,18 @@
-[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"o"}
+{"Get":{"state":"\nˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_p.json 🔗

@@ -1 +1,13 @@
-[{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps overjumps o\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,20],"end":[1,20]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"y"}
+{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}}
+{"Key":"p"}
+{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_visual_change.json 🔗

@@ -1 +1,41 @@
-[{"Text":"The quick "},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps he lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"c"}
+{"Get":{"state":"The quick ˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇazy dog","mode":"Insert"}}

crates/vim/test_data/test_visual_delete.json 🔗

@@ -1 +1,44 @@
-[{"Text":"The quick "},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The ver\nthe lquick brown\nfox jumps oazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"x"}
+{"Get":{"state":"The quickˇ ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"The ver\nthe lˇquick brown\nfox jumps oazy dog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nˇazy dog","mode":"Normal"}}

crates/vim/test_data/test_visual_line_change.json 🔗

@@ -1 +1,35 @@
-[{"Text":"\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Key":"escape"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"\nfox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇ\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nthe lazy dog","mode":"Insert"}}
+{"Key":"escape"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"\nthe lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}}

crates/vim/test_data/test_visual_line_delete.json 🔗

@@ -1 +1,31 @@
-[{"Text":"fox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"fox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"the lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"the laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quˇick brown","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}

crates/vim/test_data/test_x.json 🔗

@@ -1 +1,12 @@
-[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"x"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇs","mode":"Normal"}}
+{"Put":{"state":"Tesˇt\ntest"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇs\ntest","mode":"Normal"}}

crates/workspace/Cargo.toml 🔗

@@ -43,7 +43,7 @@ lazy_static = "1.4"
 env_logger = "0.9.1"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }

crates/zed/Cargo.toml 🔗

@@ -82,7 +82,7 @@ libc = "0.2"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 num_cpus = "1.13.0"
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = "0.8.3"
 regex = "1.5"
 rsa = "0.4"