Merge branch 'main' into randomized-tests-operation-script

Max Brunsfeld created

Change summary

Cargo.lock                                              |  43 
assets/keymaps/default.json                             |  18 
assets/keymaps/vim.json                                 |  52 
assets/settings/default.json                            |   2 
crates/collab/src/tests/integration_tests.rs            |   7 
crates/collab/src/tests/randomized_integration_tests.rs |  88 +
crates/collab_ui/src/contact_list.rs                    |   8 
crates/command_palette/src/command_palette.rs           |   6 
crates/context_menu/src/context_menu.rs                 |   8 
crates/editor/src/editor.rs                             |  54 
crates/editor/src/editor_tests.rs                       |  22 
crates/editor/src/test/editor_test_context.rs           |   4 
crates/fs/src/fs.rs                                     |   6 
crates/gpui/build.rs                                    |   4 
crates/gpui/src/app.rs                                  | 720 ++-------
crates/gpui/src/app/callback_collection.rs              | 185 +
crates/gpui/src/app/test_app_context.rs                 |  10 
crates/gpui/src/gpui.rs                                 |   2 
crates/gpui/src/keymap.rs                               | 757 -----------
crates/gpui/src/keymap_matcher.rs                       | 459 ++++++
crates/gpui/src/keymap_matcher/binding.rs               | 104 +
crates/gpui/src/keymap_matcher/keymap.rs                |  61 
crates/gpui/src/keymap_matcher/keymap_context.rs        | 123 +
crates/gpui/src/keymap_matcher/keystroke.rs             |  97 +
crates/gpui/src/platform.rs                             |   4 
crates/gpui/src/platform/event.rs                       |   2 
crates/gpui/src/platform/mac/event.rs                   |   6 
crates/gpui/src/platform/mac/platform.rs                |  13 
crates/gpui/src/platform/mac/window.rs                  |   2 
crates/gpui/src/platform/test.rs                        |   5 
crates/gpui/src/presenter.rs                            |   2 
crates/gpui_macros/src/gpui_macros.rs                   |   8 
crates/language/src/buffer.rs                           |   1 
crates/language/src/buffer_tests.rs                     |  11 
crates/live_kit_client/build.rs                         |  10 
crates/live_kit_client/examples/test_app.rs             |   2 
crates/media/build.rs                                   |   2 
crates/media/src/media.rs                               |   6 
crates/picker/src/picker.rs                             |   4 
crates/project/src/project.rs                           |  29 
crates/project_panel/src/project_panel.rs               |   5 
crates/recent_projects/src/recent_projects.rs           |   1 
crates/rpc/src/auth.rs                                  |   2 
crates/search/src/buffer_search.rs                      | 166 +
crates/settings/src/keymap_file.rs                      |   2 
crates/snippet/src/snippet.rs                           |   2 
crates/sqlez/src/connection.rs                          |   6 
crates/sqlez/src/statement.rs                           |   8 
crates/sqlez/src/thread_safe_connection.rs              |  13 
crates/sqlez/src/typed_statements.rs                    |  12 
crates/sqlez_macros/src/sqlez_macros.rs                 |   6 
crates/sum_tree/src/tree_map.rs                         |   2 
crates/terminal/src/mappings/keys.rs                    |   4 
crates/terminal/src/terminal.rs                         |   2 
crates/terminal_view/src/terminal_view.rs               |   4 
crates/theme/src/theme.rs                               |   1 
crates/util/src/channel.rs                              |   2 
crates/util/src/lib.rs                                  |   2 
crates/vim/src/motion.rs                                |  91 +
crates/vim/src/normal.rs                                |  42 
crates/vim/src/state.rs                                 |  64 
crates/vim/src/test/neovim_connection.rs                |   2 
crates/vim/src/vim.rs                                   |  11 
crates/vim/test_data/test_capital_f_and_capital_t.json  |   0 
crates/vim/test_data/test_f_and_t.json                  |   0 
crates/workspace/src/pane.rs                            |  84 
crates/workspace/src/workspace.rs                       |  12 
crates/zed/Cargo.toml                                   |   5 
crates/zed/src/languages/ruby.rs                        |   4 
crates/zed/src/languages/typescript.rs                  |   8 
crates/zed/src/main.rs                                  |   4 
crates/zed/src/menus.rs                                 |  19 
crates/zed/src/system_specs.rs                          |  52 
crates/zed/src/zed.rs                                   |  48 
styles/src/styleTree/contactNotification.ts             |   4 
styles/src/styleTree/editor.ts                          |   1 
styles/src/styleTree/search.ts                          |  12 
styles/src/styleTree/tabBar.ts                          |   2 
78 files changed, 2,042 insertions(+), 1,610 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -2759,6 +2759,12 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
+[[package]]
+name = "human_bytes"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb"
+
 [[package]]
 name = "humantime"
 version = "2.1.0"
@@ -3757,6 +3763,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "ntapi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "nu-ansi-term"
 version = "0.46.0"
@@ -4426,7 +4441,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
 dependencies = [
  "libc",
  "log",
- "ntapi",
+ "ntapi 0.3.7",
  "winapi 0.3.9",
 ]
 
@@ -6222,6 +6237,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "sysinfo"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccb297c0afb439440834b4bcf02c5c9da8ec2e808e70f36b0d8e815ff403bd24"
+dependencies = [
+ "cfg-if 1.0.0",
+ "core-foundation-sys",
+ "libc",
+ "ntapi 0.4.0",
+ "once_cell",
+ "rayon",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "system-interface"
 version = "0.20.0"
@@ -7204,6 +7234,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
 [[package]]
 name = "usvg"
 version = "0.14.1"
@@ -8151,7 +8187,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.68.0"
+version = "0.69.0"
 dependencies = [
  "activity_indicator",
  "anyhow",
@@ -8183,6 +8219,7 @@ dependencies = [
  "fuzzy",
  "go_to_line",
  "gpui",
+ "human_bytes",
  "ignore",
  "image",
  "indexmap",
@@ -8216,6 +8253,7 @@ dependencies = [
  "smallvec",
  "smol",
  "sum_tree",
+ "sysinfo",
  "tempdir",
  "terminal_view",
  "text",
@@ -8244,6 +8282,7 @@ dependencies = [
  "tree-sitter-typescript",
  "unindent",
  "url",
+ "urlencoding",
  "util",
  "vim",
  "workspace",

assets/keymaps/default.json πŸ”—

@@ -20,8 +20,10 @@
             "alt-cmd-left": "pane::ActivatePrevItem",
             "alt-cmd-right": "pane::ActivateNextItem",
             "cmd-w": "pane::CloseActiveItem",
-            "cmd-shift-w": "workspace::CloseWindow",
             "alt-cmd-t": "pane::CloseInactiveItems",
+            "cmd-k u": "pane::CloseCleanItems",
+            "cmd-k cmd-w": "pane::CloseAllItems",
+            "cmd-shift-w": "workspace::CloseWindow",
             "cmd-s": "workspace::Save",
             "cmd-shift-s": "workspace::SaveAs",
             "cmd-=": "zed::IncreaseBufferFontSize",
@@ -67,9 +69,11 @@
             "up": "editor::MoveUp",
             "pageup": "editor::PageUp",
             "shift-pageup": "editor::MovePageUp",
+            "home": "editor::MoveToBeginningOfLine",
             "down": "editor::MoveDown",
             "pagedown": "editor::PageDown",
             "shift-pagedown": "editor::MovePageDown",
+            "end": "editor::MoveToEndOfLine",
             "left": "editor::MoveLeft",
             "right": "editor::MoveRight",
             "ctrl-p": "editor::MoveUp",
@@ -110,6 +114,12 @@
                     "stop_at_soft_wraps": true
                 }
             ],
+            "shift-home": [
+                "editor::SelectToBeginningOfLine",
+                {
+                    "stop_at_soft_wraps": true
+                }
+            ],
             "ctrl-shift-a": [
                 "editor::SelectToBeginningOfLine",
                 {
@@ -122,6 +132,12 @@
                     "stop_at_soft_wraps": true
                 }
             ],
+            "shift-end": [
+                "editor::SelectToEndOfLine",
+                {
+                    "stop_at_soft_wraps": true
+                }
+            ],
             "ctrl-shift-e": [
                 "editor::SelectToEndOfLine",
                 {

assets/keymaps/vim.json πŸ”—

@@ -1,6 +1,6 @@
 [
     {
-        "context": "Editor && VimControl",
+        "context": "Editor && VimControl && !VimWaiting",
         "bindings": {
             "g": [
                 "vim::PushOperator",
@@ -53,6 +53,42 @@
                 }
             ],
             "%": "vim::Matching",
+            "ctrl-y": [
+                "vim::Scroll",
+                "LineUp"
+            ],
+            "f": [
+                "vim::PushOperator",
+                {
+                    "FindForward": {
+                        "before": false
+                    }
+                }
+            ],
+            "t": [
+                "vim::PushOperator",
+                {
+                    "FindForward": {
+                        "before": true
+                    }
+                }
+            ],
+            "shift-f": [
+                "vim::PushOperator",
+                {
+                    "FindBackward": {
+                        "after": false
+                    }
+                }
+            ],
+            "shift-t": [
+                "vim::PushOperator",
+                {
+                    "FindBackward": {
+                        "after": true
+                    }
+                }
+            ],
             "escape": "editor::Cancel",
             "0": "vim::StartOfLine", // When no number operator present, use start of line motion
             "1": [
@@ -94,7 +130,7 @@
         }
     },
     {
-        "context": "Editor && vim_mode == normal && vim_operator == none",
+        "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
         "bindings": {
             "c": [
                 "vim::PushOperator",
@@ -173,10 +209,6 @@
             "ctrl-e": [
                 "vim::Scroll",
                 "LineDown"
-            ],
-            "ctrl-y": [
-                "vim::Scroll",
-                "LineUp"
             ]
         }
     },
@@ -255,7 +287,7 @@
         }
     },
     {
-        "context": "Editor && vim_mode == visual",
+        "context": "Editor && vim_mode == visual && !VimWaiting",
         "bindings": {
             "u": "editor::Undo",
             "c": "vim::VisualChange",
@@ -271,5 +303,11 @@
             "escape": "vim::NormalBefore",
             "ctrl-c": "vim::NormalBefore"
         }
+    },
+    {
+        "context": "Editor && VimWaiting",
+        "bindings": {
+            "*": "gpui::KeyPressed"
+        }
     }
 ]

assets/settings/default.json πŸ”—

@@ -221,7 +221,7 @@
         // rust-analyzer
         // typescript-language-server
         // vscode-json-languageserver
-        // "rust_analyzer": {
+        // "rust-analyzer": {
         //     //These initialization options are merged into Zed's defaults
         //     "initialization_options": {
         //         "checkOnSave": {

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -1131,6 +1131,7 @@ async fn test_unshare_project(
         .unwrap();
     let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
     project_b
@@ -1160,6 +1161,7 @@ async fn test_unshare_project(
         .await
         .unwrap();
     let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
+    deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
     project_c2
         .update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
@@ -1213,6 +1215,7 @@ async fn test_host_disconnect(
         .unwrap();
 
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
     let (_, workspace_b) = cx_b.add_window(|cx| {
@@ -1467,7 +1470,7 @@ async fn test_project_reconnect(
         .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
         .await;
     let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
-        assert!(tree.as_local().unwrap().is_shared());
+        assert!(!tree.as_local().unwrap().is_shared());
         tree.id()
     });
     deterministic.run_until_parked();
@@ -1489,6 +1492,7 @@ async fn test_project_reconnect(
     deterministic.run_until_parked();
     project_a1.read_with(cx_a, |project, cx| {
         assert!(project.is_shared());
+        assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
         assert_eq!(
             worktree_a1
                 .read(cx)
@@ -1510,6 +1514,7 @@ async fn test_project_reconnect(
                 "subdir2/i.txt"
             ]
         );
+        assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
         assert_eq!(
             worktree_a3
                 .read(cx)

crates/collab/src/tests/randomized_integration_tests.rs πŸ”—

@@ -15,7 +15,10 @@ use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf1
 use lsp::FakeLanguageServer;
 use parking_lot::Mutex;
 use project::{search::SearchQuery, Project, ProjectPath};
-use rand::prelude::*;
+use rand::{
+    distributions::{Alphanumeric, DistString},
+    prelude::*,
+};
 use serde::{Deserialize, Serialize};
 use std::{
     env,
@@ -293,9 +296,15 @@ async fn test_random_collaboration(
                         );
                     }
                     (None, None) => {}
-                    (None, _) => panic!("host's file is None, guest's isn't "),
-                    (_, None) => panic!("guest's file is None, hosts's isn't "),
+                    (None, _) => panic!("host's file is None, guest's isn't"),
+                    (_, None) => panic!("guest's file is None, hosts's isn't"),
                 }
+
+                let host_diff_base =
+                    host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
+                let guest_diff_base = guest_buffer
+                    .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
+                assert_eq!(guest_diff_base, host_diff_base);
             }
         }
     }
@@ -918,6 +927,37 @@ async fn apply_client_operation(
                     .unwrap();
             }
         }
+
+        ClientOperation::WriteGitIndex {
+            repo_path,
+            contents,
+        } => {
+            if !client
+                .fs
+                .metadata(&repo_path)
+                .await?
+                .map_or(false, |m| m.is_dir)
+            {
+                return Ok(false);
+            }
+
+            log::info!(
+                "{}: writing git index for repo {:?}: {:?}",
+                client.username,
+                repo_path,
+                contents
+            );
+
+            let dot_git_dir = repo_path.join(".git");
+            let contents = contents
+                .iter()
+                .map(|(path, contents)| (path.as_path(), contents.clone()))
+                .collect::<Vec<_>>();
+            if client.fs.metadata(&dot_git_dir).await?.is_none() {
+                client.fs.create_dir(&dot_git_dir).await?;
+            }
+            client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
+        }
     }
     Ok(true)
 }
@@ -1038,6 +1078,10 @@ enum ClientOperation {
         path: PathBuf,
         is_dir: bool,
     },
+    WriteGitIndex {
+        repo_path: PathBuf,
+        contents: Vec<(PathBuf, String)>,
+    },
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1221,6 +1265,7 @@ impl TestPlan {
             return None;
         }
 
+        let executor = cx.background();
         self.operation_ix += 1;
         let call = cx.read(ActiveCall::global);
         Some(loop {
@@ -1337,7 +1382,7 @@ impl TestPlan {
                                 .choose(&mut self.rng)
                                 .cloned() else { continue };
                             let project_root_name = root_name_for_project(&project, cx);
-                            let mut paths = cx.background().block(client.fs.paths());
+                            let mut paths = executor.block(client.fs.paths());
                             paths.remove(0);
                             let new_root_path = if paths.is_empty() || self.rng.gen() {
                                 Path::new("/").join(&self.next_root_dir_name(user_id))
@@ -1385,7 +1430,7 @@ impl TestPlan {
                 },
 
                 // Query and mutate buffers
-                60..=95 => {
+                60..=90 => {
                     let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
                     let project_root_name = root_name_for_project(&project, cx);
                     let is_local = project.read_with(cx, |project, _| project.is_local());
@@ -1505,6 +1550,39 @@ impl TestPlan {
                     }
                 }
 
+                // Update a git index
+                91..=95 => {
+                    let repo_path = executor
+                        .block(client.fs.directories())
+                        .choose(&mut self.rng)
+                        .unwrap()
+                        .clone();
+
+                    let mut file_paths = executor
+                        .block(client.fs.files())
+                        .into_iter()
+                        .filter(|path| path.starts_with(&repo_path))
+                        .collect::<Vec<_>>();
+                    let count = self.rng.gen_range(0..=file_paths.len());
+                    file_paths.shuffle(&mut self.rng);
+                    file_paths.truncate(count);
+
+                    let mut contents = Vec::new();
+                    for abs_child_file_path in &file_paths {
+                        let child_file_path = abs_child_file_path
+                            .strip_prefix(&repo_path)
+                            .unwrap()
+                            .to_path_buf();
+                        let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
+                        contents.push((child_file_path, new_base));
+                    }
+
+                    break ClientOperation::WriteGitIndex {
+                        repo_path,
+                        contents,
+                    };
+                }
+
                 // Create a file or directory
                 96.. => {
                     let is_dir = self.rng.gen::<bool>();

crates/collab_ui/src/contact_list.rs πŸ”—

@@ -8,8 +8,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle,
-    MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
+    impl_actions, impl_internal_actions,
+    keymap_matcher::KeymapContext,
+    AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
+    Subscription, View, ViewContext, ViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::Project;
@@ -1267,7 +1269,7 @@ impl View for ContactList {
         "ContactList"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
         cx.set.insert("menu".into());
         cx

crates/command_palette/src/command_palette.rs πŸ”—

@@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
     elements::{ChildView, Flex, Label, ParentElement},
-    keymap::Keystroke,
+    keymap_matcher::Keystroke,
     Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
     ViewContext, ViewHandle,
 };
@@ -64,8 +64,10 @@ impl CommandPalette {
                     name: humanize_action_name(name),
                     action,
                     keystrokes: bindings
+                        .iter()
+                        .filter_map(|binding| binding.keystrokes())
                         .last()
-                        .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
+                        .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
                 })
             })
             .collect();

crates/context_menu/src/context_menu.rs πŸ”—

@@ -1,7 +1,7 @@
 use gpui::{
-    elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
-    Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
-    SizeConstraint, Subscription, View, ViewContext,
+    elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
+    platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
+    MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
 };
 use menu::*;
 use settings::Settings;
@@ -75,7 +75,7 @@ impl View for ContextMenu {
         "ContextMenu"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
         cx.set.insert("menu".into());
         cx

crates/editor/src/editor.rs πŸ”—

@@ -36,6 +36,7 @@ use gpui::{
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::Vector2F,
     impl_actions, impl_internal_actions,
+    keymap_matcher::KeymapContext,
     platform::CursorStyle,
     serde_json::json,
     AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
@@ -464,7 +465,7 @@ pub struct Editor {
     searchable: bool,
     cursor_shape: CursorShape,
     workspace_id: Option<WorkspaceId>,
-    keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
+    keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
     leader_replica_id: Option<u16>,
     remote_id: Option<ViewId>,
@@ -827,6 +828,23 @@ impl CompletionsMenu {
                 })
                 .collect()
         };
+
+        //Remove all candidates where the query's start does not match the start of any word in the candidate
+        if let Some(query) = query {
+            if let Some(query_start) = query.chars().next() {
+                matches.retain(|string_match| {
+                    split_words(&string_match.string).any(|word| {
+                        //Check that the first codepoint of the word as lowercase matches the first
+                        //codepoint of the query as lowercase
+                        word.chars()
+                            .flat_map(|codepoint| codepoint.to_lowercase())
+                            .zip(query_start.to_lowercase())
+                            .all(|(word_cp, query_cp)| word_cp == query_cp)
+                    })
+                });
+            }
+        }
+
         matches.sort_unstable_by_key(|mat| {
             let completion = &self.completions[mat.candidate_id];
             (
@@ -1225,7 +1243,7 @@ impl Editor {
         }
     }
 
-    pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
+    pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
         self.keymap_context_layers
             .insert(TypeId::of::<Tag>(), context);
     }
@@ -3611,7 +3629,9 @@ impl Editor {
     }
 
     pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
+        dbg!("undo");
         if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
+            dbg!(tx_id);
             if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
                 self.change_selections(None, cx, |s| {
                     s.select_anchors(selections.to_vec());
@@ -6245,7 +6265,7 @@ impl View for Editor {
         false
     }
 
-    fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut context = Self::default_keymap_context();
         let mode = match self.mode {
             EditorMode::SingleLine => "single_line",
@@ -6799,6 +6819,34 @@ pub fn styled_runs_for_code_label<'a>(
         })
 }
 
+pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
+    let mut index = 0;
+    let mut codepoints = text.char_indices().peekable();
+
+    std::iter::from_fn(move || {
+        let start_index = index;
+        while let Some((new_index, codepoint)) = codepoints.next() {
+            index = new_index + codepoint.len_utf8();
+            let current_upper = codepoint.is_uppercase();
+            let next_upper = codepoints
+                .peek()
+                .map(|(_, c)| c.is_uppercase())
+                .unwrap_or(false);
+
+            if !current_upper && next_upper {
+                return Some(&text[start_index..index]);
+            }
+        }
+
+        index = text.len();
+        if start_index < text.len() {
+            return Some(&text[start_index..]);
+        }
+        None
+    })
+    .flat_map(|word| word.split_inclusive('_'))
+}
+
 trait RangeExt<T> {
     fn sorted(&self) -> Range<T>;
     fn to_inclusive(&self) -> RangeInclusive<T>;

crates/editor/src/editor_tests.rs πŸ”—

@@ -29,7 +29,11 @@ use workspace::{
 #[gpui::test]
 fn test_edit_events(cx: &mut MutableAppContext) {
     cx.set_global(Settings::test(cx));
-    let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
+    let buffer = cx.add_model(|cx| {
+        let mut buffer = language::Buffer::new(0, "123456", cx);
+        buffer.set_group_interval(Duration::from_secs(1));
+        buffer
+    });
 
     let events = Rc::new(RefCell::new(Vec::new()));
     let (_, editor1) = cx.add_window(Default::default(), {
@@ -3502,6 +3506,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
             ]
         );
 
+        view.undo(&Undo, cx);
+        view.undo(&Undo, cx);
         view.undo(&Undo, cx);
         assert_eq!(
             view.text(cx),
@@ -5439,6 +5445,20 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
     );
 }
 
+#[test]
+fn test_split_words() {
+    fn split<'a>(text: &'a str) -> Vec<&'a str> {
+        split_words(text).collect()
+    }
+
+    assert_eq!(split("HelloWorld"), &["Hello", "World"]);
+    assert_eq!(split("hello_world"), &["hello_", "world"]);
+    assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
+    assert_eq!(split("Hello_World"), &["Hello_", "World"]);
+    assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
+    assert_eq!(split("helloworld"), &["helloworld"]);
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/editor/src/test/editor_test_context.rs πŸ”—

@@ -9,7 +9,9 @@ use indoc::indoc;
 use crate::{
     display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
 };
-use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
+use gpui::{
+    keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
+};
 use language::{Buffer, BufferSnapshot};
 use settings::Settings;
 use util::{

crates/fs/src/fs.rs πŸ”—

@@ -35,7 +35,7 @@ use repository::FakeGitRepositoryState;
 use std::sync::Weak;
 
 lazy_static! {
-    static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
+    static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -80,13 +80,13 @@ impl LineEnding {
     }
 
     pub fn normalize(text: &mut String) {
-        if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
+        if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
             *text = replaced;
         }
     }
 
     pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
-        if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
+        if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
             replaced.into()
         } else {
             text

crates/gpui/build.rs πŸ”—

@@ -52,7 +52,7 @@ fn compile_metal_shaders() {
     println!("cargo:rerun-if-changed={}", shader_path);
 
     let output = Command::new("xcrun")
-        .args(&[
+        .args([
             "-sdk",
             "macosx",
             "metal",
@@ -76,7 +76,7 @@ fn compile_metal_shaders() {
     }
 
     let output = Command::new("xcrun")
-        .args(&["-sdk", "macosx", "metallib"])
+        .args(["-sdk", "macosx", "metallib"])
         .arg(air_output_path)
         .arg("-o")
         .arg(metallib_output_path)

crates/gpui/src/app.rs πŸ”—

@@ -26,9 +26,8 @@ use smallvec::SmallVec;
 use smol::prelude::*;
 
 pub use action::*;
-use callback_collection::{CallbackCollection, Mapping};
-use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
-use keymap::MatchResult;
+use callback_collection::CallbackCollection;
+use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
 use platform::Event;
 #[cfg(any(test, feature = "test-support"))]
 pub use test_app_context::{ContextHandle, TestAppContext};
@@ -37,7 +36,7 @@ use crate::{
     elements::ElementBox,
     executor::{self, Task},
     geometry::rect::RectF,
-    keymap::{self, Binding, Keystroke},
+    keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
     platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
@@ -72,11 +71,11 @@ pub trait View: Entity + Sized {
         false
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> keymap_matcher::KeymapContext {
         Self::default_keymap_context()
     }
-    fn default_keymap_context() -> keymap::Context {
-        let mut cx = keymap::Context::default();
+    fn default_keymap_context() -> keymap_matcher::KeymapContext {
+        let mut cx = keymap_matcher::KeymapContext::default();
         cx.set.insert(Self::ui_name().into());
         cx
     }
@@ -588,9 +587,9 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
 type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
-type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
 type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
-type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
+type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
+type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
 type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
 type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
@@ -609,24 +608,23 @@ pub struct MutableAppContext {
     capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
-    keystroke_matcher: keymap::Matcher,
+    keystroke_matcher: KeymapMatcher,
     next_entity_id: usize,
     next_window_id: usize,
     next_subscription_id: usize,
     frame_count: usize,
 
-    focus_observations: CallbackCollection<usize, FocusObservationCallback>,
-    global_subscriptions: CallbackCollection<TypeId, GlobalSubscriptionCallback>,
-    global_observations: CallbackCollection<TypeId, GlobalObservationCallback>,
     subscriptions: CallbackCollection<usize, SubscriptionCallback>,
+    global_subscriptions: CallbackCollection<TypeId, GlobalSubscriptionCallback>,
     observations: CallbackCollection<usize, ObservationCallback>,
+    global_observations: CallbackCollection<TypeId, GlobalObservationCallback>,
+    focus_observations: CallbackCollection<usize, FocusObservationCallback>,
+    release_observations: CallbackCollection<usize, ReleaseObservationCallback>,
+    action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>,
     window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
     window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
     keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
 
-    release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
-    action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
-
     #[allow(clippy::type_complexity)]
     presenters_and_platform_windows:
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
@@ -669,7 +667,7 @@ impl MutableAppContext {
             capture_actions: Default::default(),
             actions: Default::default(),
             global_actions: Default::default(),
-            keystroke_matcher: keymap::Matcher::default(),
+            keystroke_matcher: KeymapMatcher::default(),
             next_entity_id: 0,
             next_window_id: 0,
             next_subscription_id: 0,
@@ -1047,12 +1045,10 @@ impl MutableAppContext {
                 callback(payload, cx)
             }),
         });
-
-        Subscription::GlobalSubscription {
-            id: subscription_id,
-            type_id,
-            subscriptions: Some(self.global_subscriptions.downgrade()),
-        }
+        Subscription::GlobalSubscription(
+            self.global_subscriptions
+                .subscribe(type_id, subscription_id),
+        )
     }
 
     pub fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -1089,11 +1085,7 @@ impl MutableAppContext {
                 }
             }),
         });
-        Subscription::Subscription {
-            id: subscription_id,
-            entity_id: handle.id(),
-            subscriptions: Some(self.subscriptions.downgrade()),
-        }
+        Subscription::Subscription(self.subscriptions.subscribe(handle.id(), subscription_id))
     }
 
     fn observe_internal<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -1117,11 +1109,7 @@ impl MutableAppContext {
                 }
             }),
         });
-        Subscription::Observation {
-            id: subscription_id,
-            entity_id,
-            observations: Some(self.observations.downgrade()),
-        }
+        Subscription::Observation(self.observations.subscribe(entity_id, subscription_id))
     }
 
     fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
@@ -1144,12 +1132,7 @@ impl MutableAppContext {
                 }
             }),
         });
-
-        Subscription::FocusObservation {
-            id: subscription_id,
-            view_id,
-            observations: Some(self.focus_observations.downgrade()),
-        }
+        Subscription::FocusObservation(self.focus_observations.subscribe(view_id, subscription_id))
     }
 
     pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
@@ -1165,12 +1148,7 @@ impl MutableAppContext {
             id,
             Box::new(move |cx: &mut MutableAppContext| observe(cx)),
         );
-
-        Subscription::GlobalObservation {
-            id,
-            type_id,
-            observations: Some(self.global_observations.downgrade()),
-        }
+        Subscription::GlobalObservation(self.global_observations.subscribe(type_id, id))
     }
 
     pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
@@ -1192,36 +1170,31 @@ impl MutableAppContext {
         F: 'static + FnOnce(&E, &mut Self),
     {
         let id = post_inc(&mut self.next_subscription_id);
-        self.release_observations
-            .lock()
-            .entry(handle.id())
-            .or_default()
-            .insert(
-                id,
-                Box::new(move |released, cx| {
-                    let released = released.downcast_ref().unwrap();
-                    callback(released, cx)
-                }),
-            );
-        Subscription::ReleaseObservation {
+        let mut callback = Some(callback);
+        self.release_observations.add_callback(
+            handle.id(),
             id,
-            entity_id: handle.id(),
-            observations: Some(Arc::downgrade(&self.release_observations)),
-        }
+            Box::new(move |released, cx| {
+                let released = released.downcast_ref().unwrap();
+                if let Some(callback) = callback.take() {
+                    callback(released, cx)
+                }
+            }),
+        );
+        Subscription::ReleaseObservation(self.release_observations.subscribe(handle.id(), id))
     }
 
     pub fn observe_actions<F>(&mut self, callback: F) -> Subscription
     where
         F: 'static + FnMut(TypeId, &mut MutableAppContext),
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         self.action_dispatch_observations
-            .lock()
-            .insert(id, Box::new(callback));
-        Subscription::ActionObservation {
-            id,
-            observations: Some(Arc::downgrade(&self.action_dispatch_observations)),
-        }
+            .add_callback((), subscription_id, Box::new(callback));
+        Subscription::ActionObservation(
+            self.action_dispatch_observations
+                .subscribe((), subscription_id),
+        )
     }
 
     fn observe_window_activation<F>(&mut self, window_id: usize, callback: F) -> Subscription
@@ -1235,11 +1208,10 @@ impl MutableAppContext {
                 subscription_id,
                 callback: Box::new(callback),
             });
