Detailed changes
@@ -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. :)
@@ -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;
@@ -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)])
+ );
+ }
+}
@@ -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
+ }
+}
@@ -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();
}
@@ -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();
@@ -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| {