Uncomment last tests (#4009)

Mikayla Maki created

With this, we're done all known things that need porting. 

Release Notes:

-

Change summary

crates/feedback/src/feedback_modal.rs       |  41 -
crates/gpui/src/action.rs                   |   2 
crates/gpui/src/keymap/matcher.rs           | 745 +++++++++++-----------
crates/gpui/src/platform/mac/text_system.rs | 212 +----
crates/gpui/src/window.rs                   |   3 
crates/workspace/src/pane.rs                |   4 
crates/workspace/src/workspace.rs           | 766 ++++++++++++----------
7 files changed, 833 insertions(+), 940 deletions(-)

Detailed changes

crates/feedback/src/feedback_modal.rs πŸ”—

@@ -525,43 +525,4 @@ impl Render for FeedbackModal {
     }
 }
 
-// TODO: Testing of various button states, dismissal prompts, etc.
-
-// #[cfg(test)]
-// mod test {
-//     use super::*;
-
-//     #[test]
-//     fn test_invalid_email_addresses() {
-//         let markdown = markdown.await.log_err();
-//         let buffer = project.update(&mut cx, |project, cx| {
-//             project.create_buffer("", markdown, cx)
-//         })??;
-
-//         workspace.update(&mut cx, |workspace, cx| {
-//             let system_specs = SystemSpecs::new(cx);
-
-//             workspace.toggle_modal(cx, move |cx| {
-//                 let feedback_modal = FeedbackModal::new(system_specs, project, buffer, cx);
-
-//                 assert!(!feedback_modal.can_submit());
-//                 assert!(!feedback_modal.valid_email_address(cx));
-//                 assert!(!feedback_modal.valid_character_count());
-
-//                 feedback_modal
-//                     .email_address_editor
-//                     .update(cx, |this, cx| this.set_text("a", cx));
-//                 feedback_modal.set_submission_state(cx);
-
-//                 assert!(!feedback_modal.valid_email_address(cx));
-
-//                 feedback_modal
-//                     .email_address_editor
-//                     .update(cx, |this, cx| this.set_text("a&b.com", cx));
-//                 feedback_modal.set_submission_state(cx);
-
-//                 assert!(feedback_modal.valid_email_address(cx));
-//             });
-//         })?;
-//     }
-// }
+// TODO: Testing of various button states, dismissal prompts, etc. :)

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