-        Subscription::WindowActivationObservation {
-            id: subscription_id,
-            window_id,
-            observations: Some(self.window_activation_observations.downgrade()),
-        }
+        Subscription::WindowActivationObservation(
+            self.window_activation_observations
+                .subscribe(window_id, subscription_id),
+        )
     }
 
     fn observe_fullscreen<F>(&mut self, window_id: usize, callback: F) -> Subscription
@@ -1253,11 +1225,10 @@ impl MutableAppContext {
                 subscription_id,
                 callback: Box::new(callback),
             });
-        Subscription::WindowFullscreenObservation {
-            id: subscription_id,
-            window_id,
-            observations: Some(self.window_activation_observations.downgrade()),
-        }
+        Subscription::WindowActivationObservation(
+            self.window_activation_observations
+                .subscribe(window_id, subscription_id),
+        )
     }
 
     pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
@@ -1273,12 +1244,10 @@ impl MutableAppContext {
         let subscription_id = post_inc(&mut self.next_subscription_id);
         self.keystroke_observations
             .add_callback(window_id, subscription_id, Box::new(callback));
-
-        Subscription::KeystrokeObservation {
-            id: subscription_id,
-            window_id,
-            observations: Some(self.keystroke_observations.downgrade()),
-        }
+        Subscription::KeystrokeObservation(
+            self.keystroke_observations
+                .subscribe(window_id, subscription_id),
+        )
     }
 
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
@@ -1391,8 +1360,10 @@ impl MutableAppContext {
                 .views
                 .get(&(window_id, *view_id))
                 .expect("view in responder chain does not exist");
-            let cx = view.keymap_context(self.as_ref());
-            let keystrokes = self.keystroke_matcher.keystrokes_for_action(action, &cx);
+            let keymap_context = view.keymap_context(self.as_ref());
+            let keystrokes = self
+                .keystroke_matcher
+                .keystrokes_for_action(action, &keymap_context);
             if keystrokes.is_some() {
                 return keystrokes;
             }
@@ -1473,7 +1444,7 @@ impl MutableAppContext {
         })
     }
 
-    pub fn add_bindings<T: IntoIterator<Item = keymap::Binding>>(&mut self, bindings: T) {
+    pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
         self.keystroke_matcher.add_bindings(bindings);
     }
 
@@ -1999,15 +1970,13 @@ impl MutableAppContext {
                             entity_id,
                             subscription_id,
                             callback,
-                        } => self.subscriptions.add_or_remove_callback(
-                            entity_id,
-                            subscription_id,
-                            callback,
-                        ),
+                        } => self
+                            .subscriptions
+                            .add_callback(entity_id, subscription_id, callback),
 
                         Effect::Event { entity_id, payload } => {
                             let mut subscriptions = self.subscriptions.clone();
-                            subscriptions.emit_and_cleanup(entity_id, self, |callback, this| {
+                            subscriptions.emit(entity_id, self, |callback, this| {
                                 callback(payload.as_ref(), this)
                             })
                         }
@@ -2016,7 +1985,7 @@ impl MutableAppContext {
                             type_id,
                             subscription_id,
                             callback,
-                        } => self.global_subscriptions.add_or_remove_callback(
+                        } => self.global_subscriptions.add_callback(
                             type_id,
                             subscription_id,
                             callback,
@@ -2028,16 +1997,13 @@ impl MutableAppContext {
                             entity_id,
                             subscription_id,
                             callback,
-                        } => self.observations.add_or_remove_callback(
-                            entity_id,
-                            subscription_id,
-                            callback,
-                        ),
+                        } => self
+                            .observations
+                            .add_callback(entity_id, subscription_id, callback),
 
                         Effect::ModelNotification { model_id } => {
                             let mut observations = self.observations.clone();
-                            observations
-                                .emit_and_cleanup(model_id, self, |callback, this| callback(this));
+                            observations.emit(model_id, self, |callback, this| callback(this));
                         }
 
                         Effect::ViewNotification { window_id, view_id } => {
@@ -2046,7 +2012,7 @@ impl MutableAppContext {
 
                         Effect::GlobalNotification { type_id } => {
                             let mut subscriptions = self.global_observations.clone();
-                            subscriptions.emit_and_cleanup(type_id, self, |callback, this| {
+                            subscriptions.emit(type_id, self, |callback, this| {
                                 callback(this);
                                 true
                             });
@@ -2080,7 +2046,7 @@ impl MutableAppContext {
                             subscription_id,
                             callback,
                         } => {
-                            self.focus_observations.add_or_remove_callback(
+                            self.focus_observations.add_callback(
                                 view_id,
                                 subscription_id,
                                 callback,
@@ -2099,7 +2065,7 @@ impl MutableAppContext {
                             window_id,
                             subscription_id,
                             callback,
-                        } => self.window_activation_observations.add_or_remove_callback(
+                        } => self.window_activation_observations.add_callback(
                             window_id,
                             subscription_id,
                             callback,
@@ -2114,7 +2080,7 @@ impl MutableAppContext {
                             window_id,
                             subscription_id,
                             callback,
-                        } => self.window_fullscreen_observations.add_or_remove_callback(
+                        } => self.window_fullscreen_observations.add_callback(
                             window_id,
                             subscription_id,
                             callback,
@@ -2159,6 +2125,7 @@ impl MutableAppContext {
                     self.remove_dropped_entities();
                 } else {
                     self.remove_dropped_entities();
+
                     if refreshing {
                         self.perform_window_refresh();
                     } else {
@@ -2295,7 +2262,7 @@ impl MutableAppContext {
         let type_id = (&*payload).type_id();
 
         let mut subscriptions = self.global_subscriptions.clone();
-        subscriptions.emit_and_cleanup(type_id, self, |callback, this| {
+        subscriptions.emit(type_id, self, |callback, this| {
             callback(payload.as_ref(), this);
             true //Always alive
         });
@@ -2320,17 +2287,18 @@ impl MutableAppContext {
             }
 
             let mut observations = self.observations.clone();
-            observations.emit_and_cleanup(observed_view_id, self, |callback, this| callback(this));
+            observations.emit(observed_view_id, self, |callback, this| callback(this));
         }
     }
 
     fn handle_entity_release_effect(&mut self, entity_id: usize, entity: &dyn Any) {
-        let callbacks = self.release_observations.lock().remove(&entity_id);
-        if let Some(callbacks) = callbacks {
-            for (_, callback) in callbacks {
-                callback(entity, self);
-            }
-        }
+        self.release_observations
+            .clone()
+            .emit(entity_id, self, |callback, this| {
+                callback(entity, this);
+                // Release observations happen one time. So clear the callback by returning false
+                false
+            })
     }
 
     fn handle_fullscreen_effect(&mut self, window_id: usize, is_fullscreen: bool) {
@@ -2350,7 +2318,7 @@ impl MutableAppContext {
             window.is_fullscreen = is_fullscreen;
 
             let mut observations = this.window_fullscreen_observations.clone();
-            observations.emit_and_cleanup(window_id, this, |callback, this| {
+            observations.emit(window_id, this, |callback, this| {
                 callback(is_fullscreen, this)
             });
 
@@ -2367,7 +2335,7 @@ impl MutableAppContext {
     ) {
         self.update(|this| {
             let mut observations = this.keystroke_observations.clone();
-            observations.emit_and_cleanup(window_id, this, {
+            observations.emit(window_id, this, {
                 move |callback, this| callback(&keystroke, &result, handled_by.as_ref(), this)
             });
         });
@@ -2403,7 +2371,7 @@ impl MutableAppContext {
             }
 
             let mut observations = this.window_activation_observations.clone();
-            observations.emit_and_cleanup(window_id, this, |callback, this| callback(active, this));
+            observations.emit(window_id, this, |callback, this| callback(active, this));
 
             Some(())
         });
@@ -2443,8 +2411,7 @@ impl MutableAppContext {
                 }
 
                 let mut subscriptions = this.focus_observations.clone();
-                subscriptions
-                    .emit_and_cleanup(blurred_id, this, |callback, this| callback(false, this));
+                subscriptions.emit(blurred_id, this, |callback, this| callback(false, this));
             }
 
             if let Some(focused_id) = focused_id {
@@ -2456,8 +2423,7 @@ impl MutableAppContext {
                 }
 
                 let mut subscriptions = this.focus_observations.clone();
-                subscriptions
-                    .emit_and_cleanup(focused_id, this, |callback, this| callback(true, this));
+                subscriptions.emit(focused_id, this, |callback, this| callback(true, this));
             }
         })
     }
@@ -2513,11 +2479,12 @@ impl MutableAppContext {
     }
 
     fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) {
-        let mut callbacks = mem::take(&mut *self.action_dispatch_observations.lock());
-        for callback in callbacks.values_mut() {
-            callback(action_id, self);
-        }
-        self.action_dispatch_observations.lock().extend(callbacks);
+        self.action_dispatch_observations
+            .clone()
+            .emit((), self, |callback, this| {
+                callback(action_id, this);
+                true
+            });
     }
 
     fn handle_window_should_close_subscription_effect(
@@ -3173,7 +3140,7 @@ pub trait AnyView {
         window_id: usize,
         view_id: usize,
     ) -> bool;
-    fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
+    fn keymap_context(&self, cx: &AppContext) -> KeymapContext;
     fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
 
     fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String>;
@@ -3315,7 +3282,7 @@ where
         View::modifiers_changed(self, event, &mut cx)
     }
 
-    fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
+    fn keymap_context(&self, cx: &AppContext) -> KeymapContext {
         View::keymap_context(self, cx)
     }
 
@@ -4038,7 +4005,7 @@ pub struct RenderContext<'a, T: View> {
     pub refreshing: bool,
 }
 
-#[derive(Clone, Default)]
+#[derive(Debug, Clone, Default)]
 pub struct MouseState {
     hovered: bool,
     clicked: Option<MouseButton>,
@@ -5106,269 +5073,46 @@ impl<T> Drop for ElementStateHandle<T> {
 
 #[must_use]
 pub enum Subscription {
-    Subscription {
-        id: usize,
-        entity_id: usize,
-        subscriptions: Option<Weak<Mapping<usize, SubscriptionCallback>>>,
-    },
-    GlobalSubscription {
-        id: usize,
-        type_id: TypeId,
-        subscriptions: Option<Weak<Mapping<TypeId, GlobalSubscriptionCallback>>>,
-    },
-    Observation {
-        id: usize,
-        entity_id: usize,
-        observations: Option<Weak<Mapping<usize, ObservationCallback>>>,
-    },
-    GlobalObservation {
-        id: usize,
-        type_id: TypeId,
-        observations: Option<Weak<Mapping<TypeId, GlobalObservationCallback>>>,
-    },
-    FocusObservation {
-        id: usize,
-        view_id: usize,
-        observations: Option<Weak<Mapping<usize, FocusObservationCallback>>>,
-    },
-    WindowActivationObservation {
-        id: usize,
-        window_id: usize,
-        observations: Option<Weak<Mapping<usize, WindowActivationCallback>>>,
-    },
-    WindowFullscreenObservation {
-        id: usize,
-        window_id: usize,
-        observations: Option<Weak<Mapping<usize, WindowFullscreenCallback>>>,
-    },
-    KeystrokeObservation {
-        id: usize,
-        window_id: usize,
-        observations: Option<Weak<Mapping<usize, KeystrokeCallback>>>,
-    },
-
-    ReleaseObservation {
-        id: usize,
-        entity_id: usize,
-        #[allow(clippy::type_complexity)]
-        observations:
-            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>>,
-    },
-    ActionObservation {
-        id: usize,
-        observations: Option<Weak<Mutex<BTreeMap<usize, ActionObservationCallback>>>>,
-    },
+    Subscription(callback_collection::Subscription<usize, SubscriptionCallback>),
+    Observation(callback_collection::Subscription<usize, ObservationCallback>),
+    GlobalSubscription(callback_collection::Subscription<TypeId, GlobalSubscriptionCallback>),
+    GlobalObservation(callback_collection::Subscription<TypeId, GlobalObservationCallback>),
+    FocusObservation(callback_collection::Subscription<usize, FocusObservationCallback>),
+    WindowActivationObservation(callback_collection::Subscription<usize, WindowActivationCallback>),
+    WindowFullscreenObservation(callback_collection::Subscription<usize, WindowFullscreenCallback>),
+    KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>),
+    ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>),
+    ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>),
 }
 
 impl Subscription {
-    pub fn detach(&mut self) {
+    pub fn id(&self) -> usize {
         match self {
-            Subscription::Subscription { subscriptions, .. } => {
-                subscriptions.take();
-            }
-            Subscription::GlobalSubscription { subscriptions, .. } => {
-                subscriptions.take();
-            }
-            Subscription::Observation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::GlobalObservation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::ReleaseObservation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::FocusObservation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::ActionObservation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::KeystrokeObservation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::WindowActivationObservation { observations, .. } => {
-                observations.take();
-            }
-            Subscription::WindowFullscreenObservation { observations, .. } => {
-                observations.take();
-            }
+            Subscription::Subscription(subscription) => subscription.id(),
+            Subscription::Observation(subscription) => subscription.id(),
+            Subscription::GlobalSubscription(subscription) => subscription.id(),
+            Subscription::GlobalObservation(subscription) => subscription.id(),
+            Subscription::FocusObservation(subscription) => subscription.id(),
+            Subscription::WindowActivationObservation(subscription) => subscription.id(),
+            Subscription::WindowFullscreenObservation(subscription) => subscription.id(),
+            Subscription::KeystrokeObservation(subscription) => subscription.id(),
+            Subscription::ReleaseObservation(subscription) => subscription.id(),
+            Subscription::ActionObservation(subscription) => subscription.id(),
         }
     }
-}
 
-impl Drop for Subscription {
-    fn drop(&mut self) {
+    pub fn detach(&mut self) {
         match self {
-            Subscription::Subscription {
-                id,
-                entity_id,
-                subscriptions,
-            } => {
-                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
-                    match subscriptions
-                        .lock()
-                        .entry(*entity_id)
-                        .or_default()
-                        .entry(*id)
-                    {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::GlobalSubscription {
-                id,
-                type_id,
-                subscriptions,
-            } => {
-                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
-                    match subscriptions.lock().entry(*type_id).or_default().entry(*id) {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::Observation {
-                id,
-                entity_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    match observations
-                        .lock()
-                        .entry(*entity_id)
-                        .or_default()
-                        .entry(*id)
-                    {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::GlobalObservation {
-                id,
-                type_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    match observations.lock().entry(*type_id).or_default().entry(*id) {
-                        collections::btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        collections::btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::ReleaseObservation {
-                id,
-                entity_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    if let Some(observations) = observations.lock().get_mut(entity_id) {
-                        observations.remove(id);
-                    }
-                }
-            }
-            Subscription::FocusObservation {
-                id,
-                view_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    match observations.lock().entry(*view_id).or_default().entry(*id) {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::ActionObservation { id, observations } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    observations.lock().remove(id);
-                }
-            }
-            Subscription::KeystrokeObservation {
-                id,
-                window_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    match observations
-                        .lock()
-                        .entry(*window_id)
-                        .or_default()
-                        .entry(*id)
-                    {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::WindowActivationObservation {
-                id,
-                window_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    match observations
-                        .lock()
-                        .entry(*window_id)
-                        .or_default()
-                        .entry(*id)
-                    {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-            Subscription::WindowFullscreenObservation {
-                id,
-                window_id,
-                observations,
-            } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    match observations
-                        .lock()
-                        .entry(*window_id)
-                        .or_default()
-                        .entry(*id)
-                    {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(None);
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
+            Subscription::Subscription(subscription) => subscription.detach(),
+            Subscription::GlobalSubscription(subscription) => subscription.detach(),
+            Subscription::Observation(subscription) => subscription.detach(),
+            Subscription::GlobalObservation(subscription) => subscription.detach(),
+            Subscription::FocusObservation(subscription) => subscription.detach(),
+            Subscription::KeystrokeObservation(subscription) => subscription.detach(),
+            Subscription::WindowActivationObservation(subscription) => subscription.detach(),
+            Subscription::WindowFullscreenObservation(subscription) => subscription.detach(),
+            Subscription::ReleaseObservation(subscription) => subscription.detach(),
+            Subscription::ActionObservation(subscription) => subscription.detach(),
         }
     }
 }
@@ -6015,60 +5759,44 @@ mod tests {
 
     #[crate::test(self)]
     fn test_view_events(cx: &mut MutableAppContext) {
-        #[derive(Default)]
-        struct View {
-            events: Vec<usize>,
-        }
-
-        impl Entity for View {
-            type Event = usize;
-        }
-
-        impl super::View for View {
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-                Empty::new().boxed()
-            }
-
-            fn ui_name() -> &'static str {
-                "View"
-            }
-        }
-
         struct Model;
 
         impl Entity for Model {
-            type Event = usize;
+            type Event = String;
         }
 
-        let (_, handle_1) = cx.add_window(Default::default(), |_| View::default());
-        let handle_2 = cx.add_view(&handle_1, |_| View::default());
+        let (_, handle_1) = cx.add_window(Default::default(), |_| TestView::default());
+        let handle_2 = cx.add_view(&handle_1, |_| TestView::default());
         let handle_3 = cx.add_model(|_| Model);
 
         handle_1.update(cx, |_, cx| {
             cx.subscribe(&handle_2, move |me, emitter, event, cx| {
-                me.events.push(*event);
+                me.events.push(event.clone());
 
                 cx.subscribe(&emitter, |me, _, event, _| {
-                    me.events.push(*event * 2);
+                    me.events.push(format!("{event} from inner"));
                 })
                 .detach();
             })
             .detach();
 
             cx.subscribe(&handle_3, |me, _, event, _| {
-                me.events.push(*event);
+                me.events.push(event.clone());
             })
             .detach();
         });
 
-        handle_2.update(cx, |_, c| c.emit(7));
-        assert_eq!(handle_1.read(cx).events, vec![7]);
+        handle_2.update(cx, |_, c| c.emit("7".into()));
+        assert_eq!(handle_1.read(cx).events, vec!["7"]);
 
-        handle_2.update(cx, |_, c| c.emit(5));
-        assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]);
+        handle_2.update(cx, |_, c| c.emit("5".into()));
+        assert_eq!(handle_1.read(cx).events, vec!["7", "5", "5 from inner"]);
 
-        handle_3.update(cx, |_, c| c.emit(9));
-        assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]);
+        handle_3.update(cx, |_, c| c.emit("9".into()));
+        assert_eq!(
+            handle_1.read(cx).events,
+            vec!["7", "5", "5 from inner", "9"]
+        );
     }
 
     #[crate::test(self)]
@@ -6259,31 +5987,15 @@ mod tests {
 
     #[crate::test(self)]
     fn test_dropping_subscribers(cx: &mut MutableAppContext) {
-        struct View;
-
-        impl Entity for View {
-            type Event = ();
-        }
-
-        impl super::View for View {
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-                Empty::new().boxed()
-            }
-
-            fn ui_name() -> &'static str {
-                "View"
-            }
-        }
-
         struct Model;
 
         impl Entity for Model {
             type Event = ();
         }
 
-        let (_, root_view) = cx.add_window(Default::default(), |_| View);
-        let observing_view = cx.add_view(&root_view, |_| View);
-        let emitting_view = cx.add_view(&root_view, |_| View);
+        let (_, root_view) = cx.add_window(Default::default(), |_| TestView::default());
+        let observing_view = cx.add_view(&root_view, |_| TestView::default());
+        let emitting_view = cx.add_view(&root_view, |_| TestView::default());
         let observing_model = cx.add_model(|_| Model);
         let observed_model = cx.add_model(|_| Model);
 
@@ -6300,165 +6012,117 @@ mod tests {
             drop(observing_model);
         });
 
-        emitting_view.update(cx, |_, cx| cx.emit(()));
+        emitting_view.update(cx, |_, cx| cx.emit(Default::default()));
         observed_model.update(cx, |_, cx| cx.emit(()));
     }
 
     #[crate::test(self)]
     fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) {
-        #[derive(Default)]
-        struct TestView;
-
-        impl Entity for TestView {
-            type Event = ();
-        }
-
-        impl View for TestView {
-            fn ui_name() -> &'static str {
-                "TestView"
-            }
-
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-                Empty::new().boxed()
-            }
-        }
-
-        let events = Rc::new(RefCell::new(Vec::new()));
-        cx.add_window(Default::default(), |cx| {
+        let (_, view) = cx.add_window::<TestView, _>(Default::default(), |cx| {
             drop(cx.subscribe(&cx.handle(), {
-                let events = events.clone();
-                move |_, _, _, _| events.borrow_mut().push("dropped before flush")
+                move |this, _, _, _| this.events.push("dropped before flush".into())
             }));
             cx.subscribe(&cx.handle(), {
-                let events = events.clone();
-                move |_, _, _, _| events.borrow_mut().push("before emit")
+                move |this, _, _, _| this.events.push("before emit".into())
             })
             .detach();
-            cx.emit(());
+            cx.emit("the event".into());
             cx.subscribe(&cx.handle(), {
-                let events = events.clone();
-                move |_, _, _, _| events.borrow_mut().push("after emit")
+                move |this, _, _, _| this.events.push("after emit".into())
             })
             .detach();
-            TestView
+            TestView { events: Vec::new() }
         });
-        assert_eq!(*events.borrow(), ["before emit"]);
+
+        assert_eq!(view.read(cx).events, ["before emit"]);
     }
 
     #[crate::test(self)]
     fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) {
-        #[derive(Default)]
-        struct View {
-            events: Vec<usize>,
-        }
-
-        impl Entity for View {
-            type Event = usize;
-        }
-
-        impl super::View for View {
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-                Empty::new().boxed()
-            }
-
-            fn ui_name() -> &'static str {
-                "View"
-            }
-        }
-
         #[derive(Default)]
         struct Model {
-            count: usize,
+            state: String,
         }
 
         impl Entity for Model {
             type Event = ();
         }
 
-        let (_, view) = cx.add_window(Default::default(), |_| View::default());
-        let model = cx.add_model(|_| Model::default());
+        let (_, view) = cx.add_window(Default::default(), |_| TestView::default());
+        let model = cx.add_model(|_| Model {
+            state: "old-state".into(),
+        });
 
         view.update(cx, |_, c| {
             c.observe(&model, |me, observed, c| {
-                me.events.push(observed.read(c).count)
+                me.events.push(observed.read(c).state.clone())
             })
             .detach();
         });
 
-        model.update(cx, |model, c| {
-            model.count = 11;
-            c.notify();
+        model.update(cx, |model, cx| {
+            model.state = "new-state".into();
+            cx.notify();
         });
-        assert_eq!(view.read(cx).events, vec![11]);
+        assert_eq!(view.read(cx).events, vec!["new-state"]);
     }
 
     #[crate::test(self)]
     fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) {
-        #[derive(Default)]
-        struct TestView;
-
-        impl Entity for TestView {
-            type Event = ();
-        }
-
-        impl View for TestView {
-            fn ui_name() -> &'static str {
-                "TestView"
-            }
-
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-                Empty::new().boxed()
-            }
-        }
-
-        let events = Rc::new(RefCell::new(Vec::new()));
-        cx.add_window(Default::default(), |cx| {
+        let (_, view) = cx.add_window::<TestView, _>(Default::default(), |cx| {
             drop(cx.observe(&cx.handle(), {
-                let events = events.clone();
-                move |_, _, _| events.borrow_mut().push("dropped before flush")
+                move |this, _, _| this.events.push("dropped before flush".into())
             }));
             cx.observe(&cx.handle(), {
-                let events = events.clone();
-                move |_, _, _| events.borrow_mut().push("before notify")
+                move |this, _, _| this.events.push("before notify".into())
             })
             .detach();
             cx.notify();
             cx.observe(&cx.handle(), {
-                let events = events.clone();
-                move |_, _, _| events.borrow_mut().push("after notify")
+                move |this, _, _| this.events.push("after notify".into())
             })
             .detach();
-            TestView
+            TestView { events: Vec::new() }
         });
-        assert_eq!(*events.borrow(), ["before notify"]);
+
+        assert_eq!(view.read(cx).events, ["before notify"]);
     }
 
     #[crate::test(self)]
-    fn test_dropping_observers(cx: &mut MutableAppContext) {
-        struct View;
-
-        impl Entity for View {
+    fn test_notify_and_drop_observe_subscription_in_same_update_cycle(cx: &mut MutableAppContext) {
+        struct Model;
+        impl Entity for Model {
             type Event = ();
         }
 
-        impl super::View for View {
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-                Empty::new().boxed()
-            }
+        let model = cx.add_model(|_| Model);
+        let (_, view) = cx.add_window(Default::default(), |_| TestView::default());
 
-            fn ui_name() -> &'static str {
-                "View"
-            }
+        view.update(cx, |_, cx| {
+            model.update(cx, |_, cx| cx.notify());
+            drop(cx.observe(&model, move |this, _, _| {
+                this.events.push("model notified".into());
+            }));
+            model.update(cx, |_, cx| cx.notify());
+        });
+
+        for _ in 0..3 {
+            model.update(cx, |_, cx| cx.notify());
         }
 
+        assert_eq!(view.read(cx).events, Vec::<String>::new());
+    }
+
+    #[crate::test(self)]
+    fn test_dropping_observers(cx: &mut MutableAppContext) {
         struct Model;
 
         impl Entity for Model {
             type Event = ();
         }
 
-        let (_, root_view) = cx.add_window(Default::default(), |_| View);
-        let observing_view = cx.add_view(root_view, |_| View);
+        let (_, root_view) = cx.add_window(Default::default(), |_| TestView::default());
+        let observing_view = cx.add_view(root_view, |_| TestView::default());
         let observing_model = cx.add_model(|_| Model);
         let observed_model = cx.add_model(|_| Model);
 

crates/gpui/src/app/callback_collection.rs πŸ”—

@@ -1,19 +1,44 @@
+use crate::MutableAppContext;
+use collections::{BTreeMap, HashMap, HashSet};
+use parking_lot::Mutex;
 use std::sync::Arc;
 use std::{hash::Hash, sync::Weak};
 
-use parking_lot::Mutex;
+pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
+    internal: Arc<Mutex<Mapping<K, F>>>,
+}
 
-use collections::{btree_map, BTreeMap, HashMap};
+pub struct Subscription<K: Clone + Hash + Eq, F> {
+    key: K,
+    id: usize,
+    mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
+}
 
-use crate::MutableAppContext;
+struct Mapping<K, F> {
+    callbacks: HashMap<K, BTreeMap<usize, F>>,
+    dropped_subscriptions: HashMap<K, HashSet<usize>>,
+}
 
-pub type Mapping<K, F> = Mutex<HashMap<K, BTreeMap<usize, Option<F>>>>;
+impl<K: Hash + Eq, F> Mapping<K, F> {
+    fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
+        if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
+            subscriptions.remove(&subscription_id)
+        } else {
+            false
+        }
+    }
+}
 
-pub struct CallbackCollection<K: Hash + Eq, F> {
-    internal: Arc<Mapping<K, F>>,
+impl<K, F> Default for Mapping<K, F> {
+    fn default() -> Self {
+        Self {
+            callbacks: Default::default(),
+            dropped_subscriptions: Default::default(),
+        }
+    }
 }
 
-impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
+impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
     fn clone(&self) -> Self {
         Self {
             internal: self.internal.clone(),
@@ -21,7 +46,7 @@ impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
     }
 }
 
-impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
+impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
     fn default() -> Self {
         CallbackCollection {
             internal: Arc::new(Mutex::new(Default::default())),
@@ -29,78 +54,114 @@ impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
     }
 }
 
-impl<K: Hash + Eq + Copy, F> CallbackCollection<K, F> {
-    pub fn downgrade(&self) -> Weak<Mapping<K, F>> {
-        Arc::downgrade(&self.internal)
-    }
-
+impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
     #[cfg(test)]
     pub fn is_empty(&self) -> bool {
-        self.internal.lock().is_empty()
+        self.internal.lock().callbacks.is_empty()
     }
 
-    pub fn add_callback(&mut self, id: K, subscription_id: usize, callback: F) {
-        self.internal
-            .lock()
-            .entry(id)
-            .or_default()
-            .insert(subscription_id, Some(callback));
+    pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
+        Subscription {
+            key,
+            id: subscription_id,
+            mapping: Some(Arc::downgrade(&self.internal)),
+        }
     }
 
-    pub fn remove(&mut self, id: K) {
-        self.internal.lock().remove(&id);
-    }
+    pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
+        let mut this = self.internal.lock();
+
+        // If this callback's subscription was dropped before the callback was
+        // added, then just drop the callback.
+        if this.clear_dropped_state(&key, subscription_id) {
+            return;
+        }
 
-    pub fn add_or_remove_callback(&mut self, id: K, subscription_id: usize, callback: F) {
-        match self
-            .internal
-            .lock()
-            .entry(id)
+        this.callbacks
+            .entry(key)
             .or_default()
-            .entry(subscription_id)
-        {
-            btree_map::Entry::Vacant(entry) => {
-                entry.insert(Some(callback));
-            }
+            .insert(subscription_id, callback);
+    }
 
-            btree_map::Entry::Occupied(entry) => {
-                // TODO: This seems like it should never be called because no code
-                // should ever attempt to remove an existing callback
-                debug_assert!(entry.get().is_none());
-                entry.remove();
-            }
-        }
+    pub fn remove(&mut self, key: K) {
+        // Drop these callbacks after releasing the lock, in case one of them
+        // owns a subscription to this callback collection.
+        let mut this = self.internal.lock();
+        let callbacks = this.callbacks.remove(&key);
+        this.dropped_subscriptions.remove(&key);
+        drop(this);
+        drop(callbacks);
     }
 
-    pub fn emit_and_cleanup<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
+    pub fn emit<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
         &mut self,
-        id: K,
+        key: K,
         cx: &mut MutableAppContext,
         mut call_callback: C,
     ) {
-        let callbacks = self.internal.lock().remove(&id);
+        let callbacks = self.internal.lock().callbacks.remove(&key);
         if let Some(callbacks) = callbacks {
-            for (subscription_id, callback) in callbacks {
-                if let Some(mut callback) = callback {
-                    let alive = call_callback(&mut callback, cx);
-                    if alive {
-                        match self
-                            .internal
-                            .lock()
-                            .entry(id)
-                            .or_default()
-                            .entry(subscription_id)
-                        {
-                            btree_map::Entry::Vacant(entry) => {
-                                entry.insert(Some(callback));
-                            }
-                            btree_map::Entry::Occupied(entry) => {
-                                entry.remove();
-                            }
-                        }
-                    }
+            for (subscription_id, mut callback) in callbacks {
+                // If this callback's subscription was dropped while invoking an
+                // earlier callback, then just drop the callback.
+                let mut this = self.internal.lock();
+                if this.clear_dropped_state(&key, subscription_id) {
+                    continue;
                 }
+
+                drop(this);
+                let alive = call_callback(&mut callback, cx);
+
+                // If this callback's subscription was dropped while invoking the callback
+                // itself, or if the callback returns false, then just drop the callback.
+                let mut this = self.internal.lock();
+                if this.clear_dropped_state(&key, subscription_id) || !alive {
+                    continue;
+                }
+
+                this.callbacks
+                    .entry(key)
+                    .or_default()
+                    .insert(subscription_id, callback);
             }
         }
     }
 }
+
+impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
+    pub fn id(&self) -> usize {
+        self.id
+    }
+
+    pub fn detach(&mut self) {
+        self.mapping.take();
+    }
+}
+
+impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
+    fn drop(&mut self) {
+        if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
+            let mut mapping = mapping.lock();
+
+            // If the callback is present in the mapping, then just remove it.
+            if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
+                let callback = callbacks.remove(&self.id);
+                if callback.is_some() {
+                    drop(mapping);
+                    drop(callback);
+                    return;
+                }
+            }
+
+            // If this subscription's callback is not present, then either it has been
+            // temporarily removed during emit, or it has not yet been added. Record
+            // that this subscription has been dropped so that the callback can be
+            // removed later.
+            mapping
+                .dropped_subscriptions
+                .entry(self.key.clone())
+                .or_default()
+                .insert(self.id);
+        }
+    }
+}

crates/gpui/src/app/test_app_context.rs πŸ”—

@@ -17,11 +17,11 @@ use parking_lot::{Mutex, RwLock};
 use smol::stream::StreamExt;
 
 use crate::{
-    executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
-    AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
-    ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
-    RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
-    WindowInputHandler,
+    executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
+    AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
+    LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
+    ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
+    WeakHandle, WindowInputHandler,
 };
 use collections::BTreeMap;
 

crates/gpui/src/gpui.rs πŸ”—

@@ -25,7 +25,7 @@ pub mod executor;
 pub use executor::Task;
 pub mod color;
 pub mod json;
-pub mod keymap;
+pub mod keymap_matcher;
 pub mod platform;
 pub use gpui_macros::test;
 pub use platform::*;

crates/gpui/src/keymap.rs πŸ”—

@@ -1,757 +0,0 @@
-use crate::Action;
-use anyhow::{anyhow, Result};
-use smallvec::SmallVec;
-use std::{
-    any::{Any, TypeId},
-    collections::{HashMap, HashSet},
-    fmt::{Debug, Write},
-};
-use tree_sitter::{Language, Node, Parser};
-
-extern "C" {
-    fn tree_sitter_context_predicate() -> Language;
-}
-
-pub struct Matcher {
-    pending_views: HashMap<usize, Context>,
-    pending_keystrokes: Vec<Keystroke>,
-    keymap: Keymap,
-}
-
-#[derive(Default)]
-pub struct Keymap {
-    bindings: Vec<Binding>,
-    binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
-}
-
-pub struct Binding {
-    keystrokes: SmallVec<[Keystroke; 2]>,
-    action: Box<dyn Action>,
-    context_predicate: Option<ContextPredicate>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Keystroke {
-    pub ctrl: bool,
-    pub alt: bool,
-    pub shift: bool,
-    pub cmd: bool,
-    pub function: bool,
-    pub key: String,
-}
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct Context {
-    pub set: HashSet<String>,
-    pub map: HashMap<String, String>,
-}
-
-#[derive(Debug, Eq, PartialEq)]
-enum ContextPredicate {
-    Identifier(String),
-    Equal(String, String),
-    NotEqual(String, String),
-    Not(Box<ContextPredicate>),
-    And(Box<ContextPredicate>, Box<ContextPredicate>),
-    Or(Box<ContextPredicate>, Box<ContextPredicate>),
-}
-
-trait ActionArg {
-    fn boxed_clone(&self) -> Box<dyn Any>;
-}
-
-impl<T> ActionArg for T
-where
-    T: 'static + Any + Clone,
-{
-    fn boxed_clone(&self) -> Box<dyn Any> {
-        Box::new(self.clone())
-    }
-}
-
-pub enum MatchResult {
-    None,
-    Pending,
-    Matches(Vec<(usize, Box<dyn Action>)>),
-}
-
-impl Debug for MatchResult {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            MatchResult::None => f.debug_struct("MatchResult::None").finish(),
-            MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
-            MatchResult::Matches(matches) => f
-                .debug_list()
-                .entries(
-                    matches
-                        .iter()
-                        .map(|(view_id, action)| format!("{view_id}, {}", action.name())),
-                )
-                .finish(),
-        }
-    }
-}
-
-impl PartialEq for MatchResult {
-    fn eq(&self, other: &Self) -> bool {
-        match (self, other) {
-            (MatchResult::None, MatchResult::None) => true,
-            (MatchResult::Pending, MatchResult::Pending) => true,
-            (MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
-                matches.len() == other_matches.len()
-                    && matches.iter().zip(other_matches.iter()).all(
-                        |((view_id, action), (other_view_id, other_action))| {
-                            view_id == other_view_id && action.eq(other_action.as_ref())
-                        },
-                    )
-            }
-            _ => false,
-        }
-    }
-}
-
-impl Eq for MatchResult {}
-
-impl Clone for MatchResult {
-    fn clone(&self) -> Self {
-        match self {
-            MatchResult::None => MatchResult::None,
-            MatchResult::Pending => MatchResult::Pending,
-            MatchResult::Matches(matches) => MatchResult::Matches(
-                matches
-                    .iter()
-                    .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
-                    .collect(),
-            ),
-        }
-    }
-}
-
-impl Matcher {
-    pub fn new(keymap: Keymap) -> Self {
-        Self {
-            pending_views: HashMap::new(),
-            pending_keystrokes: Vec::new(),
-            keymap,
-        }
-    }
-
-    pub fn set_keymap(&mut self, keymap: Keymap) {
-        self.clear_pending();
-        self.keymap = keymap;
-    }
-
-    pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
-        self.clear_pending();
-        self.keymap.add_bindings(bindings);
-    }
-
-    pub fn clear_bindings(&mut self) {
-        self.clear_pending();
-        self.keymap.clear();
-    }
-
-    pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
-        self.keymap.bindings_for_action_type(action_type)
-    }
-
-    pub fn clear_pending(&mut self) {
-        self.pending_keystrokes.clear();
-        self.pending_views.clear();
-    }
-
-    pub fn has_pending_keystrokes(&self) -> bool {
-        !self.pending_keystrokes.is_empty()
-    }
-
-    pub fn push_keystroke(
-        &mut self,
-        keystroke: Keystroke,
-        dispatch_path: Vec<(usize, Context)>,
-    ) -> MatchResult {
-        let mut any_pending = false;
-        let mut matched_bindings = Vec::new();
-
-        let first_keystroke = self.pending_keystrokes.is_empty();
-        self.pending_keystrokes.push(keystroke);
-
-        for (view_id, context) in dispatch_path {
-            // Don't require pending view entry if there are no pending keystrokes
-            if !first_keystroke && !self.pending_views.contains_key(&view_id) {
-                continue;
-            }
-
-            // If there is a previous view context, invalidate that view if it
-            // has changed
-            if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
-                if previous_view_context != context {
-                    continue;
-                }
-            }
-
-            // Find the bindings which map the pending keystrokes and current context
-            for binding in self.keymap.bindings.iter().rev() {
-                if binding.keystrokes.starts_with(&self.pending_keystrokes)
-                    && binding
-                        .context_predicate
-                        .as_ref()
-                        .map(|c| c.eval(&context))
-                        .unwrap_or(true)
-                {
-                    // If the binding is completed, push it onto the matches list
-                    if binding.keystrokes.len() == self.pending_keystrokes.len() {
-                        matched_bindings.push((view_id, binding.action.boxed_clone()));
-                    } else {
-                        // Otherwise, the binding is still pending
-                        self.pending_views.insert(view_id, context.clone());
-                        any_pending = true;
-                    }
-                }
-            }
-        }
-
-        if !any_pending {
-            self.clear_pending();
-        }
-
-        if !matched_bindings.is_empty() {
-            MatchResult::Matches(matched_bindings)
-        } else if any_pending {
-            MatchResult::Pending
-        } else {
-            MatchResult::None
-        }
-    }
-
-    pub fn keystrokes_for_action(
-        &self,
-        action: &dyn Action,
-        cx: &Context,
-    ) -> Option<SmallVec<[Keystroke; 2]>> {
-        for binding in self.keymap.bindings.iter().rev() {
-            if binding.action.eq(action)
-                && binding
-                    .context_predicate
-                    .as_ref()
-                    .map_or(true, |predicate| predicate.eval(cx))
-            {
-                return Some(binding.keystrokes.clone());
-            }
-        }
-        None
-    }
-}
-
-impl Default for Matcher {
-    fn default() -> Self {
-        Self::new(Keymap::default())
-    }
-}
-
-impl Keymap {
-    pub fn new(bindings: Vec<Binding>) -> Self {
-        let mut binding_indices_by_action_type = HashMap::new();
-        for (ix, binding) in bindings.iter().enumerate() {
-            binding_indices_by_action_type
-                .entry(binding.action.as_any().type_id())
-                .or_insert_with(SmallVec::new)
-                .push(ix);
-        }
-        Self {
-            binding_indices_by_action_type,
-            bindings,
-        }
-    }
-
-    fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &'_ Binding> {
-        self.binding_indices_by_action_type
-            .get(&action_type)
-            .map(SmallVec::as_slice)
-            .unwrap_or(&[])
-            .iter()
-            .map(|ix| &self.bindings[*ix])
-    }
-
-    fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
-        for binding in bindings {
-            self.binding_indices_by_action_type
-                .entry(binding.action.as_any().type_id())
-                .or_default()
-                .push(self.bindings.len());
-            self.bindings.push(binding);
-        }
-    }
-
-    fn clear(&mut self) {
-        self.bindings.clear();
-        self.binding_indices_by_action_type.clear();
-    }
-}
-
-impl Binding {
-    pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
-        Self::load(keystrokes, Box::new(action), context).unwrap()
-    }
-
-    pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
-        let context = if let Some(context) = context {
-            Some(ContextPredicate::parse(context)?)
-        } else {
-            None
-        };
-
-        let keystrokes = keystrokes
-            .split_whitespace()
-            .map(Keystroke::parse)
-            .collect::<Result<_>>()?;
-
-        Ok(Self {
-            keystrokes,
-            action,
-            context_predicate: context,
-        })
-    }
-
-    pub fn keystrokes(&self) -> &[Keystroke] {
-        &self.keystrokes
-    }
-
-    pub fn action(&self) -> &dyn Action {
-        self.action.as_ref()
-    }
-}
-
-impl Keystroke {
-    pub fn parse(source: &str) -> anyhow::Result<Self> {
-        let mut ctrl = false;
-        let mut alt = false;
-        let mut shift = false;
-        let mut cmd = false;
-        let mut function = false;
-        let mut key = None;
-
-        let mut components = source.split('-').peekable();
-        while let Some(component) = components.next() {
-            match component {
-                "ctrl" => ctrl = true,
-                "alt" => alt = true,
-                "shift" => shift = true,
-                "cmd" => cmd = true,
-                "fn" => function = true,
-                _ => {
-                    if let Some(component) = components.peek() {
-                        if component.is_empty() && source.ends_with('-') {
-                            key = Some(String::from("-"));
-                            break;
-                        } else {
-                            return Err(anyhow!("Invalid keystroke `{}`", source));
-                        }
-                    } else {
-                        key = Some(String::from(component));
-                    }
-                }
-            }
-        }
-
-        let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
-
-        Ok(Keystroke {
-            ctrl,
-            alt,
-            shift,
-            cmd,
-            function,
-            key,
-        })
-    }
-
-    pub fn modified(&self) -> bool {
-        self.ctrl || self.alt || self.shift || self.cmd
-    }
-}
-
-impl std::fmt::Display for Keystroke {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        if self.ctrl {
-            f.write_char('^')?;
-        }
-        if self.alt {
-            f.write_char('βŽ‡')?;
-        }
-        if self.cmd {
-            f.write_char('⌘')?;
-        }
-        if self.shift {
-            f.write_char('⇧')?;
-        }
-        let key = match self.key.as_str() {
-            "backspace" => '⌫',
-            "up" => '↑',
-            "down" => '↓',
-            "left" => '←',
-            "right" => 'β†’',
-            "tab" => 'β‡₯',
-            "escape" => 'βŽ‹',
-            key => {
-                if key.len() == 1 {
-                    key.chars().next().unwrap().to_ascii_uppercase()
-                } else {
-                    return f.write_str(key);
-                }
-            }
-        };
-        f.write_char(key)
-    }
-}
-
-impl Context {
-    pub fn extend(&mut self, other: &Context) {
-        for v in &other.set {
-            self.set.insert(v.clone());
-        }
-        for (k, v) in &other.map {
-            self.map.insert(k.clone(), v.clone());
-        }
-    }
-}
-
-impl ContextPredicate {
-    fn parse(source: &str) -> anyhow::Result<Self> {
-        let mut parser = Parser::new();
-        let language = unsafe { tree_sitter_context_predicate() };
-        parser.set_language(language).unwrap();
-        let source = source.as_bytes();
-        let tree = parser.parse(source, None).unwrap();
-        Self::from_node(tree.root_node(), source)
-    }
-
-    fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
-        let parse_error = "error parsing context predicate";
-        let kind = node.kind();
-
-        match kind {
-            "source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
-            "identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
-            "not" => {
-                let child = Self::from_node(
-                    node.child_by_field_name("expression")
-                        .ok_or_else(|| anyhow!(parse_error))?,
-                    source,
-                )?;
-                Ok(Self::Not(Box::new(child)))
-            }
-            "and" | "or" => {
-                let left = Box::new(Self::from_node(
-                    node.child_by_field_name("left")
-                        .ok_or_else(|| anyhow!(parse_error))?,
-                    source,
-                )?);
-                let right = Box::new(Self::from_node(
-                    node.child_by_field_name("right")
-                        .ok_or_else(|| anyhow!(parse_error))?,
-                    source,
-                )?);
-                if kind == "and" {
-                    Ok(Self::And(left, right))
-                } else {
-                    Ok(Self::Or(left, right))
-                }
-            }
-            "equal" | "not_equal" => {
-                let left = node
-                    .child_by_field_name("left")
-                    .ok_or_else(|| anyhow!(parse_error))?
-                    .utf8_text(source)?
-                    .into();
-                let right = node
-                    .child_by_field_name("right")
-                    .ok_or_else(|| anyhow!(parse_error))?
-                    .utf8_text(source)?
-                    .into();
-                if kind == "equal" {
-                    Ok(Self::Equal(left, right))
-                } else {
-                    Ok(Self::NotEqual(left, right))
-                }
-            }
-            "parenthesized" => Self::from_node(
-                node.child_by_field_name("expression")
-                    .ok_or_else(|| anyhow!(parse_error))?,
-                source,
-            ),
-            _ => Err(anyhow!(parse_error)),
-        }
-    }
-
-    fn eval(&self, cx: &Context) -> bool {
-        match self {
-            Self::Identifier(name) => cx.set.contains(name.as_str()),
-            Self::Equal(left, right) => cx
-                .map
-                .get(left)
-                .map(|value| value == right)
-                .unwrap_or(false),
-            Self::NotEqual(left, right) => {
-                cx.map.get(left).map(|value| value != right).unwrap_or(true)
-            }
-            Self::Not(pred) => !pred.eval(cx),
-            Self::And(left, right) => left.eval(cx) && right.eval(cx),
-            Self::Or(left, right) => left.eval(cx) || right.eval(cx),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use anyhow::Result;
-    use serde::Deserialize;
-
-    use crate::{actions, impl_actions};
-
-    use super::*;
-
-    #[test]
-    fn test_push_keystroke() -> Result<()> {
-        actions!(test, [B, AB, C, D, DA]);
-
-        let mut ctx1 = Context::default();
-        ctx1.set.insert("1".into());
-
-        let mut ctx2 = Context::default();
-        ctx2.set.insert("2".into());
-
-        let dispatch_path = vec![(2, ctx2), (1, ctx1)];
-
-        let keymap = Keymap::new(vec![
-            Binding::new("a b", AB, Some("1")),
-            Binding::new("b", B, Some("2")),
-            Binding::new("c", C, Some("2")),
-            Binding::new("d", D, Some("1")),
-            Binding::new("d", D, Some("2")),
-            Binding::new("d a", DA, Some("2")),
-        ]);
-
-        let mut matcher = Matcher::new(keymap);
-
-        // Binding with pending prefix always takes precedence
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
-            MatchResult::Pending,
-        );
-        // B alone doesn't match because a was pending, so AB is returned instead
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
-            MatchResult::Matches(vec![(1, Box::new(AB))]),
-        );
-        assert!(!matcher.has_pending_keystrokes());
-
-        // Without an a prefix, B is dispatched like expected
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
-            MatchResult::Matches(vec![(2, Box::new(B))]),
-        );
-        assert!(!matcher.has_pending_keystrokes());
-
-        // If a is prefixed, C will not be dispatched because there
-        // was a pending binding for it
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
-            MatchResult::Pending,
-        );
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
-            MatchResult::None,
-        );
-        assert!(!matcher.has_pending_keystrokes());
-
-        // If a single keystroke matches multiple bindings in the tree
-        // all of them are returned so that we can fallback if the action
-        // handler decides to propagate the action
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
-            MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
-        );
-        // If none of the d action handlers consume the binding, a pending
-        // binding may then be used
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
-            MatchResult::Matches(vec![(2, Box::new(DA))]),
-        );
-        assert!(!matcher.has_pending_keystrokes());
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_keystroke_parsing() -> Result<()> {
-        assert_eq!(
-            Keystroke::parse("ctrl-p")?,
-            Keystroke {
-                key: "p".into(),
-                ctrl: true,
-                alt: false,
-                shift: false,
-                cmd: false,
-                function: false,
-            }
-        );
-
-        assert_eq!(
-            Keystroke::parse("alt-shift-down")?,
-            Keystroke {
-                key: "down".into(),
-                ctrl: false,
-                alt: true,
-                shift: true,
-                cmd: false,
-                function: false,
-            }
-        );
-
-        assert_eq!(
-            Keystroke::parse("shift-cmd--")?,
-            Keystroke {
-                key: "-".into(),
-                ctrl: false,
-                alt: false,
-                shift: true,
-                cmd: true,
-                function: false,
-            }
-        );
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_context_predicate_parsing() -> Result<()> {
-        use ContextPredicate::*;
-
-        assert_eq!(
-            ContextPredicate::parse("a && (b == c || d != e)")?,
-            And(
-                Box::new(Identifier("a".into())),
-                Box::new(Or(
-                    Box::new(Equal("b".into(), "c".into())),
-                    Box::new(NotEqual("d".into(), "e".into())),
-                ))
-            )
-        );
-
-        assert_eq!(
-            ContextPredicate::parse("!a")?,
-            Not(Box::new(Identifier("a".into())),)
-        );
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_context_predicate_eval() -> Result<()> {
-        let predicate = ContextPredicate::parse("a && b || c == d")?;
-
-        let mut context = Context::default();
-        context.set.insert("a".into());
-        assert!(!predicate.eval(&context));
-
-        context.set.insert("b".into());
-        assert!(predicate.eval(&context));
-
-        context.set.remove("b");
-        context.map.insert("c".into(), "x".into());
-        assert!(!predicate.eval(&context));
-
-        context.map.insert("c".into(), "d".into());
-        assert!(predicate.eval(&context));
-
-        let predicate = ContextPredicate::parse("!a")?;
-        assert!(predicate.eval(&Context::default()));
-
-        Ok(())
-    }
-
-    #[test]
-    fn test_matcher() -> Result<()> {
-        #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
-        pub struct A(pub String);
-        impl_actions!(test, [A]);
-        actions!(test, [B, Ab]);
-
-        #[derive(Clone, Debug, Eq, PartialEq)]
-        struct ActionArg {
-            a: &'static str,
-        }
-
-        let keymap = Keymap::new(vec![
-            Binding::new("a", A("x".to_string()), Some("a")),
-            Binding::new("b", B, Some("a")),
-            Binding::new("a b", Ab, Some("a || b")),
-        ]);
-
-        let mut ctx_a = Context::default();
-        ctx_a.set.insert("a".into());
-
-        let mut ctx_b = Context::default();
-        ctx_b.set.insert("b".into());
-
-        let mut matcher = Matcher::new(keymap);
-
-        // Basic match
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
-            MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
-        );
-        matcher.clear_pending();
-
-        // Multi-keystroke match
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
-            MatchResult::Pending
-        );
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
-            MatchResult::Matches(vec![(1, Box::new(Ab))])
-        );
-        matcher.clear_pending();
-
-        // Failed matches don't interfere with matching subsequent keys
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, ctx_a.clone())]),
-            MatchResult::None
-        );
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
-            MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
-        );
-        matcher.clear_pending();
-
-        // Pending keystrokes are cleared when the context changes
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
-            MatchResult::Pending
-        );
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_a.clone())]),
-            MatchResult::None
-        );
-        matcher.clear_pending();
-
-        let mut ctx_c = Context::default();
-        ctx_c.set.insert("c".into());
-
-        // Pending keystrokes are maintained per-view
-        assert_eq!(
-            matcher.push_keystroke(
-                Keystroke::parse("a")?,
-                vec![(1, ctx_b.clone()), (2, ctx_c.clone())]
-            ),
-            MatchResult::Pending
-        );
-        assert_eq!(
-            matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
-            MatchResult::Matches(vec![(1, Box::new(Ab))])
-        );
-
-        Ok(())
-    }
-}