@@ -170,7 +170,7 @@ impl ActionRegistry {
 macro_rules! actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
         $(
-            #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::private::serde_derive::Deserialize)]
+            #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
             #[serde(crate = "gpui::private::serde")]
             pub struct $name;
 

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

@@ -28,11 +28,11 @@ impl KeystrokeMatcher {
 
     /// Pushes a keystroke onto the matcher.
     /// The result of the new keystroke is returned:
-    ///     KeyMatch::None =>
+    /// - KeyMatch::None =>
     ///         No match is valid for this key given any pending keystrokes.
-    ///     KeyMatch::Pending =>
+    /// - KeyMatch::Pending =>
     ///         There exist bindings which are still waiting for more keys.
-    ///     KeyMatch::Complete(matches) =>
+    /// - KeyMatch::Complete(matches) =>
     ///         One or more bindings have received the necessary key presses.
     ///         Bindings added later will take precedence over earlier bindings.
     pub fn match_keystroke(
@@ -77,12 +77,10 @@ impl KeystrokeMatcher {
 
         if let Some(pending_key) = pending_key {
             self.pending_keystrokes.push(pending_key);
-        }
-
-        if self.pending_keystrokes.is_empty() {
-            KeyMatch::None
-        } else {
             KeyMatch::Pending
+        } else {
+            self.pending_keystrokes.clear();
+            KeyMatch::None
         }
     }
 }
@@ -98,367 +96,374 @@ impl KeyMatch {
     pub fn is_some(&self) -> bool {
         matches!(self, KeyMatch::Some(_))
     }
+
+    pub fn matches(self) -> Option<Vec<Box<dyn Action>>> {
+        match self {
+            KeyMatch::Some(matches) => Some(matches),
+            _ => None,
+        }
+    }
+}
+
+impl PartialEq for KeyMatch {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (KeyMatch::None, KeyMatch::None) => true,
+            (KeyMatch::Pending, KeyMatch::Pending) => true,
+            (KeyMatch::Some(a), KeyMatch::Some(b)) => {
+                if a.len() != b.len() {
+                    return false;
+                }
+
+                for (a, b) in a.iter().zip(b.iter()) {
+                    if !a.partial_eq(b.as_ref()) {
+                        return false;
+                    }
+                }
+
+                true
+            }
+            _ => false,
+        }
+    }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use anyhow::Result;
-//     use serde::Deserialize;
-
-//     use crate::{actions, impl_actions, keymap_matcher::ActionContext};
-
-//     use super::*;
-
-//     #[test]
-//     fn test_keymap_and_view_ordering() -> Result<()> {
-//         actions!(test, [EditorAction, ProjectPanelAction]);
-
-//         let mut editor = ActionContext::default();
-//         editor.add_identifier("Editor");
-
-//         let mut project_panel = ActionContext::default();
-//         project_panel.add_identifier("ProjectPanel");
-
-//         // Editor 'deeper' in than project panel
-//         let dispatch_path = vec![(2, editor), (1, project_panel)];
-
-//         // But editor actions 'higher' up in keymap
-//         let keymap = Keymap::new(vec![
-//             Binding::new("left", EditorAction, Some("Editor")),
-//             Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
-//         ]);
-
-//         let mut matcher = KeymapMatcher::new(keymap);
-
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
-//             KeyMatch::Matches(vec![
-//                 (2, Box::new(EditorAction)),
-//                 (1, Box::new(ProjectPanelAction)),
-//             ]),
-//         );
-
-//         Ok(())
-//     }
-
-//     #[test]
-//     fn test_push_keystroke() -> Result<()> {
-//         actions!(test, [B, AB, C, D, DA, E, EF]);
-
-//         let mut context1 = ActionContext::default();
-//         context1.add_identifier("1");
-
-//         let mut context2 = ActionContext::default();
-//         context2.add_identifier("2");
-
-//         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.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
-//             KeyMatch::Pending,
-//         );
-//         // B alone doesn't match because a was pending, so AB is returned instead
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
-//             KeyMatch::Matches(vec![(1, Box::new(AB))]),
-//         );
-//         assert!(!matcher.has_pending_keystrokes());
-
-//         // Without an a prefix, B is dispatched like expected
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
-//             KeyMatch::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.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
-//             KeyMatch::Pending,
-//         );
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
-//             KeyMatch::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.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
-//             KeyMatch::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.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
-//             KeyMatch::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,
-//                 ime_key: None,
-//             }
-//         );
-
-//         assert_eq!(
-//             Keystroke::parse("alt-shift-down")?,
-//             Keystroke {
-//                 key: "down".into(),
-//                 ctrl: false,
-//                 alt: true,
-//                 shift: true,
-//                 cmd: false,
-//                 function: false,
-//                 ime_key: None,
-//             }
-//         );
-
-//         assert_eq!(
-//             Keystroke::parse("shift-cmd--")?,
-//             Keystroke {
-//                 key: "-".into(),
-//                 ctrl: false,
-//                 alt: false,
-//                 shift: true,
-//                 cmd: true,
-//                 function: false,
-//                 ime_key: None,
-//             }
-//         );
-
-//         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() {
-//         let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
-
-//         let mut context = ActionContext::default();
-//         context.add_identifier("a");
-//         assert!(!predicate.eval(&[context]));
-
-//         let mut context = ActionContext::default();
-//         context.add_identifier("a");
-//         context.add_identifier("b");
-//         assert!(predicate.eval(&[context]));
-
-//         let mut context = ActionContext::default();
-//         context.add_identifier("a");
-//         context.add_key("c", "x");
-//         assert!(!predicate.eval(&[context]));
-
-//         let mut context = ActionContext::default();
-//         context.add_identifier("a");
-//         context.add_key("c", "d");
-//         assert!(predicate.eval(&[context]));
-
-//         let predicate = KeymapContextPredicate::parse("!a").unwrap();
-//         assert!(predicate.eval(&[ActionContext::default()]));
-//     }
-
-//     #[test]
-//     fn test_context_child_predicate_eval() {
-//         let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
-//         let contexts = [
-//             context_set(&["e", "f"]),
-//             context_set(&["c", "d"]), // match this context
-//             context_set(&["a", "b"]),
-//         ];
-
-//         assert!(!predicate.eval(&contexts[0..]));
-//         assert!(predicate.eval(&contexts[1..]));
-//         assert!(!predicate.eval(&contexts[2..]));
-
-//         let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
-//         let contexts = [
-//             context_set(&["f"]),
-//             context_set(&["e"]), // only match this context
-//             context_set(&["c"]),
-//             context_set(&["a", "b"]),
-//             context_set(&["e"]),
-//             context_set(&["c", "d"]),
-//             context_set(&["a", "b"]),
-//         ];
-
-//         assert!(!predicate.eval(&contexts[0..]));
-//         assert!(predicate.eval(&contexts[1..]));
-//         assert!(!predicate.eval(&contexts[2..]));
-//         assert!(!predicate.eval(&contexts[3..]));
-//         assert!(!predicate.eval(&contexts[4..]));
-//         assert!(!predicate.eval(&contexts[5..]));
-//         assert!(!predicate.eval(&contexts[6..]));
-
-//         fn context_set(names: &[&str]) -> ActionContext {
-//             let mut keymap = ActionContext::new();
-//             names
-//                 .iter()
-//                 .for_each(|name| keymap.add_identifier(name.to_string()));
-//             keymap
-//         }
-//     }
-
-//     #[test]
-//     fn test_matcher() -> Result<()> {
-//         #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
-//         pub struct A(pub String);
-//         impl_actions!(test, [A]);
-//         actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
-
-//         #[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")),
-//             Binding::new("$", Dollar, Some("a")),
-//             Binding::new("\"", Quote, Some("a")),
-//             Binding::new("alt-s", Ess, Some("a")),
-//             Binding::new("ctrl-`", Backtick, Some("a")),
-//         ]);
-
-//         let mut context_a = ActionContext::default();
-//         context_a.add_identifier("a");
-
-//         let mut context_b = ActionContext::default();
-//         context_b.add_identifier("b");
-
-//         let mut matcher = KeymapMatcher::new(keymap);
-
-//         // Basic match
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
-//         );
-//         matcher.clear_pending();
-
-//         // Multi-keystroke match
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
-//             KeyMatch::Pending
-//         );
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(Ab))])
-//         );
-//         matcher.clear_pending();
-
-//         // Failed matches don't interfere with matching subsequent keys
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
-//             KeyMatch::None
-//         );
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))])
-//         );
-//         matcher.clear_pending();
-
-//         // Pending keystrokes are cleared when the context changes
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
-//             KeyMatch::Pending
-//         );
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
-//             KeyMatch::None
-//         );
-//         matcher.clear_pending();
-
-//         let mut context_c = ActionContext::default();
-//         context_c.add_identifier("c");
-
-//         // Pending keystrokes are maintained per-view
-//         assert_eq!(
-//             matcher.match_keystroke(
-//                 Keystroke::parse("a")?,
-//                 vec![(1, context_b.clone()), (2, context_c.clone())]
-//             ),
-//             KeyMatch::Pending
-//         );
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(Ab))])
-//         );
-
-//         // handle Czech $ (option + 4 key)
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("alt-Γ§->$")?, vec![(1, context_a.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(Dollar))])
-//         );
-
-//         // handle Brazillian quote (quote key then space key)
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(Quote))])
-//         );
-
-//         // handle ctrl+` on a brazillian keyboard
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(Backtick))])
-//         );
-
-//         // handle alt-s on a US keyboard
-//         assert_eq!(
-//             matcher.match_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
-//             KeyMatch::Matches(vec![(1, Box::new(Ess))])
-//         );
-
-//         Ok(())
-//     }
-// }
+#[cfg(test)]
+mod tests {
+
+    use serde_derive::Deserialize;
+
+    use super::*;
+    use crate::{self as gpui, KeyBindingContextPredicate, Modifiers};
+    use crate::{actions, KeyBinding};
+
+    #[test]
+    fn test_keymap_and_view_ordering() {
+        actions!(test, [EditorAction, ProjectPanelAction]);
+
+        let mut editor = KeyContext::default();
+        editor.add("Editor");
+
+        let mut project_panel = KeyContext::default();
+        project_panel.add("ProjectPanel");
+
+        // Editor 'deeper' in than project panel
+        let dispatch_path = vec![project_panel, editor];
+
+        // But editor actions 'higher' up in keymap
+        let keymap = Keymap::new(vec![
+            KeyBinding::new("left", EditorAction, Some("Editor")),
+            KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")),
+        ]);
+
+        let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
+
+        let matches = matcher
+            .match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path)
+            .matches()
+            .unwrap();
+
+        assert!(matches[0].partial_eq(&EditorAction));
+        assert!(matches.get(1).is_none());
+    }
+
+    #[test]
+    fn test_multi_keystroke_match() {
+        actions!(test, [B, AB, C, D, DA, E, EF]);
+
+        let mut context1 = KeyContext::default();
+        context1.add("1");
+
+        let mut context2 = KeyContext::default();
+        context2.add("2");
+
+        let dispatch_path = vec![context2, context1];
+
+        let keymap = Keymap::new(vec![
+            KeyBinding::new("a b", AB, Some("1")),
+            KeyBinding::new("b", B, Some("2")),
+            KeyBinding::new("c", C, Some("2")),
+            KeyBinding::new("d", D, Some("1")),
+            KeyBinding::new("d", D, Some("2")),
+            KeyBinding::new("d a", DA, Some("2")),
+        ]);
+
+        let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
+
+        // Binding with pending prefix always takes precedence
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
+            KeyMatch::Pending,
+        );
+        // B alone doesn't match because a was pending, so AB is returned instead
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path),
+            KeyMatch::Some(vec![Box::new(AB)]),
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        // Without an a prefix, B is dispatched like expected
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]),
+            KeyMatch::Some(vec![Box::new(B)]),
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        eprintln!("PROBLEM AREA");
+        // If a is prefixed, C will not be dispatched because there
+        // was a pending binding for it
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
+            KeyMatch::Pending,
+        );
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path),
+            KeyMatch::None,
+        );
+        assert!(!matcher.has_pending_keystrokes());
+
+        // If a single keystroke matches multiple bindings in the tree
+        // only one of them is returned.
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path),
+            KeyMatch::Some(vec![Box::new(D)]),
+        );
+    }
+
+    #[test]
+    fn test_keystroke_parsing() {
+        assert_eq!(
+            Keystroke::parse("ctrl-p").unwrap(),
+            Keystroke {
+                key: "p".into(),
+                modifiers: Modifiers {
+                    control: true,
+                    alt: false,
+                    shift: false,
+                    command: false,
+                    function: false,
+                },
+                ime_key: None,
+            }
+        );
+
+        assert_eq!(
+            Keystroke::parse("alt-shift-down").unwrap(),
+            Keystroke {
+                key: "down".into(),
+                modifiers: Modifiers {
+                    control: false,
+                    alt: true,
+                    shift: true,
+                    command: false,
+                    function: false,
+                },
+                ime_key: None,
+            }
+        );
+
+        assert_eq!(
+            Keystroke::parse("shift-cmd--").unwrap(),
+            Keystroke {
+                key: "-".into(),
+                modifiers: Modifiers {
+                    control: false,
+                    alt: false,
+                    shift: true,
+                    command: true,
+                    function: false,
+                },
+                ime_key: None,
+            }
+        );
+    }
+
+    #[test]
+    fn test_context_predicate_parsing() {
+        use KeyBindingContextPredicate::*;
+
+        assert_eq!(
+            KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
+            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!(
+            KeyBindingContextPredicate::parse("!a").unwrap(),
+            Not(Box::new(Identifier("a".into())),)
+        );
+    }
+
+    #[test]
+    fn test_context_predicate_eval() {
+        let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap();
+
+        let mut context = KeyContext::default();
+        context.add("a");
+        assert!(!predicate.eval(&[context]));
+
+        let mut context = KeyContext::default();
+        context.add("a");
+        context.add("b");
+        assert!(predicate.eval(&[context]));
+
+        let mut context = KeyContext::default();
+        context.add("a");
+        context.set("c", "x");
+        assert!(!predicate.eval(&[context]));
+
+        let mut context = KeyContext::default();
+        context.add("a");
+        context.set("c", "d");
+        assert!(predicate.eval(&[context]));
+
+        let predicate = KeyBindingContextPredicate::parse("!a").unwrap();
+        assert!(predicate.eval(&[KeyContext::default()]));
+    }
+
+    #[test]
+    fn test_context_child_predicate_eval() {
+        let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap();
+        let contexts = [
+            context_set(&["a", "b"]),
+            context_set(&["c", "d"]), // match this context
+            context_set(&["e", "f"]),
+        ];
+
+        assert!(!predicate.eval(&contexts[..=0]));
+        assert!(predicate.eval(&contexts[..=1]));
+        assert!(!predicate.eval(&contexts[..=2]));
+
+        let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap();
+        let contexts = [
+            context_set(&["a", "b"]),
+            context_set(&["c", "d"]),
+            context_set(&["e"]),
+            context_set(&["a", "b"]),
+            context_set(&["c"]),
+            context_set(&["e"]), // only match this context
+            context_set(&["f"]),
+        ];
+
+        assert!(!predicate.eval(&contexts[..=0]));
+        assert!(!predicate.eval(&contexts[..=1]));
+        assert!(!predicate.eval(&contexts[..=2]));
+        assert!(!predicate.eval(&contexts[..=3]));
+        assert!(!predicate.eval(&contexts[..=4]));
+        assert!(predicate.eval(&contexts[..=5]));
+        assert!(!predicate.eval(&contexts[..=6]));
+
+        fn context_set(names: &[&str]) -> KeyContext {
+            let mut keymap = KeyContext::default();
+            names.iter().for_each(|name| keymap.add(name.to_string()));
+            keymap
+        }
+    }
+
+    #[test]
+    fn test_matcher() {
+        #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+        pub struct A(pub String);
+        impl_actions!(test, [A]);
+        actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
+
+        #[derive(Clone, Debug, Eq, PartialEq)]
+        struct ActionArg {
+            a: &'static str,
+        }
+
+        let keymap = Keymap::new(vec![
+            KeyBinding::new("a", A("x".to_string()), Some("a")),
+            KeyBinding::new("b", B, Some("a")),
+            KeyBinding::new("a b", Ab, Some("a || b")),
+            KeyBinding::new("$", Dollar, Some("a")),
+            KeyBinding::new("\"", Quote, Some("a")),
+            KeyBinding::new("alt-s", Ess, Some("a")),
+            KeyBinding::new("ctrl-`", Backtick, Some("a")),
+        ]);
+
+        let mut context_a = KeyContext::default();
+        context_a.add("a");
+
+        let mut context_b = KeyContext::default();
+        context_b.add("b");
+
+        let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
+
+        // Basic match
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
+            KeyMatch::Some(vec![Box::new(A("x".to_string()))])
+        );
+        matcher.clear_pending();
+
+        // Multi-keystroke match
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]),
+            KeyMatch::Pending
+        );
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
+            KeyMatch::Some(vec![Box::new(Ab)])
+        );
+        matcher.clear_pending();
+
+        // Failed matches don't interfere with matching subsequent keys
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]),
+            KeyMatch::None
+        );
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
+            KeyMatch::Some(vec![Box::new(A("x".to_string()))])
+        );
+        matcher.clear_pending();
+
+        let mut context_c = KeyContext::default();
+        context_c.add("c");
+
+        assert_eq!(
+            matcher.match_keystroke(
+                &Keystroke::parse("a").unwrap(),
+                &[context_c.clone(), context_b.clone()]
+            ),
+            KeyMatch::Pending
+        );
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
+            KeyMatch::Some(vec![Box::new(Ab)])
+        );
+
+        // handle Czech $ (option + 4 key)
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("alt-Γ§->$").unwrap(), &[context_a.clone()]),
+            KeyMatch::Some(vec![Box::new(Dollar)])
+        );
+
+        // handle Brazillian quote (quote key then space key)
+        assert_eq!(
+            matcher.match_keystroke(
+                &Keystroke::parse("space->\"").unwrap(),
+                &[context_a.clone()]
+            ),
+            KeyMatch::Some(vec![Box::new(Quote)])
+        );
+
+        // handle ctrl+` on a brazillian keyboard
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
+            KeyMatch::Some(vec![Box::new(Backtick)])
+        );
+
+        // handle alt-s on a US keyboard
+        assert_eq!(
+            matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]),
+            KeyMatch::Some(vec![Box::new(Ess)])
+        );
+    }
+}

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