crates/gpui/src/keymap_matcher.rs πŸ”—

@@ -0,0 +1,459 @@
+mod binding;
+mod keymap;
+mod keymap_context;
+mod keystroke;
+
+use std::{any::TypeId, fmt::Debug};
+
+use collections::HashMap;
+use serde::Deserialize;
+use smallvec::SmallVec;
+
+use crate::{impl_actions, Action};
+
+pub use binding::{Binding, BindingMatchResult};
+pub use keymap::Keymap;
+pub use keymap_context::{KeymapContext, KeymapContextPredicate};
+pub use keystroke::Keystroke;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
+pub struct KeyPressed {
+    #[serde(default)]
+    pub keystroke: Keystroke,
+}
+
+impl_actions!(gpui, [KeyPressed]);
+
+pub struct KeymapMatcher {
+    pending_views: HashMap<usize, KeymapContext>,
+    pending_keystrokes: Vec<Keystroke>,
+    keymap: Keymap,
+}
+
+impl KeymapMatcher {
+    pub fn new(keymap: Keymap) -> Self {
+        Self {
+            pending_views: Default::default(),
+            pending_keystrokes: Vec::new(),
+            keymap,
+        }
+    }
+
+    pub fn set_keymap(&mut self, keymap: Keymap) {
+        self.clear_pending();
+        self.keymap = keymap;
+    }
+
+    pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
+        self.clear_pending();
+        self.keymap.add_bindings(bindings);
+    }
+
+    pub fn clear_bindings(&mut self) {
+        self.clear_pending();
+        self.keymap.clear();
+    }
+
+    pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
+        self.keymap.bindings_for_action_type(action_type)
+    }
+
+    pub fn clear_pending(&mut self) {
+        self.pending_keystrokes.clear();
+        self.pending_views.clear();
+    }
+
+    pub fn has_pending_keystrokes(&self) -> bool {
+        !self.pending_keystrokes.is_empty()
+    }
+
+    pub fn push_keystroke(
+        &mut self,
+        keystroke: Keystroke,
+        dispatch_path: Vec<(usize, KeymapContext)>,
+    ) -> MatchResult {
+        let mut any_pending = false;
+        let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
+
+        let first_keystroke = self.pending_keystrokes.is_empty();
+        self.pending_keystrokes.push(keystroke.clone());
+
+        for (view_id, context) in dispatch_path {
+            // Don't require pending view entry if there are no pending keystrokes
+            if !first_keystroke && !self.pending_views.contains_key(&view_id) {
+                continue;
+            }
+
+            // If there is a previous view context, invalidate that view if it
+            // has changed
+            if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
+                if previous_view_context != context {
+                    continue;
+                }
+            }
+
+            // Find the bindings which map the pending keystrokes and current context
+            for binding in self.keymap.bindings().iter().rev() {
+                match binding.match_keys_and_context(&self.pending_keystrokes, &context) {
+                    BindingMatchResult::Complete(mut action) => {
+                        // Swap in keystroke for special KeyPressed action
+                        if action.name() == "KeyPressed" && action.namespace() == "gpui" {
+                            action = Box::new(KeyPressed {
+                                keystroke: keystroke.clone(),
+                            });
+                        }
+                        matched_bindings.push((view_id, action))
+                    }
+                    BindingMatchResult::Partial => {
+                        self.pending_views.insert(view_id, context.clone());
+                        any_pending = true;
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        if !any_pending {
+            self.clear_pending();
+        }
+
+        if !matched_bindings.is_empty() {
+            MatchResult::Matches(matched_bindings)
+        } else if any_pending {
+            MatchResult::Pending
+        } else {
+            MatchResult::None
+        }
+    }
+
+    pub fn keystrokes_for_action(
+        &self,
+        action: &dyn Action,
+        context: &KeymapContext,
+    ) -> Option<SmallVec<[Keystroke; 2]>> {
+        self.keymap
+            .bindings()
+            .iter()
+            .rev()
+            .find_map(|binding| binding.keystrokes_for_action(action, context))
+    }
+}
+
+impl Default for KeymapMatcher {
+    fn default() -> Self {
+        Self::new(Keymap::default())
+    }
+}
+
+pub enum MatchResult {
+    None,
+    Pending,
+    Matches(Vec<(usize, Box<dyn Action>)>),
+}
+
+impl Debug for MatchResult {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            MatchResult::None => f.debug_struct("MatchResult::None").finish(),
+            MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
+            MatchResult::Matches(matches) => f
+                .debug_list()
+                .entries(
+                    matches
+                        .iter()
+                        .map(|(view_id, action)| format!("{view_id}, {}", action.name())),
+                )
+                .finish(),
+        }
+    }
+}
+
+impl PartialEq for MatchResult {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (MatchResult::None, MatchResult::None) => true,
+            (MatchResult::Pending, MatchResult::Pending) => true,
+            (MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
+                matches.len() == other_matches.len()
+                    && matches.iter().zip(other_matches.iter()).all(
+                        |((view_id, action), (other_view_id, other_action))| {
+                            view_id == other_view_id && action.eq(other_action.as_ref())
+                        },
+                    )
+            }
+            _ => false,
+        }
+    }
+}
+
+impl Eq for MatchResult {}
+
+impl Clone for MatchResult {
+    fn clone(&self) -> Self {
+        match self {
+            MatchResult::None => MatchResult::None,
+            MatchResult::Pending => MatchResult::Pending,
+            MatchResult::Matches(matches) => MatchResult::Matches(
+                matches
+                    .iter()
+                    .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
+                    .collect(),
+            ),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use anyhow::Result;
+    use serde::Deserialize;
+
+    use crate::{actions, impl_actions, keymap_matcher::KeymapContext};
+
+    use super::*;
+
+    #[test]
+    fn test_push_keystroke() -> Result<()> {
+        actions!(test, [B, AB, C, D, DA]);
+
+        let mut context1 = KeymapContext::default();
+        context1.set.insert("1".into());
+
+        let mut context2 = KeymapContext::default();
+        context2.set.insert("2".into());
+
+        let dispatch_path = vec![(2, context2), (1, context1)];
+
+        let keymap = Keymap::new(vec![
+            Binding::new("a b", AB, Some("1")),
+            Binding::new("b", B, Some("2")),
+            Binding::new("c", C, Some("2")),
+            Binding::new("d", D, Some("1")),
+            Binding::new("d", D, Some("2")),
+            Binding::new("d a", DA, Some("2")),
+        ]);
+
+        let mut matcher = KeymapMatcher::new(keymap);
+
+        // Binding with pending prefix always takes precedence
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+            MatchResult::Pending,
+        );
+        // B alone doesn't match because a was pending, so AB is returned instead
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+            MatchResult::Matches(vec![(1, Box::new(AB))]),
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        // Without an a prefix, B is dispatched like expected
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+            MatchResult::Matches(vec![(2, Box::new(B))]),
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        // If a is prefixed, C will not be dispatched because there
+        // was a pending binding for it
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+            MatchResult::Pending,
+        );
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
+            MatchResult::None,
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        // If a single keystroke matches multiple bindings in the tree
+        // all of them are returned so that we can fallback if the action
+        // handler decides to propagate the action
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
+            MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
+        );
+        // If none of the d action handlers consume the binding, a pending
+        // binding may then be used
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+            MatchResult::Matches(vec![(2, Box::new(DA))]),
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_keystroke_parsing() -> Result<()> {
+        assert_eq!(
+            Keystroke::parse("ctrl-p")?,
+            Keystroke {
+                key: "p".into(),
+                ctrl: true,
+                alt: false,
+                shift: false,
+                cmd: false,
+                function: false,
+            }
+        );
+
+        assert_eq!(
+            Keystroke::parse("alt-shift-down")?,
+            Keystroke {
+                key: "down".into(),
+                ctrl: false,
+                alt: true,
+                shift: true,
+                cmd: false,
+                function: false,
+            }
+        );
+
+        assert_eq!(
+            Keystroke::parse("shift-cmd--")?,
+            Keystroke {
+                key: "-".into(),
+                ctrl: false,
+                alt: false,
+                shift: true,
+                cmd: true,
+                function: false,
+            }
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_context_predicate_parsing() -> Result<()> {
+        use KeymapContextPredicate::*;
+
+        assert_eq!(
+            KeymapContextPredicate::parse("a && (b == c || d != e)")?,
+            And(
+                Box::new(Identifier("a".into())),
+                Box::new(Or(
+                    Box::new(Equal("b".into(), "c".into())),
+                    Box::new(NotEqual("d".into(), "e".into())),
+                ))
+            )
+        );
+
+        assert_eq!(
+            KeymapContextPredicate::parse("!a")?,
+            Not(Box::new(Identifier("a".into())),)
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_context_predicate_eval() -> Result<()> {
+        let predicate = KeymapContextPredicate::parse("a && b || c == d")?;
+
+        let mut context = KeymapContext::default();
+        context.set.insert("a".into());
+        assert!(!predicate.eval(&context));
+
+        context.set.insert("b".into());
+        assert!(predicate.eval(&context));
+
+        context.set.remove("b");
+        context.map.insert("c".into(), "x".into());
+        assert!(!predicate.eval(&context));
+
+        context.map.insert("c".into(), "d".into());
+        assert!(predicate.eval(&context));
+
+        let predicate = KeymapContextPredicate::parse("!a")?;
+        assert!(predicate.eval(&KeymapContext::default()));
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_matcher() -> Result<()> {
+        #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+        pub struct A(pub String);
+        impl_actions!(test, [A]);
+        actions!(test, [B, Ab]);
+
+        #[derive(Clone, Debug, Eq, PartialEq)]
+        struct ActionArg {
+            a: &'static str,
+        }
+
+        let keymap = Keymap::new(vec![
+            Binding::new("a", A("x".to_string()), Some("a")),
+            Binding::new("b", B, Some("a")),
+            Binding::new("a b", Ab, Some("a || b")),
+        ]);
+
+        let mut context_a = KeymapContext::default();
+        context_a.set.insert("a".into());
+
+        let mut context_b = KeymapContext::default();
+        context_b.set.insert("b".into());
+
+        let mut matcher = KeymapMatcher::new(keymap);
+
+        // Basic match
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
+        );
+        matcher.clear_pending();
+
+        // Multi-keystroke match
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+            MatchResult::Pending
+        );
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(Ab))])
+        );
+        matcher.clear_pending();
+
+        // Failed matches don't interfere with matching subsequent keys
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
+            MatchResult::None
+        );
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
+        );
+        matcher.clear_pending();
+
+        // Pending keystrokes are cleared when the context changes
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+            MatchResult::Pending
+        );
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
+            MatchResult::None
+        );
+        matcher.clear_pending();
+
+        let mut context_c = KeymapContext::default();
+        context_c.set.insert("c".into());
+
+        // Pending keystrokes are maintained per-view
+        assert_eq!(
+            matcher.push_keystroke(
+                Keystroke::parse("a")?,
+                vec![(1, context_b.clone()), (2, context_c.clone())]
+            ),
+            MatchResult::Pending
+        );
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(Ab))])
+        );
+
+        Ok(())
+    }
+}

crates/gpui/src/keymap_matcher/binding.rs πŸ”—

@@ -0,0 +1,104 @@
+use anyhow::Result;
+use smallvec::SmallVec;
+
+use crate::Action;
+
+use super::{KeymapContext, KeymapContextPredicate, Keystroke};
+
+pub struct Binding {
+    action: Box<dyn Action>,
+    keystrokes: Option<SmallVec<[Keystroke; 2]>>,
+    context_predicate: Option<KeymapContextPredicate>,
+}
+
+impl Binding {
+    pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
+        Self::load(keystrokes, Box::new(action), context).unwrap()
+    }
+
+    pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
+        let context = if let Some(context) = context {
+            Some(KeymapContextPredicate::parse(context)?)
+        } else {
+            None
+        };
+
+        let keystrokes = if keystrokes == "*" {
+            None // Catch all context
+        } else {
+            Some(
+                keystrokes
+                    .split_whitespace()
+                    .map(Keystroke::parse)
+                    .collect::<Result<_>>()?,
+            )
+        };
+
+        Ok(Self {
+            keystrokes,
+            action,
+            context_predicate: context,
+        })
+    }
+
+    fn match_context(&self, context: &KeymapContext) -> bool {
+        self.context_predicate
+            .as_ref()
+            .map(|predicate| predicate.eval(context))
+            .unwrap_or(true)
+    }
+
+    pub fn match_keys_and_context(
+        &self,
+        pending_keystrokes: &Vec<Keystroke>,
+        context: &KeymapContext,
+    ) -> BindingMatchResult {
+        if self
+            .keystrokes
+            .as_ref()
+            .map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
+            .unwrap_or(true)
+            && self.match_context(context)
+        {
+            // If the binding is completed, push it onto the matches list
+            if self
+                .keystrokes
+                .as_ref()
+                .map(|keystrokes| keystrokes.len() == pending_keystrokes.len())
+                .unwrap_or(true)
+            {
+                BindingMatchResult::Complete(self.action.boxed_clone())
+            } else {
+                BindingMatchResult::Partial
+            }
+        } else {
+            BindingMatchResult::Fail
+        }
+    }
+
+    pub fn keystrokes_for_action(
+        &self,
+        action: &dyn Action,
+        context: &KeymapContext,
+    ) -> Option<SmallVec<[Keystroke; 2]>> {
+        if self.action.eq(action) && self.match_context(context) {
+            self.keystrokes.clone()
+        } else {
+            None
+        }
+    }
+
+    pub fn keystrokes(&self) -> Option<&[Keystroke]> {
+        self.keystrokes.as_deref()
+    }
+
+    pub fn action(&self) -> &dyn Action {
+        self.action.as_ref()
+    }
+}
+
+pub enum BindingMatchResult {
+    Complete(Box<dyn Action>),
+    Partial,
+    Fail,
+}

crates/gpui/src/keymap_matcher/keymap.rs πŸ”—

@@ -0,0 +1,61 @@
+use smallvec::SmallVec;
+use std::{
+    any::{Any, TypeId},
+    collections::HashMap,
+};
+
+use super::Binding;
+
+#[derive(Default)]
+pub struct Keymap {
+    bindings: Vec<Binding>,
+    binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
+}
+
+impl Keymap {
+    pub fn new(bindings: Vec<Binding>) -> Self {
+        let mut binding_indices_by_action_type = HashMap::new();
+        for (ix, binding) in bindings.iter().enumerate() {
+            binding_indices_by_action_type
+                .entry(binding.action().type_id())
+                .or_insert_with(SmallVec::new)
+                .push(ix);
+        }
+
+        Self {
+            binding_indices_by_action_type,
+            bindings,
+        }
+    }
+
+    pub(crate) fn bindings_for_action_type(
+        &self,
+        action_type: TypeId,
+    ) -> impl Iterator<Item = &'_ Binding> {
+        self.binding_indices_by_action_type
+            .get(&action_type)
+            .map(SmallVec::as_slice)
+            .unwrap_or(&[])
+            .iter()
+            .map(|ix| &self.bindings[*ix])
+    }
+
+    pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
+        for binding in bindings {
+            self.binding_indices_by_action_type
+                .entry(binding.action().type_id())
+                .or_default()
+                .push(self.bindings.len());
+            self.bindings.push(binding);
+        }
+    }
+
+    pub(crate) fn clear(&mut self) {
+        self.bindings.clear();
+        self.binding_indices_by_action_type.clear();
+    }
+
+    pub fn bindings(&self) -> &Vec<Binding> {
+        &self.bindings
+    }
+}

crates/gpui/src/keymap_matcher/keymap_context.rs πŸ”—