@@ -592,169 +592,49 @@ impl From<FontStyle> for FontkitStyle {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::AppContext;
-//     use font_kit::properties::{Style, Weight};
-//     use platform::FontSystem as _;
-
-//     #[crate::test(self, retries = 5)]
-//     fn test_layout_str(_: &mut AppContext) {
-//         // This is failing intermittently on CI and we don't have time to figure it out
-//         let fonts = FontSystem::new();
-//         let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();
-//         let menlo_regular = RunStyle {
-//             font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
-//             color: Default::default(),
-//             underline: Default::default(),
-//         };
-//         let menlo_italic = RunStyle {
-//             font_id: fonts
-//                 .select_font(&menlo, Properties::new().style(Style::Italic))
-//                 .unwrap(),
-//             color: Default::default(),
-//             underline: Default::default(),
-//         };
-//         let menlo_bold = RunStyle {
-//             font_id: fonts
-//                 .select_font(&menlo, Properties::new().weight(Weight::BOLD))
-//                 .unwrap(),
-//             color: Default::default(),
-//             underline: Default::default(),
-//         };
-//         assert_ne!(menlo_regular, menlo_italic);
-//         assert_ne!(menlo_regular, menlo_bold);
-//         assert_ne!(menlo_italic, menlo_bold);
-
-//         let line = fonts.layout_line(
-//             "hello world",
-//             16.0,
-//             &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
-//         );
-//         assert_eq!(line.runs.len(), 3);
-//         assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
-//         assert_eq!(line.runs[0].glyphs.len(), 2);
-//         assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
-//         assert_eq!(line.runs[1].glyphs.len(), 4);
-//         assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
-//         assert_eq!(line.runs[2].glyphs.len(), 5);
-//     }
-
-//     #[test]
-//     fn test_glyph_offsets() -> crate::Result<()> {
-//         let fonts = FontSystem::new();
-//         let zapfino = fonts.load_family("Zapfino", &Default::default())?;
-//         let zapfino_regular = RunStyle {
-//             font_id: fonts.select_font(&zapfino, &Properties::new())?,
-//             color: Default::default(),
-//             underline: Default::default(),
-//         };
-//         let menlo = fonts.load_family("Menlo", &Default::default())?;
-//         let menlo_regular = RunStyle {
-//             font_id: fonts.select_font(&menlo, &Properties::new())?,
-//             color: Default::default(),
-//             underline: Default::default(),
-//         };
-
-//         let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
-//         let line = fonts.layout_line(
-//             text,
-//             16.0,
-//             &[
-//                 (9, zapfino_regular),
-//                 (13, menlo_regular),
-//                 (text.len() - 22, zapfino_regular),
-//             ],
-//         );
-//         assert_eq!(
-//             line.runs
-//                 .iter()
-//                 .flat_map(|r| r.glyphs.iter())
-//                 .map(|g| g.index)
-//                 .collect::<Vec<_>>(),
-//             vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37],
-//         );
-//         Ok(())
-//     }
-
-//     #[test]
-//     #[ignore]
-//     fn test_rasterize_glyph() {
-//         use std::{fs::File, io::BufWriter, path::Path};
-
-//         let fonts = FontSystem::new();
-//         let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap();
-//         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
-//         let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
-
-//         const VARIANTS: usize = 1;
-//         for i in 0..VARIANTS {
-//             let variant = i as f32 / VARIANTS as f32;
-//             let (bounds, bytes) = fonts
-//                 .rasterize_glyph(
-//                     font_id,
-//                     16.0,
-//                     glyph_id,
-//                     vec2f(variant, variant),
-//                     2.,
-//                     RasterizationOptions::Alpha,
-//                 )
-//                 .unwrap();
-
-//             let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
-//             let path = Path::new(&name);
-//             let file = File::create(path).unwrap();
-//             let w = &mut BufWriter::new(file);
-
-//             let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
-//             encoder.set_color(png::ColorType::Grayscale);
-//             encoder.set_depth(png::BitDepth::Eight);
-//             let mut writer = encoder.write_header().unwrap();
-//             writer.write_image_data(&bytes).unwrap();
-//         }
-//     }
-
-//     #[test]
-//     fn test_wrap_line() {
-//         let fonts = FontSystem::new();
-//         let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
-//         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
-
-//         let line = "one two three four five\n";
-//         let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
-//         assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
-
-//         let line = "aaa Ξ±Ξ±Ξ± βœ‹βœ‹βœ‹ πŸŽ‰πŸŽ‰πŸŽ‰\n";
-//         let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
-//         assert_eq!(
-//             wrap_boundaries,
-//             &["aaa Ξ±Ξ±Ξ± ".len(), "aaa Ξ±Ξ±Ξ± βœ‹βœ‹βœ‹ ".len(),]
-//         );
-//     }
-
-//     #[test]
-//     fn test_layout_line_bom_char() {
-//         let fonts = FontSystem::new();
-//         let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
-//         let style = RunStyle {
-//             font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
-//             color: Default::default(),
-//             underline: Default::default(),
-//         };
-
-//         let line = "\u{feff}";
-//         let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
-//         assert_eq!(layout.len, line.len());
-//         assert!(layout.runs.is_empty());
-
-//         let line = "a\u{feff}b";
-//         let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
-//         assert_eq!(layout.len, line.len());
-//         assert_eq!(layout.runs.len(), 1);
-//         assert_eq!(layout.runs[0].glyphs.len(), 2);
-//         assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
-//                                                      // There's no glyph for \u{feff}
-//         assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use crate::{font, px, FontRun, MacTextSystem, PlatformTextSystem};
+
+    #[test]
+    fn test_wrap_line() {
+        let fonts = MacTextSystem::new();
+        let font_id = fonts.font_id(&font("Helvetica")).unwrap();
+
+        let line = "one two three four five\n";
+        let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
+        assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
+
+        let line = "aaa Ξ±Ξ±Ξ± βœ‹βœ‹βœ‹ πŸŽ‰πŸŽ‰πŸŽ‰\n";
+        let wrap_boundaries = fonts.wrap_line(line, font_id, px(16.), px(64.0));
+        assert_eq!(
+            wrap_boundaries,
+            &["aaa Ξ±Ξ±Ξ± ".len(), "aaa Ξ±Ξ±Ξ± βœ‹βœ‹βœ‹ ".len(),]
+        );
+    }
+
+    #[test]
+    fn test_layout_line_bom_char() {
+        let fonts = MacTextSystem::new();
+        let font_id = fonts.font_id(&font("Helvetica")).unwrap();
+        let line = "\u{feff}";
+        let mut style = FontRun {
+            font_id,
+            len: line.len(),
+        };
+
+        let layout = fonts.layout_line(line, px(16.), &[style]);
+        assert_eq!(layout.len, line.len());
+        assert!(layout.runs.is_empty());
+
+        let line = "a\u{feff}b";
+        style.len = line.len();
+        let layout = fonts.layout_line(line, px(16.), &[style]);
+        assert_eq!(layout.len, line.len());
+        assert_eq!(layout.runs.len(), 1);
+        assert_eq!(layout.runs[0].glyphs.len(), 2);
+        assert_eq!(layout.runs[0].glyphs[0].id, 68u32.into()); // a
+                                                               // There's no glyph for \u{feff}
+        assert_eq!(layout.runs[0].glyphs[1].id, 69u32.into()); // b
+    }
+}

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