@@ -0,0 +1,123 @@
+use anyhow::anyhow;
+
+use collections::{HashMap, HashSet};
+use tree_sitter::{Language, Node, Parser};
+
+extern "C" {
+    fn tree_sitter_context_predicate() -> Language;
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct KeymapContext {
+    pub set: HashSet<String>,
+    pub map: HashMap<String, String>,
+}
+
+impl KeymapContext {
+    pub fn extend(&mut self, other: &Self) {
+        for v in &other.set {
+            self.set.insert(v.clone());
+        }
+        for (k, v) in &other.map {
+            self.map.insert(k.clone(), v.clone());
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum KeymapContextPredicate {
+    Identifier(String),
+    Equal(String, String),
+    NotEqual(String, String),
+    Not(Box<KeymapContextPredicate>),
+    And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
+    Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
+}
+
+impl KeymapContextPredicate {
+    pub fn parse(source: &str) -> anyhow::Result<Self> {
+        let mut parser = Parser::new();
+        let language = unsafe { tree_sitter_context_predicate() };
+        parser.set_language(language).unwrap();
+        let source = source.as_bytes();
+        let tree = parser.parse(source, None).unwrap();
+        Self::from_node(tree.root_node(), source)
+    }
+
+    fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
+        let parse_error = "error parsing context predicate";
+        let kind = node.kind();
+
+        match kind {
+            "source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
+            "identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
+            "not" => {
+                let child = Self::from_node(
+                    node.child_by_field_name("expression")
+                        .ok_or_else(|| anyhow!(parse_error))?,
+                    source,
+                )?;
+                Ok(Self::Not(Box::new(child)))
+            }
+            "and" | "or" => {
+                let left = Box::new(Self::from_node(
+                    node.child_by_field_name("left")
+                        .ok_or_else(|| anyhow!(parse_error))?,
+                    source,
+                )?);
+                let right = Box::new(Self::from_node(
+                    node.child_by_field_name("right")
+                        .ok_or_else(|| anyhow!(parse_error))?,
+                    source,
+                )?);
+                if kind == "and" {
+                    Ok(Self::And(left, right))
+                } else {
+                    Ok(Self::Or(left, right))
+                }
+            }
+            "equal" | "not_equal" => {
+                let left = node
+                    .child_by_field_name("left")
+                    .ok_or_else(|| anyhow!(parse_error))?
+                    .utf8_text(source)?
+                    .into();
+                let right = node
+                    .child_by_field_name("right")
+                    .ok_or_else(|| anyhow!(parse_error))?
+                    .utf8_text(source)?
+                    .into();
+                if kind == "equal" {
+                    Ok(Self::Equal(left, right))
+                } else {
+                    Ok(Self::NotEqual(left, right))
+                }
+            }
+            "parenthesized" => Self::from_node(
+                node.child_by_field_name("expression")
+                    .ok_or_else(|| anyhow!(parse_error))?,
+                source,
+            ),
+            _ => Err(anyhow!(parse_error)),
+        }
+    }
+
+    pub fn eval(&self, context: &KeymapContext) -> bool {
+        match self {
+            Self::Identifier(name) => context.set.contains(name.as_str()),
+            Self::Equal(left, right) => context
+                .map
+                .get(left)
+                .map(|value| value == right)
+                .unwrap_or(false),
+            Self::NotEqual(left, right) => context
+                .map
+                .get(left)
+                .map(|value| value != right)
+                .unwrap_or(true),
+            Self::Not(pred) => !pred.eval(context),
+            Self::And(left, right) => left.eval(context) && right.eval(context),
+            Self::Or(left, right) => left.eval(context) || right.eval(context),
+        }
+    }
+}

crates/gpui/src/keymap_matcher/keystroke.rs πŸ”—

@@ -0,0 +1,97 @@
+use std::fmt::Write;
+
+use anyhow::anyhow;
+use serde::Deserialize;
+
+#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)]
+pub struct Keystroke {
+    pub ctrl: bool,
+    pub alt: bool,
+    pub shift: bool,
+    pub cmd: bool,
+    pub function: bool,
+    pub key: String,
+}
+
+impl Keystroke {
+    pub fn parse(source: &str) -> anyhow::Result<Self> {
+        let mut ctrl = false;
+        let mut alt = false;
+        let mut shift = false;
+        let mut cmd = false;
+        let mut function = false;
+        let mut key = None;
+
+        let mut components = source.split('-').peekable();
+        while let Some(component) = components.next() {
+            match component {
+                "ctrl" => ctrl = true,
+                "alt" => alt = true,
+                "shift" => shift = true,
+                "cmd" => cmd = true,
+                "fn" => function = true,
+                _ => {
+                    if let Some(component) = components.peek() {
+                        if component.is_empty() && source.ends_with('-') {
+                            key = Some(String::from("-"));
+                            break;
+                        } else {
+                            return Err(anyhow!("Invalid keystroke `{}`", source));
+                        }
+                    } else {
+                        key = Some(String::from(component));
+                    }
+                }
+            }
+        }
+
+        let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
+
+        Ok(Keystroke {
+            ctrl,
+            alt,
+            shift,
+            cmd,
+            function,
+            key,
+        })
+    }
+
+    pub fn modified(&self) -> bool {
+        self.ctrl || self.alt || self.shift || self.cmd
+    }
+}
+
+impl std::fmt::Display for Keystroke {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.ctrl {
+            f.write_char('^')?;
+        }
+        if self.alt {
+            f.write_char('βŽ‡')?;
+        }
+        if self.cmd {
+            f.write_char('⌘')?;
+        }
+        if self.shift {
+            f.write_char('⇧')?;
+        }
+        let key = match self.key.as_str() {
+            "backspace" => '⌫',
+            "up" => '↑',
+            "down" => '↓',
+            "left" => '←',
+            "right" => 'β†’',
+            "tab" => 'β‡₯',
+            "escape" => 'βŽ‹',
+            key => {
+                if key.len() == 1 {
+                    key.chars().next().unwrap().to_ascii_uppercase()
+                } else {
+                    return f.write_str(key);
+                }
+            }
+        };
+        f.write_char(key)
+    }
+}

crates/gpui/src/platform.rs πŸ”—

@@ -14,7 +14,7 @@ use crate::{
         rect::{RectF, RectI},
         vector::Vector2F,
     },
-    keymap,
+    keymap_matcher::KeymapMatcher,
     text_layout::{LineLayout, RunStyle},
     Action, ClipboardItem, Menu, Scene,
 };
@@ -87,7 +87,7 @@ pub(crate) trait ForegroundPlatform {
     fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
     fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
     fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
-    fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
+    fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,

crates/gpui/src/platform/event.rs πŸ”—

@@ -2,7 +2,7 @@ use std::ops::Deref;
 
 use pathfinder_geometry::vector::vec2f;
 
-use crate::{geometry::vector::Vector2F, keymap::Keystroke};
+use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
 
 #[derive(Clone, Debug)]
 pub struct KeyDownEvent {

crates/gpui/src/platform/mac/event.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
     geometry::vector::vec2f,
-    keymap::Keystroke,
+    keymap_matcher::Keystroke,
     platform::{Event, NavigationDirection},
     KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
     MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
@@ -47,6 +47,8 @@ pub fn key_to_native(key: &str) -> Cow<str> {
         "right" => NSRightArrowFunctionKey,
         "pageup" => NSPageUpFunctionKey,
         "pagedown" => NSPageDownFunctionKey,
+        "home" => NSHomeFunctionKey,
+        "end" => NSEndFunctionKey,
         "delete" => NSDeleteFunctionKey,
         "f1" => NSF1FunctionKey,
         "f2" => NSF2FunctionKey,
@@ -258,6 +260,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
         Some(NSRightArrowFunctionKey) => "right".to_string(),
         Some(NSPageUpFunctionKey) => "pageup".to_string(),
         Some(NSPageDownFunctionKey) => "pagedown".to_string(),
+        Some(NSHomeFunctionKey) => "home".to_string(),
+        Some(NSEndFunctionKey) => "end".to_string(),
         Some(NSDeleteFunctionKey) => "delete".to_string(),
         Some(NSF1FunctionKey) => "f1".to_string(),
         Some(NSF2FunctionKey) => "f2".to_string(),

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -3,7 +3,8 @@ use super::{
     FontSystem, Window,
 };
 use crate::{
-    executor, keymap,
+    executor,
+    keymap_matcher::KeymapMatcher,
     platform::{self, CursorStyle},
     Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
 };
@@ -135,7 +136,7 @@ impl MacForegroundPlatform {
         menus: Vec<Menu>,
         delegate: id,
         actions: &mut Vec<Box<dyn Action>>,
-        keystroke_matcher: &keymap::Matcher,
+        keystroke_matcher: &KeymapMatcher,
     ) -> id {
         let application_menu = NSMenu::new(nil).autorelease();
         application_menu.setDelegate_(delegate);
@@ -172,7 +173,7 @@ impl MacForegroundPlatform {
         item: MenuItem,
         delegate: id,
         actions: &mut Vec<Box<dyn Action>>,
-        keystroke_matcher: &keymap::Matcher,
+        keystroke_matcher: &KeymapMatcher,
     ) -> id {
         match item {
             MenuItem::Separator => NSMenuItem::separatorItem(nil),
@@ -183,7 +184,7 @@ impl MacForegroundPlatform {
                     .map(|binding| binding.keystrokes());
 
                 let item;
-                if let Some(keystrokes) = keystrokes {
+                if let Some(keystrokes) = keystrokes.flatten() {
                     if keystrokes.len() == 1 {
                         let keystroke = &keystrokes[0];
                         let mut mask = NSEventModifierFlags::empty();
@@ -317,7 +318,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().validate_menu_command = Some(callback);
     }
 
-    fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
+    fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
         unsafe {
             let app: id = msg_send![APP_CLASS, sharedApplication];
             let mut state = self.0.borrow_mut();
@@ -647,7 +648,7 @@ impl platform::Platform for MacPlatform {
             attrs.set(kSecReturnAttributes as *const _, cf_true);
             attrs.set(kSecReturnData as *const _, cf_true);
 
-            let mut result = CFTypeRef::from(ptr::null_mut());
+            let mut result = CFTypeRef::from(ptr::null());
             let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
             match status {
                 security::errSecSuccess => {}

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -4,7 +4,7 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    keymap::Keystroke,
+    keymap_matcher::Keystroke,
     mac::platform::NSViewLayerContentsRedrawDuringViewResize,
     platform::{
         self,

crates/gpui/src/platform/test.rs πŸ”—

@@ -4,7 +4,8 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    keymap, Action, ClipboardItem,
+    keymap_matcher::KeymapMatcher,
+    Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -84,7 +85,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
     fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
     fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
     fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
-    fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
+    fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
 
     fn prompt_for_paths(
         &self,

crates/gpui/src/presenter.rs πŸ”—

@@ -4,7 +4,7 @@ use crate::{
     font_cache::FontCache,
     geometry::rect::RectF,
     json::{self, ToJson},
-    keymap::Keystroke,
+    keymap_matcher::Keystroke,
     platform::{CursorStyle, Event},
     scene::{
         CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,

crates/gpui_macros/src/gpui_macros.rs πŸ”—

@@ -162,11 +162,9 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
             if let FnArg::Typed(arg) = arg {
                 if let Type::Path(ty) = &*arg.ty {
                     let last_segment = ty.path.segments.last();
-                    match last_segment.map(|s| s.ident.to_string()).as_deref() {
-                        Some("StdRng") => {
-                            inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
-                        }
-                        _ => {}
+
+                    if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
+                        inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
                     }
                 } else {
                     inner_fn_args.extend(quote!(cx,));

crates/language/src/buffer.rs πŸ”—

@@ -682,7 +682,6 @@ impl Buffer {
         task
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn diff_base(&self) -> Option<&str> {
         self.diff_base.as_deref()
     }

crates/language/src/buffer_tests.rs πŸ”—

@@ -289,6 +289,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
     );
 
     buffer.update(cx, |buf, cx| {
+        buf.undo(cx);
+        buf.undo(cx);
+        buf.undo(cx);
         buf.undo(cx);
         assert_eq!(buf.text(), "fn a() {}");
         assert!(buf.is_parsing());
@@ -304,6 +307,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
     );
 
     buffer.update(cx, |buf, cx| {
+        buf.redo(cx);
+        buf.redo(cx);
+        buf.redo(cx);
         buf.redo(cx);
         assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
         assert!(buf.is_parsing());
@@ -1022,8 +1028,11 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
             .unindent()
         );
 
+        // Grouping is disabled in tests, so we need 2 undos
+        buffer.undo(cx); // Undo the auto-indent
+        buffer.undo(cx); // Undo the original edit
+
         // Insert the block at a deeper indent level. The entire block is outdented.
-        buffer.undo(cx);
         buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "        ")], None, cx);
         buffer.edit(
             [(Point::new(2, 8)..Point::new(2, 8), inserted_text)],

crates/live_kit_client/build.rs πŸ”—

@@ -5,7 +5,7 @@ use std::{
     process::Command,
 };
 
-const SWIFT_PACKAGE_NAME: &'static str = "LiveKitBridge";
+const SWIFT_PACKAGE_NAME: &str = "LiveKitBridge";
 
 #[derive(Debug, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -61,8 +61,8 @@ fn build_bridge(swift_target: &SwiftTarget) {
     let swift_package_root = swift_package_root();
     if !Command::new("swift")
         .arg("build")
-        .args(&["--configuration", &env::var("PROFILE").unwrap()])
-        .args(&["--triple", &swift_target.target.triple])
+        .args(["--configuration", &env::var("PROFILE").unwrap()])
+        .args(["--triple", &swift_target.target.triple])
         .current_dir(&swift_package_root)
         .status()
         .unwrap()
@@ -116,7 +116,7 @@ fn get_swift_target() -> SwiftTarget {
     let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
 
     let swift_target_info_str = Command::new("swift")
-        .args(&["-target", &target, "-print-target-info"])
+        .args(["-target", &target, "-print-target-info"])
         .output()
         .unwrap()
         .stdout;
@@ -143,7 +143,7 @@ fn copy_dir(source: &Path, destination: &Path) {
     assert!(
         Command::new("cp")
             .arg("-R")
-            .args(&[source, destination])
+            .args([source, destination])
             .status()
             .unwrap()
             .success(),

crates/live_kit_client/examples/test_app.rs πŸ”—

@@ -1,5 +1,5 @@
 use futures::StreamExt;
-use gpui::{actions, keymap::Binding, Menu, MenuItem};
+use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem};
 use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
 use live_kit_server::token::{self, VideoGrant};
 use log::LevelFilter;

crates/media/build.rs πŸ”—

@@ -3,7 +3,7 @@ use std::{env, path::PathBuf, process::Command};
 fn main() {
     let sdk_path = String::from_utf8(
         Command::new("xcrun")
-            .args(&["--sdk", "macosx", "--show-sdk-path"])
+            .args(["--sdk", "macosx", "--show-sdk-path"])
             .output()
             .unwrap()
             .stdout,

crates/media/src/media.rs πŸ”—

@@ -113,9 +113,9 @@ pub mod core_video {
                 let mut this = ptr::null();
                 let result = CVMetalTextureCacheCreate(
                     kCFAllocatorDefault,
-                    ptr::null_mut(),
+                    ptr::null(),
                     metal_device,
-                    ptr::null_mut(),
+                    ptr::null(),
                     &mut this,
                 );
                 if result == kCVReturnSuccess {
@@ -192,7 +192,7 @@ pub mod core_video {
         pub fn as_texture_ref(&self) -> &metal::TextureRef {
             unsafe {
                 let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
-                &metal::TextureRef::from_ptr(texture as *mut _)
+                metal::TextureRef::from_ptr(texture as *mut _)
             }
         }
     }

crates/picker/src/picker.rs πŸ”—

@@ -2,7 +2,7 @@ use editor::Editor;
 use gpui::{
     elements::*,
     geometry::vector::{vec2f, Vector2F},
-    keymap,
+    keymap_matcher::KeymapContext,
     platform::CursorStyle,
     AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
     RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -124,7 +124,7 @@ impl<D: PickerDelegate> View for Picker<D> {
             .named("picker")
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
         cx.set.insert("menu".into());
         cx

crates/project/src/project.rs πŸ”—

@@ -5189,20 +5189,27 @@ impl Project {
 
                     let operations = buffer.serialize_ops(Some(remote_version), cx);
                     let client = this.client.clone();
-                    let file = buffer.file().cloned();
+                    if let Some(file) = buffer.file() {
+                        client
+                            .send(proto::UpdateBufferFile {
+                                project_id,
+                                buffer_id: buffer_id as u64,
+                                file: Some(file.to_proto()),
+                            })
+                            .log_err();
+                    }
+
+                    client
+                        .send(proto::UpdateDiffBase {
+                            project_id,
+                            buffer_id: buffer_id as u64,
+                            diff_base: buffer.diff_base().map(Into::into),
+                        })
+                        .log_err();
+
                     cx.background()
                         .spawn(
                             async move {
-                                if let Some(file) = file {
-                                    client
-                                        .send(proto::UpdateBufferFile {
-                                            project_id,
-                                            buffer_id: buffer_id as u64,
-                                            file: Some(file.to_proto()),
-                                        })
-                                        .log_err();
-                                }
-
                                 let operations = operations.await;
                                 for chunk in split_operations(operations) {
                                     client

crates/project_panel/src/project_panel.rs πŸ”—

@@ -10,7 +10,8 @@ use gpui::{
         MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
-    impl_internal_actions, keymap,
+    impl_internal_actions,
+    keymap_matcher::KeymapContext,
     platform::CursorStyle,
     AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
     MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
@@ -1301,7 +1302,7 @@ impl View for ProjectPanel {
             .boxed()
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
         cx.set.insert("menu".into());
         cx

crates/rpc/src/auth.rs πŸ”—

@@ -23,7 +23,7 @@ pub fn random_token() -> String {
     for byte in token_bytes.iter_mut() {
         *byte = rng.gen();
     }
-    base64::encode_config(&token_bytes, base64::URL_SAFE)
+    base64::encode_config(token_bytes, base64::URL_SAFE)
 }
 
 impl PublicKey {

crates/search/src/buffer_search.rs πŸ”—

@@ -106,73 +106,79 @@ impl View for BufferSearchBar {
             .with_child(
                 Flex::row()
                     .with_child(
-                        ChildView::new(&self.query_editor, cx)
-                            .aligned()
-                            .left()
-                            .flex(1., true)
-                            .boxed(),
-                    )
-                    .with_children(self.active_searchable_item.as_ref().and_then(
-                        |searchable_item| {
-                            let matches = self
-                                .seachable_items_with_matches
-                                .get(&searchable_item.downgrade())?;
-                            let message = if let Some(match_ix) = self.active_match_index {
-                                format!("{}/{}", match_ix + 1, matches.len())
-                            } else {
-                                "No matches".to_string()
-                            };
-
-                            Some(
-                                Label::new(message, theme.search.match_index.text.clone())
-                                    .contained()
-                                    .with_style(theme.search.match_index.container)
+                        Flex::row()
+                            .with_child(
+                                ChildView::new(&self.query_editor, cx)
                                     .aligned()
+                                    .left()
+                                    .flex(1., true)
                                     .boxed(),
                             )
-                        },
-                    ))
-                    .contained()
-                    .with_style(editor_container)
-                    .aligned()
-                    .constrained()
-                    .with_min_width(theme.search.editor.min_width)
-                    .with_max_width(theme.search.editor.max_width)
-                    .flex(1., false)
-                    .boxed(),
-            )
-            .with_child(
-                Flex::row()
-                    .with_child(self.render_nav_button("<", Direction::Prev, cx))
-                    .with_child(self.render_nav_button(">", Direction::Next, cx))
-                    .aligned()
-                    .boxed(),
-            )
-            .with_child(
-                Flex::row()
-                    .with_children(self.render_search_option(
-                        supported_options.case,
-                        "Case",
-                        SearchOption::CaseSensitive,
-                        cx,
-                    ))
-                    .with_children(self.render_search_option(
-                        supported_options.word,
-                        "Word",
-                        SearchOption::WholeWord,
-                        cx,
-                    ))
-                    .with_children(self.render_search_option(
-                        supported_options.regex,
-                        "Regex",
-                        SearchOption::Regex,
-                        cx,
-                    ))
-                    .contained()
-                    .with_style(theme.search.option_button_group)
-                    .aligned()
+                            .with_children(self.active_searchable_item.as_ref().and_then(
+                                |searchable_item| {
+                                    let matches = self
+                                        .seachable_items_with_matches
+                                        .get(&searchable_item.downgrade())?;
+                                    let message = if let Some(match_ix) = self.active_match_index {
+                                        format!("{}/{}", match_ix + 1, matches.len())
+                                    } else {
+                                        "No matches".to_string()
+                                    };
+
+                                    Some(
+                                        Label::new(message, theme.search.match_index.text.clone())
+                                            .contained()
+                                            .with_style(theme.search.match_index.container)
+                                            .aligned()
+                                            .boxed(),
+                                    )
+                                },
+                            ))
+                            .contained()
+                            .with_style(editor_container)
+                            .aligned()
+                            .constrained()
+                            .with_min_width(theme.search.editor.min_width)
+                            .with_max_width(theme.search.editor.max_width)
+                            .flex(1., false)
+                            .boxed(),
+                    )
+                    .with_child(
+                        Flex::row()
+                            .with_child(self.render_nav_button("<", Direction::Prev, cx))
+                            .with_child(self.render_nav_button(">", Direction::Next, cx))
+                            .aligned()
+                            .boxed(),
+                    )
+                    .with_child(
+                        Flex::row()
+                            .with_children(self.render_search_option(
+                                supported_options.case,
+                                "Case",
+                                SearchOption::CaseSensitive,
+                                cx,
+                            ))
+                            .with_children(self.render_search_option(
+                                supported_options.word,
+                                "Word",
+                                SearchOption::WholeWord,
+                                cx,
+                            ))
+                            .with_children(self.render_search_option(
+                                supported_options.regex,
+                                "Regex",
+                                SearchOption::Regex,
+                                cx,
+                            ))
+                            .contained()
+                            .with_style(theme.search.option_button_group)
+                            .aligned()
+                            .boxed(),
+                    )
+                    .flex(1., true)
                     .boxed(),
             )
+            .with_child(self.render_close_button(&theme.search, cx))
             .contained()
             .with_style(theme.search.container)
             .named("search bar")
@@ -325,7 +331,7 @@ impl BufferSearchBar {
         let is_active = self.is_search_option_enabled(option);
         Some(
             MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
-                let style = &cx
+                let style = cx
                     .global::<Settings>()
                     .theme
                     .search
@@ -373,7 +379,7 @@ impl BufferSearchBar {
 
         enum NavButton {}
         MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
-            let style = &cx
+            let style = cx
                 .global::<Settings>()
                 .theme
                 .search
@@ -399,6 +405,38 @@ impl BufferSearchBar {
         .boxed()
     }
 
+    fn render_close_button(
+        &self,
+        theme: &theme::Search,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        let action = Box::new(Dismiss);
+        let tooltip = "Dismiss Buffer Search";
+        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+
+        enum CloseButton {}
+        MouseEventHandler::<CloseButton>::new(0, cx, |state, _| {
+            let style = theme.dismiss_button.style_for(state, false);
+            Svg::new("icons/x_mark_8.svg")
+                .with_color(style.color)
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .contained()
+                .with_style(style.container)
+                .boxed()
+        })
+        .on_click(MouseButton::Left, {
+            let action = action.boxed_clone();
+            move |_, cx| cx.dispatch_any_action(action.boxed_clone())
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .with_tooltip::<CloseButton, _>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
+        .boxed()
+    }
+
     fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
             if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {

crates/settings/src/keymap_file.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{parse_json_with_comments, Settings};
 use anyhow::{Context, Result};
 use assets::Assets;
 use collections::BTreeMap;
-use gpui::{keymap::Binding, MutableAppContext};
+use gpui::{keymap_matcher::Binding, MutableAppContext};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},

crates/snippet/src/snippet.rs πŸ”—

@@ -62,7 +62,7 @@ fn parse_snippet<'a>(
                 }
             }
             Some(_) => {
-                let chunk_end = source.find(&['}', '$', '\\']).unwrap_or(source.len());
+                let chunk_end = source.find(['}', '$', '\\']).unwrap_or(source.len());
                 let (chunk, rest) = source.split_at(chunk_end);
                 text.push_str(chunk);
                 source = rest;

crates/sqlez/src/connection.rs πŸ”—

@@ -20,7 +20,7 @@ unsafe impl Send for Connection {}
 impl Connection {
     pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
         let mut connection = Self {
-            sqlite3: 0 as *mut _,
+            sqlite3: ptr::null_mut(),
             persistent,
             write: RefCell::new(true),
             _sqlite: PhantomData,
@@ -32,7 +32,7 @@ impl Connection {
                 CString::new(uri)?.as_ptr(),
                 &mut connection.sqlite3,
                 flags,
-                0 as *const _,
+                ptr::null(),
             );
 
             // Turn on extended error codes
@@ -97,7 +97,7 @@ impl Connection {
                 let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
                 remaining_sql_str != ";" && !remaining_sql_str.is_empty()
             } {
-                let mut raw_statement = 0 as *mut sqlite3_stmt;
+                let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
                 let mut remaining_sql_ptr = ptr::null();
                 sqlite3_prepare_v2(
                     self.sqlite3,

crates/sqlez/src/statement.rs πŸ”—

@@ -48,7 +48,7 @@ impl<'a> Statement<'a> {
                     .trim();
                 remaining_sql_str != ";" && !remaining_sql_str.is_empty()
             } {
-                let mut raw_statement = 0 as *mut sqlite3_stmt;
+                let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
                 let mut remaining_sql_ptr = ptr::null();
                 sqlite3_prepare_v2(
                     connection.sqlite3,
@@ -101,7 +101,7 @@ impl<'a> Statement<'a> {
         }
     }
 
-    fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt) -> ()) -> Result<()> {
+    fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt)) -> Result<()> {
         let mut any_succeed = false;
         unsafe {
             for raw_statement in self.raw_statements.iter() {
@@ -133,7 +133,7 @@ impl<'a> Statement<'a> {
         })
     }
 
-    pub fn column_blob<'b>(&'b mut self, index: i32) -> Result<&'b [u8]> {
+    pub fn column_blob(&mut self, index: i32) -> Result<&[u8]> {
         let index = index as c_int;
         let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
 
@@ -217,7 +217,7 @@ impl<'a> Statement<'a> {
         })
     }
 
-    pub fn column_text<'b>(&'b mut self, index: i32) -> Result<&'b str> {
+    pub fn column_text(&mut self, index: i32) -> Result<&str> {
         let index = index as c_int;
         let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
 

crates/sqlez/src/thread_safe_connection.rs πŸ”—

@@ -114,12 +114,12 @@ impl<M: Migrator> ThreadSafeConnection<M> {
             let mut queues = QUEUES.write();
             if !queues.contains_key(&self.uri) {
                 let mut write_queue_constructor =
-                    write_queue_constructor.unwrap_or(background_thread_queue());
+                    write_queue_constructor.unwrap_or_else(background_thread_queue);
                 queues.insert(self.uri.clone(), write_queue_constructor());
                 return true;
             }
         }
-        return false;
+        false
     }
 
     pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
@@ -187,10 +187,9 @@ impl<M: Migrator> ThreadSafeConnection<M> {
         *connection.write.get_mut() = false;
 
         if let Some(initialize_query) = connection_initialize_query {
-            connection.exec(initialize_query).expect(&format!(
-                "Initialize query failed to execute: {}",
-                initialize_query
-            ))()
+            connection.exec(initialize_query).unwrap_or_else(|_| {
+                panic!("Initialize query failed to execute: {}", initialize_query)
+            })()
             .unwrap()
         }
 
@@ -225,7 +224,7 @@ impl<M: Migrator> Clone for ThreadSafeConnection<M> {
         Self {
             uri: self.uri.clone(),
             persistent: self.persistent,
-            connection_initialize_query: self.connection_initialize_query.clone(),
+            connection_initialize_query: self.connection_initialize_query,
             connections: self.connections.clone(),
             _migrator: PhantomData,
         }

crates/sqlez/src/typed_statements.rs πŸ”—

@@ -8,7 +8,7 @@ use crate::{
 
 impl Connection {
     pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
-        let mut statement = Statement::prepare(&self, query)?;
+        let mut statement = Statement::prepare(self, query)?;
         Ok(move || statement.exec())
     }
 
@@ -16,7 +16,7 @@ impl Connection {
         &'a self,
         query: &str,
     ) -> Result<impl 'a + FnMut(B) -> Result<()>> {
-        let mut statement = Statement::prepare(&self, query)?;
+        let mut statement = Statement::prepare(self, query)?;
         Ok(move |bindings| statement.with_bindings(bindings)?.exec())
     }
 
@@ -24,7 +24,7 @@ impl Connection {
         &'a self,
         query: &str,
     ) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
-        let mut statement = Statement::prepare(&self, query)?;
+        let mut statement = Statement::prepare(self, query)?;
         Ok(move || statement.rows::<C>())
     }
 
@@ -32,7 +32,7 @@ impl Connection {
         &'a self,
         query: &str,
     ) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
-        let mut statement = Statement::prepare(&self, query)?;
+        let mut statement = Statement::prepare(self, query)?;
         Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
     }
 
@@ -40,7 +40,7 @@ impl Connection {
         &'a self,
         query: &str,
     ) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
-        let mut statement = Statement::prepare(&self, query)?;
+        let mut statement = Statement::prepare(self, query)?;
         Ok(move || statement.maybe_row::<C>())
     }
 
@@ -48,7 +48,7 @@ impl Connection {
         &'a self,
         query: &str,
     ) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
-        let mut statement = Statement::prepare(&self, query)?;
+        let mut statement = Statement::prepare(self, query)?;
         Ok(move |bindings| {
             statement
                 .with_bindings(bindings)

crates/sqlez_macros/src/sqlez_macros.rs πŸ”—

@@ -33,14 +33,14 @@ fn create_error(
         .skip_while(|(offset, _)| offset <= &error_offset)
         .map(|(_, span)| span)
         .next()
-        .unwrap_or(Span::call_site());
+        .unwrap_or_else(Span::call_site);
     let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql);
     TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error())
 }
 
 fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) {
     let mut sql_tokens = vec![];
-    flatten_stream(tokens.clone(), &mut sql_tokens);
+    flatten_stream(tokens, &mut sql_tokens);
     // Lookup of spans by offset at the end of the token
     let mut spans: Vec<(usize, Span)> = Vec::new();
     let mut sql = String::new();
@@ -67,7 +67,7 @@ fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) {
                 result.push((close_delimiter(group.delimiter()), group.span()));
             }
             TokenTree::Ident(ident) => {
-                result.push((format!("{} ", ident.to_string()), ident.span()));
+                result.push((format!("{} ", ident), ident.span()));
             }
             leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())),
         }

crates/sum_tree/src/tree_map.rs πŸ”—

@@ -58,7 +58,7 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
         self.0.insert_or_replace(MapEntry { key, value }, &());
     }
 
-    pub fn remove<'a>(&mut self, key: &'a K) -> Option<V> {
+    pub fn remove(&mut self, key: &K) -> Option<V> {
         let mut removed = None;
         let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
         let key = MapKeyRef(Some(key));

crates/terminal/src/mappings/keys.rs πŸ”—

@@ -1,6 +1,6 @@
 /// The mappings defined in this file where created from reading the alacritty source
 use alacritty_terminal::term::TermMode;
-use gpui::keymap::Keystroke;
+use gpui::keymap_matcher::Keystroke;
 
 #[derive(Debug, PartialEq, Eq)]
 pub enum Modifiers {
@@ -273,6 +273,8 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
 
 #[cfg(test)]
 mod test {
+    use gpui::keymap_matcher::Keystroke;
+
     use super::*;
 
     #[test]

crates/terminal/src/terminal.rs πŸ”—

@@ -50,7 +50,7 @@ use thiserror::Error;
 
 use gpui::{
     geometry::vector::{vec2f, Vector2F},
-    keymap::Keystroke,
+    keymap_matcher::Keystroke,
     scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
     ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
 };

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -14,7 +14,7 @@ use gpui::{
     elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
     geometry::vector::Vector2F,
     impl_actions, impl_internal_actions,
-    keymap::Keystroke,
+    keymap_matcher::{KeymapContext, Keystroke},
     AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
     View, ViewContext, ViewHandle, WeakViewHandle,
 };
@@ -465,7 +465,7 @@ impl View for TerminalView {
         });
     }
 
-    fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
+    fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext {
         let mut context = Self::default_keymap_context();
 
         let mode = self.terminal.read(cx).last_content.mode;

crates/theme/src/theme.rs πŸ”—

@@ -247,6 +247,7 @@ pub struct Search {
     pub results_status: TextStyle,
     pub tab_icon_width: f32,
     pub tab_icon_spacing: f32,
+    pub dismiss_button: Interactive<IconButton>,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/util/src/channel.rs πŸ”—

@@ -4,7 +4,7 @@ use lazy_static::lazy_static;
 
 lazy_static! {
     pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
-        .unwrap_or(include_str!("../../zed/RELEASE_CHANNEL").to_string());
+        .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
     pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
         "dev" => ReleaseChannel::Dev,
         "preview" => ReleaseChannel::Preview,

crates/util/src/lib.rs πŸ”—

@@ -36,7 +36,7 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
     debug_assert!(max_chars >= 5);
 
     if s.len() > max_chars {
-        format!("{}…", truncate(&s, max_chars.saturating_sub(3)))
+        format!("{}…", truncate(s, max_chars.saturating_sub(3)))
     } else {
         s.to_string()
     }

crates/vim/src/motion.rs πŸ”—

@@ -3,7 +3,7 @@ use editor::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     movement, Bias, CharKind, DisplayPoint,
 };
-use gpui::{actions, impl_actions, MutableAppContext};
+use gpui::{actions, impl_actions, keymap_matcher::KeyPressed, MutableAppContext};
 use language::{Point, Selection, SelectionGoal};
 use serde::Deserialize;
 use workspace::Workspace;
@@ -32,6 +32,8 @@ pub enum Motion {
     StartOfDocument,
     EndOfDocument,
     Matching,
+    FindForward { before: bool, character: char },
+    FindBackward { after: bool, character: char },
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -107,10 +109,34 @@ pub fn init(cx: &mut MutableAppContext) {
          &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
+    cx.add_action(
+        |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| match Vim::read(cx)
+            .active_operator()
+        {
+            Some(Operator::FindForward { before }) => motion(
+                Motion::FindForward {
+                    before,
+                    character: keystroke.key.chars().next().unwrap(),
+                },
+                cx,
+            ),
+            Some(Operator::FindBackward { after }) => motion(
+                Motion::FindBackward {
+                    after,
+                    character: keystroke.key.chars().next().unwrap(),
+                },
+                cx,
+            ),
+            _ => cx.propagate_action(),
+        },
+    )
 }
 
 pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
-    if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
+    if let Some(Operator::Namespace(_))
+    | Some(Operator::FindForward { .. })
+    | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
+    {
         Vim::update(cx, |vim, cx| vim.pop_operator(cx));
     }
 
@@ -152,14 +178,16 @@ impl Motion {
             | CurrentLine
             | EndOfLine
             | NextWordEnd { .. }
-            | Matching => true,
+            | Matching
+            | FindForward { .. } => true,
             Left
             | Backspace
             | Right
             | StartOfLine
             | NextWordStart { .. }
             | PreviousWordStart { .. }
-            | FirstNonWhitespace => false,
+            | FirstNonWhitespace
+            | FindBackward { .. } => false,
         }
     }
 
@@ -196,6 +224,14 @@ impl Motion {
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
             Matching => (matching(map, point), SelectionGoal::None),
+            FindForward { before, character } => (
+                find_forward(map, point, before, character, times),
+                SelectionGoal::None,
+            ),
+            FindBackward { after, character } => (
+                find_backward(map, point, after, character, times),
+                SelectionGoal::None,
+            ),
         };
 
         (new_point != point || self.infallible()).then_some((new_point, goal))
@@ -446,3 +482,50 @@ fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
         point
     }
 }
+
+fn find_forward(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    before: bool,
+    target: char,
+    mut times: usize,
+) -> DisplayPoint {
+    let mut previous_point = from;
+
+    for (ch, point) in map.chars_at(from) {
+        if ch == target && point != from {
+            times -= 1;
+            if times == 0 {
+                return if before { previous_point } else { point };
+            }
+        } else if ch == '\n' {
+            break;
+        }
+        previous_point = point;
+    }
+
+    from
+}
+
+fn find_backward(
+    map: &DisplaySnapshot,
+    from: DisplayPoint,
+    after: bool,
+    target: char,
+    mut times: usize,
+) -> DisplayPoint {
+    let mut previous_point = from;
+    for (ch, point) in map.reverse_chars_at(from) {
+        if ch == target && point != from {
+            times -= 1;
+            if times == 0 {
+                return if after { previous_point } else { point };
+            }
+        } else if ch == '\n' {
+            break;
+        }
+        previous_point = point;
+    }
+
+    from
+}

crates/vim/src/normal.rs πŸ”—

@@ -18,6 +18,7 @@ use editor::{
 };
 use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
 use language::{AutoindentMode, Point, SelectionGoal};
+use log::error;
 use serde::Deserialize;
 use workspace::Workspace;
 
@@ -101,8 +102,9 @@ pub fn normal_motion(
             Some(Operator::Change) => change_motion(vim, motion, times, cx),
             Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
             Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
-            _ => {
+            Some(operator) => {
                 // Can't do anything for text objects or namespace operators. Ignoring
+                error!("Unexpected normal mode motion operator: {:?}", operator)
             }
         }
     });
@@ -912,4 +914,42 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
         cx.assert_all("TestΛ‡β”œΛ‡β”€β”€Λ‡β”Λ‡Test").await;
     }
+
+    #[gpui::test]
+    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        for count in 1..=3 {
+            let test_case = indoc! {"
+                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
+                ˇ    ˇbˇaaˇa ˇbˇbˇb
+                Λ‡   
+                Λ‡b
+            "};
+
+            cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
+                .await;
+
+            cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
+                .await;
+        }
+    }
+
+    #[gpui::test]
+    async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        for count in 1..=3 {
+            let test_case = indoc! {"
+                ˇaaaˇbˇ ˇbˇ   ˇbˇbˇ aˇaaˇbaaa
+                ˇ    ˇbˇaaˇa ˇbˇbˇb
+                Λ‡   
+                Λ‡b
+            "};
+
+            cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
+                .await;
+
+            cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
+                .await;
+        }
+    }
 }

crates/vim/src/state.rs πŸ”—

@@ -1,4 +1,4 @@
-use gpui::keymap::Context;
+use gpui::keymap_matcher::KeymapContext;
 use language::CursorShape;
 use serde::{Deserialize, Serialize};
 
@@ -29,6 +29,8 @@ pub enum Operator {
     Delete,
     Yank,
     Object { around: bool },
+    FindForward { before: bool },
+    FindBackward { after: bool },
 }
 
 #[derive(Default)]
@@ -54,6 +56,10 @@ impl VimState {
 
     pub fn vim_controlled(&self) -> bool {
         !matches!(self.mode, Mode::Insert)
+            || matches!(
+                self.operator_stack.last(),
+                Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
+            )
     }
 
     pub fn clip_at_line_end(&self) -> bool {
@@ -64,8 +70,8 @@ impl VimState {
         !matches!(self.mode, Mode::Visual { .. })
     }
 
-    pub fn keymap_context_layer(&self) -> Context {
-        let mut context = Context::default();
+    pub fn keymap_context_layer(&self) -> KeymapContext {
+        let mut context = KeymapContext::default();
         context.map.insert(
             "vim_mode".to_string(),
             match self.mode {
@@ -81,34 +87,48 @@ impl VimState {
         }
 
         let active_operator = self.operator_stack.last();
-        if matches!(active_operator, Some(Operator::Object { .. })) {
-            context.set.insert("VimObject".to_string());
+
+        if let Some(active_operator) = active_operator {
+            for context_flag in active_operator.context_flags().into_iter() {
+                context.set.insert(context_flag.to_string());
+            }
         }
 
-        Operator::set_context(active_operator, &mut context);
+        context.map.insert(
+            "vim_operator".to_string(),
+            active_operator
+                .map(|op| op.id())
+                .unwrap_or_else(|| "none")
+                .to_string(),
+        );
 
         context
     }
 }
 
 impl Operator {
-    pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
-        let operator_context = match operator {
-            Some(Operator::Number(_)) => "n",
-            Some(Operator::Namespace(Namespace::G)) => "g",
-            Some(Operator::Namespace(Namespace::Z)) => "z",
-            Some(Operator::Object { around: false }) => "i",
-            Some(Operator::Object { around: true }) => "a",
-            Some(Operator::Change) => "c",
-            Some(Operator::Delete) => "d",
-            Some(Operator::Yank) => "y",
-
-            None => "none",
+    pub fn id(&self) -> &'static str {
+        match self {
+            Operator::Number(_) => "n",
+            Operator::Namespace(Namespace::G) => "g",
+            Operator::Namespace(Namespace::Z) => "z",
+            Operator::Object { around: false } => "i",
+            Operator::Object { around: true } => "a",
+            Operator::Change => "c",
+            Operator::Delete => "d",
+            Operator::Yank => "y",
+            Operator::FindForward { before: false } => "f",
+            Operator::FindForward { before: true } => "t",
+            Operator::FindBackward { after: false } => "F",
+            Operator::FindBackward { after: true } => "T",
         }
-        .to_owned();
+    }
 
-        context
-            .map
-            .insert("vim_operator".to_string(), operator_context);
+    pub fn context_flags(&self) -> &'static [&'static str] {
+        match self {
+            Operator::Object { .. } => &["VimObject"],
+            Operator::FindForward { .. } | Operator::FindBackward { .. } => &["VimWaiting"],
+            _ => &[],
+        }
     }
 }

crates/vim/src/test/neovim_connection.rs πŸ”—

@@ -7,7 +7,7 @@ use async_compat::Compat;
 #[cfg(feature = "neovim")]
 use async_trait::async_trait;
 #[cfg(feature = "neovim")]
-use gpui::keymap::Keystroke;
+use gpui::keymap_matcher::Keystroke;
 
 use language::{Point, Selection};
 

crates/vim/src/vim.rs πŸ”—

@@ -16,7 +16,6 @@ use editor::{Bias, Cancel, Editor};
 use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
 use language::CursorShape;
 use serde::Deserialize;
-
 use settings::Settings;
 use state::{Mode, Operator, VimState};
 use workspace::{self, Workspace};
@@ -55,7 +54,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
     // Editor Actions
     cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
-        // If we are in a non normal mode or have an active operator, swap to normal mode
+        // If we are in aren't in normal mode or have an active operator, swap to normal mode
         // Otherwise forward cancel on to the editor
         let vim = Vim::read(cx);
         if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
@@ -81,17 +80,21 @@ pub fn init(cx: &mut MutableAppContext) {
     .detach();
 }
 
-// Any keystrokes not mapped to vim should clear the active operator
 pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
     cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
         if let Some(handled_by) = handled_by {
-            if handled_by.namespace() == "vim" {
+            // Keystroke is handled by the vim system, so continue forward
+            // Also short circuit if it is the special cancel action
+            if handled_by.namespace() == "vim"
+                || (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
+            {
                 return true;
             }
         }
 
         Vim::update(cx, |vim, cx| {
             if vim.active_operator().is_some() {
+                // If the keystroke is not handled by vim, we should clear the operator
                 vim.clear_operator(cx);
             }
         });

crates/workspace/src/pane.rs πŸ”—

@@ -44,6 +44,8 @@ actions!(
         ActivateLastItem,
         CloseActiveItem,
         CloseInactiveItems,
+        CloseCleanItems,
+        CloseAllItems,
         ReopenClosedItem,
         SplitLeft,
         SplitUp,
@@ -122,6 +124,8 @@ pub fn init(cx: &mut MutableAppContext) {
     });
     cx.add_async_action(Pane::close_active_item);
     cx.add_async_action(Pane::close_inactive_items);
+    cx.add_async_action(Pane::close_clean_items);
+    cx.add_async_action(Pane::close_all_items);
     cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
         let pane = action.pane.upgrade(cx)?;
         let task = Pane::close_item(workspace, pane, action.item_id, cx);
@@ -258,6 +262,13 @@ pub enum ReorderBehavior {
     MoveToIndex(usize),
 }
 
+enum ItemType {
+    Active,
+    Inactive,
+    Clean,
+    All,
+}
+
 impl Pane {
     pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
         let handle = cx.weak_handle();
@@ -696,40 +707,67 @@ impl Pane {
         _: &CloseActiveItem,
         cx: &mut ViewContext<Workspace>,
     ) -> Option<Task<Result<()>>> {
-        let pane_handle = workspace.active_pane().clone();
-        let pane = pane_handle.read(cx);
-        if pane.items.is_empty() {
-            None
-        } else {
-            let item_id_to_close = pane.items[pane.active_item_index].id();
-            let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
-                item_id == item_id_to_close
-            });
-            Some(cx.foreground().spawn(async move {
-                task.await?;
-                Ok(())
-            }))
-        }
+        Self::close_main(workspace, ItemType::Active, cx)
     }
 
     pub fn close_inactive_items(
         workspace: &mut Workspace,
         _: &CloseInactiveItems,
         cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        Self::close_main(workspace, ItemType::Inactive, cx)
+    }
+
+    pub fn close_all_items(
+        workspace: &mut Workspace,
+        _: &CloseAllItems,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        Self::close_main(workspace, ItemType::All, cx)
+    }
+
+    pub fn close_clean_items(
+        workspace: &mut Workspace,
+        _: &CloseCleanItems,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        Self::close_main(workspace, ItemType::Clean, cx)
+    }
+
+    fn close_main(
+        workspace: &mut Workspace,
+        close_item_type: ItemType,
+        cx: &mut ViewContext<Workspace>,
     ) -> Option<Task<Result<()>>> {
         let pane_handle = workspace.active_pane().clone();
         let pane = pane_handle.read(cx);
         if pane.items.is_empty() {
-            None
-        } else {
-            let active_item_id = pane.items[pane.active_item_index].id();
-            let task =
-                Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
-            Some(cx.foreground().spawn(async move {
-                task.await?;
-                Ok(())
-            }))
+            return None;
         }
+
+        let active_item_id = pane.items[pane.active_item_index].id();
+        let clean_item_ids: Vec<_> = pane
+            .items()
+            .filter(|item| !item.is_dirty(cx))
+            .map(|item| item.id())
+            .collect();
+        let task =
+            Self::close_items(
+                workspace,
+                pane_handle,
+                cx,
+                move |item_id| match close_item_type {
+                    ItemType::Active => item_id == active_item_id,
+                    ItemType::Inactive => item_id != active_item_id,
+                    ItemType::Clean => clean_item_ids.contains(&item_id),
+                    ItemType::All => true,
+                },
+            );
+
+        Some(cx.foreground().spawn(async move {
+            task.await?;
+            Ok(())
+        }))
     }
 
     pub fn close_item(

crates/workspace/src/workspace.rs πŸ”—

@@ -33,6 +33,7 @@ use gpui::{
     actions,
     elements::*,
     impl_actions, impl_internal_actions,
+    keymap_matcher::KeymapContext,
     platform::{CursorStyle, WindowOptions},
     AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
@@ -95,7 +96,7 @@ actions!(
         ToggleLeftSidebar,
         ToggleRightSidebar,
         NewTerminal,
-        NewSearch,
+        NewSearch
     ]
 );
 
@@ -2142,7 +2143,6 @@ impl Workspace {
         let call = self.active_call()?;
         let room = call.read(cx).room()?.read(cx);
         let participant = room.remote_participant_for_peer_id(leader_id)?;
-
         let mut items_to_add = Vec::new();
         match participant.location {
             call::ParticipantLocation::SharedProject { project_id } => {
@@ -2153,6 +2153,12 @@ impl Workspace {
                             .and_then(|id| state.items_by_leader_view_id.get(&id))
                         {
                             items_to_add.push((pane.clone(), item.boxed_clone()));
+                        } else {
+                            if let Some(shared_screen) =
+                                self.shared_screen_for_peer(leader_id, pane, cx)
+                            {
+                                items_to_add.push((pane.clone(), Box::new(shared_screen)));
+                            }
                         }
                     }
                 }
@@ -2588,7 +2594,7 @@ impl View for Workspace {
         }
     }
 
-    fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut keymap = Self::default_keymap_context();
         if self.active_pane() == self.dock_pane() {
             keymap.set.insert("Dock".into());

crates/zed/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.68.0"
+version = "0.69.0"
 
 [lib]
 name = "zed"
@@ -30,6 +30,7 @@ clock = { path = "../clock" }
 diagnostics = { path = "../diagnostics" }
 editor = { path = "../editor" }
 file_finder = { path = "../file_finder" }
+human_bytes = "0.4.1"
 search = { path = "../search" }
 fs = { path = "../fs" }
 fsevent = { path = "../fsevent" }
@@ -48,6 +49,7 @@ recent_projects = { path = "../recent_projects" }
 rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
+sysinfo = "0.27.1"
 text = { path = "../text" }
 terminal_view = { path = "../terminal_view" }
 theme = { path = "../theme" }
@@ -108,6 +110,7 @@ tree-sitter-html = "0.19.0"
 tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
 tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
 url = "2.2"
+urlencoding = "2.1.2"
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }

crates/zed/src/languages/ruby.rs πŸ”—

@@ -50,14 +50,14 @@ impl LspAdapter for RubyLanguageServer {
                 grammar.highlight_id_for_name("type")?
             }
             lsp::CompletionItemKind::KEYWORD => {
-                if label.starts_with(":") {
+                if label.starts_with(':') {
                     grammar.highlight_id_for_name("string.special.symbol")?
                 } else {
                     grammar.highlight_id_for_name("keyword")?
                 }
             }
             lsp::CompletionItemKind::VARIABLE => {
-                if label.starts_with("@") {
+                if label.starts_with('@') {
                     grammar.highlight_id_for_name("property")?
                 } else {
                     return None;

crates/zed/src/languages/typescript.rs πŸ”—

@@ -128,8 +128,14 @@ impl LspAdapter for TypeScriptLspAdapter {
             Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
             _ => None,
         }?;
+
+        let text = match &item.detail {
+            Some(detail) => format!("{} {}", item.label, detail),
+            None => item.label.clone(),
+        };
+
         Some(language::CodeLabel {
-            text: item.label.clone(),
+            text,
             runs: vec![(0..len, highlight_id)],
             filter_range: 0..len,
         })

crates/zed/src/main.rs πŸ”—

@@ -101,7 +101,7 @@ fn main() {
 
         //Setup settings global before binding actions
         cx.set_global(SettingsFile::new(
-            &*paths::SETTINGS,
+            &paths::SETTINGS,
             settings_file_content.clone(),
             fs.clone(),
         ));
@@ -586,7 +586,7 @@ async fn handle_cli_connection(
 
                 responses
                     .send(CliResponse::Exit {
-                        status: if errored { 1 } else { 0 },
+                        status: i32::from(errored),
                     })
                     .log_err();
             }

crates/zed/src/menus.rs πŸ”—

@@ -339,15 +339,22 @@ pub fn menus() -> Vec<Menu<'static>> {
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
-                    name: "Documentation",
-                    action: Box::new(crate::OpenBrowser {
-                        url: "https://zed.dev/docs".into(),
-                    }),
+                    name: "Copy System Specs Into Clipboard",
+                    action: Box::new(crate::CopySystemSpecsIntoClipboard),
+                },
+                MenuItem::Action {
+                    name: "File Bug Report",
+                    action: Box::new(crate::FileBugReport),
                 },
                 MenuItem::Action {
-                    name: "Give Feedback",
+                    name: "Request Feature",
+                    action: Box::new(crate::RequestFeature),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Documentation",
                     action: Box::new(crate::OpenBrowser {
-                        url: super::feedback::NEW_ISSUE_URL.into(),
+                        url: "https://zed.dev/docs".into(),
                     }),
                 },
                 MenuItem::Action {

crates/zed/src/system_specs.rs πŸ”—

@@ -0,0 +1,52 @@
+use std::{env, fmt::Display};
+
+use gpui::AppContext;
+use human_bytes::human_bytes;
+use sysinfo::{System, SystemExt};
+use util::channel::ReleaseChannel;
+
+pub struct SystemSpecs {
+    app_version: &'static str,
+    release_channel: &'static str,
+    os_name: &'static str,
+    os_version: Option<String>,
+    memory: u64,
+    architecture: &'static str,
+}
+
+impl SystemSpecs {
+    pub fn new(cx: &AppContext) -> Self {
+        let platform = cx.platform();
+        let system = System::new_all();
+
+        SystemSpecs {
+            app_version: env!("CARGO_PKG_VERSION"),
+            release_channel: cx.global::<ReleaseChannel>().dev_name(),
+            os_name: platform.os_name(),
+            os_version: platform
+                .os_version()
+                .ok()
+                .map(|os_version| os_version.to_string()),
+            memory: system.total_memory(),
+            architecture: env::consts::ARCH,
+        }
+    }
+}
+
+impl Display for SystemSpecs {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let os_information = match &self.os_version {
+            Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
+            None => format!("OS: {}", self.os_name),
+        };
+        let system_specs = [
+            format!("Zed: {} ({})", self.app_version, self.release_channel),
+            os_information,
+            format!("Memory: {}", human_bytes(self.memory as f64)),
+            format!("Architecture: {}", self.architecture),
+        ]
+        .join("\n");
+
+        write!(f, "{system_specs}")
+    }
+}

crates/zed/src/zed.rs πŸ”—

@@ -1,6 +1,7 @@
 mod feedback;
 pub mod languages;
 pub mod menus;
+pub mod system_specs;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
@@ -21,7 +22,7 @@ use gpui::{
     },
     impl_actions,
     platform::{WindowBounds, WindowOptions},
-    AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
+    AssetSource, AsyncAppContext, ClipboardItem, TitlebarOptions, ViewContext, WindowKind,
 };
 use language::Rope;
 use lazy_static::lazy_static;
@@ -33,6 +34,7 @@ use serde::Deserialize;
 use serde_json::to_string_pretty;
 use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
 use std::{env, path::Path, str, sync::Arc};
+use system_specs::SystemSpecs;
 use util::{channel::ReleaseChannel, paths, ResultExt};
 pub use workspace;
 use workspace::{sidebar::SidebarSide, AppState, Workspace};
@@ -67,6 +69,9 @@ actions!(
         ResetBufferFontSize,
         InstallCommandLineInterface,
         ResetDatabase,
+        CopySystemSpecsIntoClipboard,
+        RequestFeature,
+        FileBugReport
     ]
 );
 
@@ -245,6 +250,41 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
         },
     );
 
+    cx.add_action(
+        |_: &mut Workspace, _: &CopySystemSpecsIntoClipboard, cx: &mut ViewContext<Workspace>| {
+            let system_specs = SystemSpecs::new(cx).to_string();
+            let item = ClipboardItem::new(system_specs.clone());
+            cx.prompt(
+                gpui::PromptLevel::Info,
+                &format!("Copied into clipboard:\n\n{system_specs}"),
+                &["OK"],
+            );
+            cx.write_to_clipboard(item);
+        },
+    );
+
+    cx.add_action(
+        |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
+            let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
+            cx.dispatch_action(OpenBrowser {
+                url: url.into(),
+            });
+        },
+    );
+
+    cx.add_action(
+        |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
+            let system_specs_text = SystemSpecs::new(cx).to_string();
+            let url = format!(
+                "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}", 
+                urlencoding::encode(&system_specs_text)
+            );
+            cx.dispatch_action(OpenBrowser {
+                url: url.into(),
+            });
+        },
+    );
+
     activity_indicator::init(cx);
     call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
     settings::KeymapFileContent::load_defaults(cx);
@@ -298,11 +338,11 @@ pub fn initialize_workspace(
                 },
                 "schemas": [
                     {
-                        "fileMatch": [schema_file_match(&*paths::SETTINGS)],
+                        "fileMatch": [schema_file_match(&paths::SETTINGS)],
                         "schema": settings_file_json_schema(theme_names, language_names),
                     },
                     {
-                        "fileMatch": [schema_file_match(&*paths::KEYMAP)],
+                        "fileMatch": [schema_file_match(&paths::KEYMAP)],
                         "schema": keymap_file_json_schema(&action_names),
                     }
                 ]
@@ -606,7 +646,7 @@ fn open_bundled_config_file(
     cx: &mut ViewContext<Workspace>,
 ) {
     workspace
-        .with_local_workspace(&app_state.clone(), cx, |workspace, cx| {
+        .with_local_workspace(&app_state, cx, |workspace, cx| {
             let project = workspace.project().clone();
             let buffer = project.update(cx, |project, cx| {
                 let text = Assets::get(asset_path).unwrap().data;

styles/src/styleTree/contactNotification.ts πŸ”—

@@ -32,13 +32,13 @@ export default function contactNotification(colorScheme: ColorScheme): Object {
       },
     },
     dismissButton: {
-      color: foreground(layer, "on"),
+      color: foreground(layer, "variant"),
       iconWidth: 8,
       iconHeight: 8,
       buttonWidth: 8,
       buttonHeight: 8,
       hover: {
-        color: foreground(layer, "on", "hovered"),
+        color: foreground(layer, "hovered"),
       },
     },
   };

styles/src/styleTree/editor.ts πŸ”—

@@ -257,7 +257,6 @@ export default function editor(colorScheme: ColorScheme) {
         right: 6,
       },
       hover: {
-        color: foreground(layer, "on", "hovered"),
         background: background(layer, "on", "hovered"),
       },
     },

styles/src/styleTree/search.ts πŸ”—

@@ -80,5 +80,17 @@ export default function search(colorScheme: ColorScheme) {
       ...text(layer, "mono", "on"),
       size: 18,
     },
+    dismissButton: {
+      color: foreground(layer, "variant"),
+      iconWidth: 12,
+      buttonWidth: 14,
+      padding: {
+        left: 10,
+        right: 10,
+      },
+      hover: {
+        color: foreground(layer, "hovered"),
+      },
+    },
   };
 }

styles/src/styleTree/tabBar.ts πŸ”—

@@ -26,7 +26,7 @@ export default function tabBar(colorScheme: ColorScheme) {
     // Close icons
     iconWidth: 8,
     iconClose: foreground(layer, "variant"),
-    iconCloseActive: foreground(layer),
+    iconCloseActive: foreground(layer, "hovered"),
 
     // Indicators
     iconConflict: foreground(layer, "warning"),