@@ -1906,7 +1906,8 @@ impl<'a> WindowContext<'a> {
             .platform_window
             .on_should_close(Box::new(move || {
                 this.update(|_, cx| {
-                    // Ensure that the window is removed from the app if it's been closed.
+                    // Ensure that the window is removed from the app if it's been closed
+                    // by always pre-empting the system close event.
                     if f(cx) {
                         cx.remove_window();
                     }

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

@@ -255,8 +255,8 @@ impl Pane {
         let focus_handle = cx.focus_handle();
 
         let subscriptions = vec![
-            cx.on_focus_in(&focus_handle, move |this, cx| this.focus_in(cx)),
-            cx.on_focus_out(&focus_handle, move |this, cx| this.focus_out(cx)),
+            cx.on_focus_in(&focus_handle, Pane::focus_in),
+            cx.on_focus_out(&focus_handle, Pane::focus_out),
         ];
 
         let handle = cx.view().downgrade();

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

@@ -672,7 +672,7 @@ impl Workspace {
             // );
 
             // this.show_notification(1, cx, |cx| {
-            //     cx.build_view(|_cx| {
+            //     cx.new_view(|_cx| {
             //         simple_message_notification::MessageNotification::new(format!("Error:"))
             //             .with_click_message("click here because!")
             //     })
@@ -4363,12 +4363,15 @@ mod tests {
     use std::{cell::RefCell, rc::Rc};
 
     use super::*;
-    use crate::item::{
-        test::{TestItem, TestProjectItem},
-        ItemEvent,
+    use crate::{
+        dock::{test::TestPanel, PanelEvent},
+        item::{
+            test::{TestItem, TestProjectItem},
+            ItemEvent,
+        },
     };
     use fs::FakeFs;
-    use gpui::TestAppContext;
+    use gpui::{px, DismissEvent, TestAppContext, VisualTestContext};
     use project::{Project, ProjectEntryId};
     use serde_json::json;
     use settings::SettingsStore;
@@ -4935,362 +4938,405 @@ mod tests {
         });
     }
 
-    // #[gpui::test]
-    // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
-    //     init_test(cx);
-    //     let fs = FakeFs::new(cx.executor());
-
-    //     let project = Project::test(fs, [], cx).await;
-    //     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
-    //     let panel = workspace.update(cx, |workspace, cx| {
-    //         let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
-    //         workspace.add_panel(panel.clone(), cx);
-
-    //         workspace
-    //             .right_dock()
-    //             .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
-    //         panel
-    //     });
-
-    //     let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-    //     pane.update(cx, |pane, cx| {
-    //         let item = cx.build_view(|cx| TestItem::new(cx));
-    //         pane.add_item(Box::new(item), true, true, None, cx);
-    //     });
-
-    //     // Transfer focus from center to panel
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_panel_focus::<TestPanel>(cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(!panel.is_zoomed(cx));
-    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Transfer focus from panel to center
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_panel_focus::<TestPanel>(cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(!panel.is_zoomed(cx));
-    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Close the dock
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(!workspace.right_dock().read(cx).is_open());
-    //         assert!(!panel.is_zoomed(cx));
-    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Open the dock
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(!panel.is_zoomed(cx));
-    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Focus and zoom panel
-    //     panel.update(cx, |panel, cx| {
-    //         cx.focus_self();
-    //         panel.set_zoomed(true, cx)
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(panel.is_zoomed(cx));
-    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Transfer focus to the center closes the dock
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_panel_focus::<TestPanel>(cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(!workspace.right_dock().read(cx).is_open());
-    //         assert!(panel.is_zoomed(cx));
-    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Transferring focus back to the panel keeps it zoomed
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_panel_focus::<TestPanel>(cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(panel.is_zoomed(cx));
-    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Close the dock while it is zoomed
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx)
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(!workspace.right_dock().read(cx).is_open());
-    //         assert!(panel.is_zoomed(cx));
-    //         assert!(workspace.zoomed.is_none());
-    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Opening the dock, when it's zoomed, retains focus
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx)
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(panel.is_zoomed(cx));
-    //         assert!(workspace.zoomed.is_some());
-    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
-    //     });
-
-    //     // Unzoom and close the panel, zoom the active pane.
-    //     panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx)
-    //     });
-    //     pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
-
-    //     // Opening a dock unzooms the pane.
-    //     workspace.update(cx, |workspace, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx)
-    //     });
-    //     workspace.update(cx, |workspace, cx| {
-    //         let pane = pane.read(cx);
-    //         assert!(!pane.is_zoomed());
-    //         assert!(!pane.focus_handle(cx).is_focused(cx));
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert!(workspace.zoomed.is_none());
-    //     });
-    // }
+    #[gpui::test]
+    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
 
-    // #[gpui::test]
-    // async fn test_panels(cx: &mut gpui::TestAppContext) {
-    //     init_test(cx);
-    //     let fs = FakeFs::new(cx.executor());
-
-    //     let project = Project::test(fs, [], cx).await;
-    //     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
-
-    //     let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
-    //         // Add panel_1 on the left, panel_2 on the right.
-    //         let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
-    //         workspace.add_panel(panel_1.clone(), cx);
-    //         workspace
-    //             .left_dock()
-    //             .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
-    //         let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
-    //         workspace.add_panel(panel_2.clone(), cx);
-    //         workspace
-    //             .right_dock()
-    //             .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
-    //         let left_dock = workspace.left_dock();
-    //         assert_eq!(
-    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
-    //             panel_1.panel_id()
-    //         );
-    //         assert_eq!(
-    //             left_dock.read(cx).active_panel_size(cx).unwrap(),
-    //             panel_1.size(cx)
-    //         );
-
-    //         left_dock.update(cx, |left_dock, cx| {
-    //             left_dock.resize_active_panel(Some(1337.), cx)
-    //         });
-    //         assert_eq!(
-    //             workspace
-    //                 .right_dock()
-    //                 .read(cx)
-    //                 .visible_panel()
-    //                 .unwrap()
-    //                 .panel_id(),
-    //             panel_2.panel_id(),
-    //         );
-
-    //         (panel_1, panel_2)
-    //     });
-
-    //     // Move panel_1 to the right
-    //     panel_1.update(cx, |panel_1, cx| {
-    //         panel_1.set_position(DockPosition::Right, cx)
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
-    //         // Since it was the only panel on the left, the left dock should now be closed.
-    //         assert!(!workspace.left_dock().read(cx).is_open());
-    //         assert!(workspace.left_dock().read(cx).visible_panel().is_none());
-    //         let right_dock = workspace.right_dock();
-    //         assert_eq!(
-    //             right_dock.read(cx).visible_panel().unwrap().panel_id(),
-    //             panel_1.panel_id()
-    //         );
-    //         assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
-
-    //         // Now we move panel_2Β to the left
-    //         panel_2.set_position(DockPosition::Left, cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         // Since panel_2 was not visible on the right, we don't open the left dock.
-    //         assert!(!workspace.left_dock().read(cx).is_open());
-    //         // And the right dock is unaffected in it's displaying of panel_1
-    //         assert!(workspace.right_dock().read(cx).is_open());
-    //         assert_eq!(
-    //             workspace
-    //                 .right_dock()
-    //                 .read(cx)
-    //                 .visible_panel()
-    //                 .unwrap()
-    //                 .panel_id(),
-    //             panel_1.panel_id(),
-    //         );
-    //     });
-
-    //     // Move panel_1 back to the left
-    //     panel_1.update(cx, |panel_1, cx| {
-    //         panel_1.set_position(DockPosition::Left, cx)
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
-    //         let left_dock = workspace.left_dock();
-    //         assert!(left_dock.read(cx).is_open());
-    //         assert_eq!(
-    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
-    //             panel_1.panel_id()
-    //         );
-    //         assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
-    //         // And right the dock should be closed as it no longer has any panels.
-    //         assert!(!workspace.right_dock().read(cx).is_open());
-
-    //         // Now we move panel_1 to the bottom
-    //         panel_1.set_position(DockPosition::Bottom, cx);
-    //     });
-
-    //     workspace.update(cx, |workspace, cx| {
-    //         // Since panel_1 was visible on the left, we close the left dock.
-    //         assert!(!workspace.left_dock().read(cx).is_open());
-    //         // The bottom dock is sized based on the panel's default size,
-    //         // since the panel orientation changed from vertical to horizontal.
-    //         let bottom_dock = workspace.bottom_dock();
-    //         assert_eq!(
-    //             bottom_dock.read(cx).active_panel_size(cx).unwrap(),
-    //             panel_1.size(cx),
-    //         );
-    //         // Close bottom dock and move panel_1 back to the left.
-    //         bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
-    //         panel_1.set_position(DockPosition::Left, cx);
-    //     });
-
-    //     // Emit activated event on panel 1
-    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
-
-    //     // Now the left dock is open and panel_1 is active and focused.
-    //     workspace.update(cx, |workspace, cx| {
-    //         let left_dock = workspace.left_dock();
-    //         assert!(left_dock.read(cx).is_open());
-    //         assert_eq!(
-    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
-    //             panel_1.panel_id(),
-    //         );
-    //         assert!(panel_1.focus_handle(cx).is_focused(cx));
-    //     });
-
-    //     // Emit closed event on panel 2, which is not active
-    //     panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
-
-    //     // Wo don't close the left dock, because panel_2 wasn't the active panel
-    //     workspace.update(cx, |workspace, cx| {
-    //         let left_dock = workspace.left_dock();
-    //         assert!(left_dock.read(cx).is_open());
-    //         assert_eq!(
-    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
-    //             panel_1.panel_id(),
-    //         );
-    //     });
-
-    //     // Emitting a ZoomIn event shows the panel as zoomed.
-    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
-    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
-    //     });
-
-    //     // Move panel to another dock while it is zoomed
-    //     panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
-
-    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-    //     });
-
-    //     // If focus is transferred to another view that's not a panel or another pane, we still show
-    //     // the panel as zoomed.
-    //     let other_focus_handle = cx.update(|cx| cx.focus_handle());
-    //     cx.update(|cx| cx.focus(&other_focus_handle));
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
-    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-    //     });
-
-    //     // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
-    //     workspace.update(cx, |_, cx| cx.focus_self());
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, None);
-    //         assert_eq!(workspace.zoomed_position, None);
-    //     });
-
-    //     // If focus is transferred again to another view that's not a panel or a pane, we won't
-    //     // show the panel as zoomed because it wasn't zoomed before.
-    //     cx.update(|cx| cx.focus(&other_focus_handle));
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, None);
-    //         assert_eq!(workspace.zoomed_position, None);
-    //     });
-
-    //     // When focus is transferred back to the panel, it is zoomed again.
-    //     panel_1.update(cx, |_, cx| cx.focus_self());
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
-    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-    //     });
-
-    //     // Emitting a ZoomOut event unzooms the panel.
-    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
-    //     workspace.update(cx, |workspace, _| {
-    //         assert_eq!(workspace.zoomed, None);
-    //         assert_eq!(workspace.zoomed_position, None);
-    //     });
-
-    //     // Emit closed event on panel 1, which is active
-    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
-
-    //     // Now the left dock is closed, because panel_1 was the active panel
-    //     workspace.update(cx, |workspace, cx| {
-    //         let right_dock = workspace.right_dock();
-    //         assert!(!right_dock.read(cx).is_open());
-    //     });
-    // }
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+
+        let panel = workspace.update(cx, |workspace, cx| {
+            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
+            workspace.add_panel(panel.clone(), cx);
+
+            workspace
+                .right_dock()
+                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+            panel
+        });
+
+        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+        pane.update(cx, |pane, cx| {
+            let item = cx.new_view(|cx| TestItem::new(cx));
+            pane.add_item(Box::new(item), true, true, None, cx);
+        });
+
+        // Transfer focus from center to panel
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Transfer focus from panel to center
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Close the dock
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(!workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Open the dock
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Focus and zoom panel
+        panel.update(cx, |panel, cx| {
+            cx.focus_self();
+            panel.set_zoomed(true, cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Transfer focus to the center closes the dock
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(!workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Transferring focus back to the panel keeps it zoomed
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Close the dock while it is zoomed
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(!workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(workspace.zoomed.is_none());
+            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Opening the dock, when it's zoomed, retains focus
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(workspace.zoomed.is_some());
+            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+        });
+
+        // Unzoom and close the panel, zoom the active pane.
+        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
+
+        // Opening a dock unzooms the pane.
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+        workspace.update(cx, |workspace, cx| {
+            let pane = pane.read(cx);
+            assert!(!pane.is_zoomed());
+            assert!(!pane.focus_handle(cx).is_focused(cx));
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(workspace.zoomed.is_none());
+        });
+    }
+
+    struct TestModal(FocusHandle);
+
+    impl TestModal {
+        fn new(cx: &mut ViewContext<Self>) -> Self {
+            Self(cx.focus_handle())
+        }
+    }
+
+    impl EventEmitter<DismissEvent> for TestModal {}
+
+    impl FocusableView for TestModal {
+        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
+            self.0.clone()
+        }
+    }
+
+    impl ModalView for TestModal {}
+
+    impl Render for TestModal {
+        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
+            div().track_focus(&self.0)
+        }
+    }
+
+    #[gpui::test]
+    async fn test_panels(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.executor());
+
+        let project = Project::test(fs, [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+
+        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
+            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
+            workspace.add_panel(panel_1.clone(), cx);
+            workspace
+                .left_dock()
+                .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
+            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
+            workspace.add_panel(panel_2.clone(), cx);
+            workspace
+                .right_dock()
+                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+            let left_dock = workspace.left_dock();
+            assert_eq!(
+                left_dock.read(cx).visible_panel().unwrap().panel_id(),
+                panel_1.panel_id()
+            );
+            assert_eq!(
+                left_dock.read(cx).active_panel_size(cx).unwrap(),
+                panel_1.size(cx)
+            );
+
+            left_dock.update(cx, |left_dock, cx| {
+                left_dock.resize_active_panel(Some(px(1337.)), cx)
+            });
+            assert_eq!(
+                workspace
+                    .right_dock()
+                    .read(cx)
+                    .visible_panel()
+                    .unwrap()
+                    .panel_id(),
+                panel_2.panel_id(),
+            );
+
+            (panel_1, panel_2)
+        });
+
+        // Move panel_1 to the right
+        panel_1.update(cx, |panel_1, cx| {
+            panel_1.set_position(DockPosition::Right, cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
+            // Since it was the only panel on the left, the left dock should now be closed.
+            assert!(!workspace.left_dock().read(cx).is_open());
+            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
+            let right_dock = workspace.right_dock();
+            assert_eq!(
+                right_dock.read(cx).visible_panel().unwrap().panel_id(),
+                panel_1.panel_id()
+            );
+            assert_eq!(
+                right_dock.read(cx).active_panel_size(cx).unwrap(),
+                px(1337.)
+            );
+
+            // Now we move panel_2Β to the left
+            panel_2.set_position(DockPosition::Left, cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            // Since panel_2 was not visible on the right, we don't open the left dock.
+            assert!(!workspace.left_dock().read(cx).is_open());
+            // And the right dock is unaffected in it's displaying of panel_1
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert_eq!(
+                workspace
+                    .right_dock()
+                    .read(cx)
+                    .visible_panel()
+                    .unwrap()
+                    .panel_id(),
+                panel_1.panel_id(),
+            );
+        });
+
+        // Move panel_1 back to the left
+        panel_1.update(cx, |panel_1, cx| {
+            panel_1.set_position(DockPosition::Left, cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
+            let left_dock = workspace.left_dock();
+            assert!(left_dock.read(cx).is_open());
+            assert_eq!(
+                left_dock.read(cx).visible_panel().unwrap().panel_id(),
+                panel_1.panel_id()
+            );
+            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
+            // And the right dock should be closed as it no longer has any panels.
+            assert!(!workspace.right_dock().read(cx).is_open());
+
+            // Now we move panel_1 to the bottom
+            panel_1.set_position(DockPosition::Bottom, cx);
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            // Since panel_1 was visible on the left, we close the left dock.
+            assert!(!workspace.left_dock().read(cx).is_open());
+            // The bottom dock is sized based on the panel's default size,
+            // since the panel orientation changed from vertical to horizontal.
+            let bottom_dock = workspace.bottom_dock();
+            assert_eq!(
+                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
+                panel_1.size(cx),
+            );
+            // Close bottom dock and move panel_1 back to the left.
+            bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
+            panel_1.set_position(DockPosition::Left, cx);
+        });
+
+        // Emit activated event on panel 1
+        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
+
+        // Now the left dock is open and panel_1 is active and focused.
+        workspace.update(cx, |workspace, cx| {
+            let left_dock = workspace.left_dock();
+            assert!(left_dock.read(cx).is_open());
+            assert_eq!(
+                left_dock.read(cx).visible_panel().unwrap().panel_id(),
+                panel_1.panel_id(),
+            );
+            assert!(panel_1.focus_handle(cx).is_focused(cx));
+        });
+
+        // Emit closed event on panel 2, which is not active
+        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
+
+        // Wo don't close the left dock, because panel_2 wasn't the active panel
+        workspace.update(cx, |workspace, cx| {
+            let left_dock = workspace.left_dock();
+            assert!(left_dock.read(cx).is_open());
+            assert_eq!(
+                left_dock.read(cx).visible_panel().unwrap().panel_id(),
+                panel_1.panel_id(),
+            );
+        });
+
+        // Emitting a ZoomIn event shows the panel as zoomed.
+        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
+        });
+
+        // Move panel to another dock while it is zoomed
+        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+        });
+
+        // This is a helper for getting a:
+        // - valid focus on an element,
+        // - that isn't a part of the panes and panels system of the Workspace,
+        // - and doesn't trigger the 'on_focus_lost' API.
+        let focus_other_view = {
+            let workspace = workspace.clone();
+            move |cx: &mut VisualTestContext| {
+                workspace.update(cx, |workspace, cx| {
+                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
+                        workspace.toggle_modal(cx, TestModal::new);
+                        workspace.toggle_modal(cx, TestModal::new);
+                    } else {
+                        workspace.toggle_modal(cx, TestModal::new);
+                    }
+                })
+            }
+        };
+
+        // If focus is transferred to another view that's not a panel or another pane, we still show
+        // the panel as zoomed.
+        focus_other_view(cx);
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+        });
+
+        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
+        workspace.update(cx, |_, cx| cx.focus_self());
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, None);
+            assert_eq!(workspace.zoomed_position, None);
+        });
+
+        // If focus is transferred again to another view that's not a panel or a pane, we won't
+        // show the panel as zoomed because it wasn't zoomed before.
+        focus_other_view(cx);
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, None);
+            assert_eq!(workspace.zoomed_position, None);
+        });
+
+        // When the panel is activated, it is zoomed again.
+        cx.dispatch_action(ToggleRightDock);
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+        });
+
+        // Emitting a ZoomOut event unzooms the panel.
+        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
+        workspace.update(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, None);
+            assert_eq!(workspace.zoomed_position, None);
+        });
+
+        // Emit closed event on panel 1, which is active
+        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
+
+        // Now the left dock is closed, because panel_1 was the active panel
+        workspace.update(cx, |workspace, cx| {
+            let right_dock = workspace.right_dock();
+            assert!(!right_dock.read(cx).is_open());
+        });
+    }
 
     pub fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {