From 8a96562adfa1a0fc9eba19bdd245d1cac7eed6fe Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2024 12:07:20 -0800 Subject: [PATCH 01/43] Handle contexts correctly for disabled key bindings --- crates/gpui/src/key_dispatch.rs | 14 +- crates/gpui/src/keymap/binding.rs | 41 +-- crates/gpui/src/keymap/keymap.rs | 448 ++++++----------------- crates/gpui/src/keymap/matcher.rs | 22 +- crates/gpui/src/platform/mac/platform.rs | 4 +- 5 files changed, 136 insertions(+), 393 deletions(-) diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index ade52a13143a8ba8ea6ecaa41434f53b721262a0..22c4dffc03a78df8fde5530a3059887e91a2b876 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -188,15 +188,13 @@ impl DispatchTree { action: &dyn Action, context_stack: &Vec, ) -> Vec { - self.keymap - .lock() - .bindings_for_action(action.type_id()) - .filter(|candidate| { - if !candidate.action.partial_eq(action) { - return false; - } + let keymap = self.keymap.lock(); + keymap + .bindings_for_action(action) + .filter(|binding| { for i in 1..context_stack.len() { - if candidate.matches_context(&context_stack[0..=i]) { + let context = &context_stack[0..i]; + if keymap.binding_enabled(binding, context) { return true; } } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index 24394107849e24ae80d827768573972d36b21cb3..766e54f4734c0075e40a4ff4699a1735eb483c80 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContextPredicate, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; @@ -42,21 +42,8 @@ impl KeyBinding { }) } - pub fn matches_context(&self, contexts: &[KeyContext]) -> bool { - self.context_predicate - .as_ref() - .map(|predicate| predicate.eval(contexts)) - .unwrap_or(true) - } - - pub fn match_keystrokes( - &self, - pending_keystrokes: &[Keystroke], - contexts: &[KeyContext], - ) -> KeyMatch { - if self.keystrokes.as_ref().starts_with(pending_keystrokes) - && self.matches_context(contexts) - { + pub fn match_keystrokes(&self, pending_keystrokes: &[Keystroke]) -> KeyMatch { + if self.keystrokes.as_ref().starts_with(pending_keystrokes) { // If the binding is completed, push it onto the matches list if self.keystrokes.as_ref().len() == pending_keystrokes.len() { KeyMatch::Some(vec![self.action.boxed_clone()]) @@ -68,18 +55,6 @@ impl KeyBinding { } } - pub fn keystrokes_for_action( - &self, - action: &dyn Action, - contexts: &[KeyContext], - ) -> Option> { - if self.action.partial_eq(action) && self.matches_context(contexts) { - Some(self.keystrokes.clone()) - } else { - None - } - } - pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } @@ -88,3 +63,13 @@ impl KeyBinding { self.action.as_ref() } } + +impl std::fmt::Debug for KeyBinding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyBinding") + .field("keystrokes", &self.keystrokes) + .field("context_predicate", &self.context_predicate) + .field("action", &self.action.name()) + .finish() + } +} diff --git a/crates/gpui/src/keymap/keymap.rs b/crates/gpui/src/keymap/keymap.rs index 8152693c07a5ab316ce03ab3600ed855595b2ccd..8c74e12e08e8e67e9e0784521b6df164ec25044b 100644 --- a/crates/gpui/src/keymap/keymap.rs +++ b/crates/gpui/src/keymap/keymap.rs @@ -1,4 +1,4 @@ -use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction}; +use crate::{Action, KeyBinding, KeyBindingContextPredicate, KeyContext, Keystroke, NoAction}; use collections::HashSet; use smallvec::SmallVec; use std::{ @@ -29,54 +29,22 @@ impl Keymap { self.version } - pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { - self.binding_indices_by_action_id - .get(&action_id) - .map(SmallVec::as_slice) - .unwrap_or(&[]) - .iter() - .map(|ix| &self.bindings[*ix]) - .filter(|binding| !self.binding_disabled(binding)) - } - pub fn add_bindings>(&mut self, bindings: T) { - let no_action_id = &(NoAction {}).type_id(); - let mut new_bindings = Vec::new(); - let mut has_new_disabled_keystrokes = false; + let no_action_id = (NoAction {}).type_id(); + for binding in bindings { - if binding.action.type_id() == *no_action_id { - has_new_disabled_keystrokes |= self - .disabled_keystrokes + let action_id = binding.action().as_any().type_id(); + if action_id == no_action_id { + self.disabled_keystrokes .entry(binding.keystrokes) .or_default() .insert(binding.context_predicate); } else { - new_bindings.push(binding); - } - } - - if has_new_disabled_keystrokes { - self.binding_indices_by_action_id.retain(|_, indices| { - indices.retain(|ix| { - let binding = &self.bindings[*ix]; - match self.disabled_keystrokes.get(&binding.keystrokes) { - Some(disabled_predicates) => { - !disabled_predicates.contains(&binding.context_predicate) - } - None => true, - } - }); - !indices.is_empty() - }); - } - - for new_binding in new_bindings { - if !self.binding_disabled(&new_binding) { self.binding_indices_by_action_id - .entry(new_binding.action().as_any().type_id()) + .entry(action_id) .or_default() .push(self.bindings.len()); - self.bindings.push(new_binding); + self.bindings.push(binding); } } @@ -90,311 +58,113 @@ impl Keymap { self.version.0 += 1; } - pub fn bindings(&self) -> Vec<&KeyBinding> { - self.bindings - .iter() - .filter(|binding| !self.binding_disabled(binding)) - .collect() + /// Iterate over all bindings, in the order they were added. + pub fn bindings(&self) -> impl Iterator + DoubleEndedIterator { + self.bindings.iter() } - fn binding_disabled(&self, binding: &KeyBinding) -> bool { - match self.disabled_keystrokes.get(&binding.keystrokes) { - Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate), - None => false, - } + /// Iterate over all bindings for the given action, in the order they were added. + pub fn bindings_for_action<'a>( + &'a self, + action: &'a dyn Action, + ) -> impl 'a + Iterator + DoubleEndedIterator { + let action_id = action.type_id(); + self.binding_indices_by_action_id + .get(&action_id) + .map_or(&[] as _, SmallVec::as_slice) + .iter() + .map(|ix| &self.bindings[*ix]) + .filter(move |binding| binding.action().partial_eq(action)) } -} - -// #[cfg(test)] -// mod tests { -// use crate::actions; - -// use super::*; -// actions!( -// keymap_test, -// [Present1, Present2, Present3, Duplicate, Missing] -// ); - -// #[test] -// fn regular_keymap() { -// let present_1 = Binding::new("ctrl-q", Present1 {}, None); -// let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); -// let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor")); -// let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None); -// let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); -// let missing = Binding::new("ctrl-r", Missing {}, None); -// let all_bindings = [ -// &present_1, -// &present_2, -// &present_3, -// &keystroke_duplicate_to_1, -// &full_duplicate_to_2, -// &missing, -// ]; - -// let mut keymap = Keymap::default(); -// assert_absent(&keymap, &all_bindings); -// assert!(keymap.bindings().is_empty()); - -// keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]); -// assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]); -// assert_present( -// &keymap, -// &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")], -// ); - -// keymap.add_bindings([ -// keystroke_duplicate_to_1.clone(), -// full_duplicate_to_2.clone(), -// ]); -// assert_absent(&keymap, &[&missing]); -// assert!( -// !keymap.binding_disabled(&keystroke_duplicate_to_1), -// "Duplicate binding 1 was added and should not be disabled" -// ); -// assert!( -// !keymap.binding_disabled(&full_duplicate_to_2), -// "Duplicate binding 2 was added and should not be disabled" -// ); - -// assert_eq!( -// keymap -// .bindings_for_action(keystroke_duplicate_to_1.action().id()) -// .map(|binding| &binding.keystrokes) -// .flatten() -// .collect::>(), -// vec![&Keystroke { -// ctrl: true, -// alt: false, -// shift: false, -// cmd: false, -// function: false, -// key: "q".to_string(), -// ime_key: None, -// }], -// "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap" -// ); -// assert_eq!( -// keymap -// .bindings_for_action(full_duplicate_to_2.action().id()) -// .map(|binding| &binding.keystrokes) -// .flatten() -// .collect::>(), -// vec![ -// &Keystroke { -// ctrl: true, -// alt: false, -// shift: false, -// cmd: false, -// function: false, -// key: "w".to_string(), -// ime_key: None, -// }, -// &Keystroke { -// ctrl: true, -// alt: false, -// shift: false, -// cmd: false, -// function: false, -// key: "w".to_string(), -// ime_key: None, -// } -// ], -// "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap" -// ); - -// let updated_bindings = keymap.bindings(); -// let expected_updated_bindings = vec![ -// &present_1, -// &present_2, -// &present_3, -// &keystroke_duplicate_to_1, -// &full_duplicate_to_2, -// ]; -// assert_eq!( -// updated_bindings.len(), -// expected_updated_bindings.len(), -// "Unexpected updated keymap bindings {updated_bindings:?}" -// ); -// for (i, expected) in expected_updated_bindings.iter().enumerate() { -// let keymap_binding = &updated_bindings[i]; -// assert_eq!( -// keymap_binding.context_predicate, expected.context_predicate, -// "Unexpected context predicate for keymap {i} element: {keymap_binding:?}" -// ); -// assert_eq!( -// keymap_binding.keystrokes, expected.keystrokes, -// "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}" -// ); -// } - -// keymap.clear(); -// assert_absent(&keymap, &all_bindings); -// assert!(keymap.bindings().is_empty()); -// } - -// #[test] -// fn keymap_with_ignored() { -// let present_1 = Binding::new("ctrl-q", Present1 {}, None); -// let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); -// let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor")); -// let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None); -// let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); -// let ignored_1 = Binding::new("ctrl-q", NoAction {}, None); -// let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane")); -// let ignored_3_with_other_context = -// Binding::new("ctrl-e", NoAction {}, Some("other_context")); - -// let mut keymap = Keymap::default(); - -// keymap.add_bindings([ -// ignored_1.clone(), -// ignored_2.clone(), -// ignored_3_with_other_context.clone(), -// ]); -// assert_absent(&keymap, &[&present_3]); -// assert_disabled( -// &keymap, -// &[ -// &present_1, -// &present_2, -// &ignored_1, -// &ignored_2, -// &ignored_3_with_other_context, -// ], -// ); -// assert!(keymap.bindings().is_empty()); -// keymap.clear(); - -// keymap.add_bindings([ -// present_1.clone(), -// present_2.clone(), -// present_3.clone(), -// ignored_1.clone(), -// ignored_2.clone(), -// ignored_3_with_other_context.clone(), -// ]); -// assert_present(&keymap, &[(&present_3, "e")]); -// assert_disabled( -// &keymap, -// &[ -// &present_1, -// &present_2, -// &ignored_1, -// &ignored_2, -// &ignored_3_with_other_context, -// ], -// ); -// keymap.clear(); - -// keymap.add_bindings([ -// present_1.clone(), -// present_2.clone(), -// present_3.clone(), -// ignored_1.clone(), -// ]); -// assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]); -// assert_disabled(&keymap, &[&present_1, &ignored_1]); -// assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]); -// keymap.clear(); + pub fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { + // If binding has a context predicate, it must match the current context, + if let Some(predicate) = &binding.context_predicate { + if !predicate.eval(context) { + return false; + } + } -// keymap.add_bindings([ -// present_1.clone(), -// present_2.clone(), -// present_3.clone(), -// keystroke_duplicate_to_1.clone(), -// full_duplicate_to_2.clone(), -// ignored_1.clone(), -// ignored_2.clone(), -// ignored_3_with_other_context.clone(), -// ]); -// assert_present(&keymap, &[(&present_3, "e")]); -// assert_disabled( -// &keymap, -// &[ -// &present_1, -// &present_2, -// &keystroke_duplicate_to_1, -// &full_duplicate_to_2, -// &ignored_1, -// &ignored_2, -// &ignored_3_with_other_context, -// ], -// ); -// keymap.clear(); -// } + if let Some(disabled_predicates) = self.disabled_keystrokes.get(&binding.keystrokes) { + for disabled_predicate in disabled_predicates { + match disabled_predicate { + // The binding must not be globally disabled. + None => return false, -// #[track_caller] -// fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) { -// let keymap_bindings = keymap.bindings(); -// assert_eq!( -// expected_bindings.len(), -// keymap_bindings.len(), -// "Unexpected keymap bindings {keymap_bindings:?}" -// ); -// for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() { -// assert!( -// !keymap.binding_disabled(expected), -// "{expected:?} should not be disabled as it was added into keymap for element {i}" -// ); -// assert_eq!( -// keymap -// .bindings_for_action(expected.action().id()) -// .map(|binding| &binding.keystrokes) -// .flatten() -// .collect::>(), -// vec![&Keystroke { -// ctrl: true, -// alt: false, -// shift: false, -// cmd: false, -// function: false, -// key: expected_key.to_string(), -// ime_key: None, -// }], -// "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}" -// ); + // The binding must not be disabled in the current context. + Some(predicate) => { + if predicate.eval(context) { + return false; + } + } + } + } + } -// let keymap_binding = &keymap_bindings[i]; -// assert_eq!( -// keymap_binding.context_predicate, expected.context_predicate, -// "Unexpected context predicate for keymap {i} element: {keymap_binding:?}" -// ); -// assert_eq!( -// keymap_binding.keystrokes, expected.keystrokes, -// "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}" -// ); -// } -// } + true + } +} -// #[track_caller] -// fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) { -// for binding in bindings.iter() { -// assert!( -// !keymap.binding_disabled(binding), -// "{binding:?} should not be disabled in the keymap where was not added" -// ); -// assert_eq!( -// keymap.bindings_for_action(binding.action().id()).count(), -// 0, -// "{binding:?} should have no actions in the keymap where was not added" -// ); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use gpui::actions; + + actions!( + keymap_test, + [ActionAlpha, ActionBeta, ActionGamma, ActionDelta,] + ); + + #[test] + fn test_keymap() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, None), + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // global bindings are enabled in all contexts + assert!(keymap.binding_enabled(&bindings[0], &[])); + assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()])); + + // contextual bindings are enabled in contexts that match their predicate + assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()])); + assert!(keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()])); + + assert!(!keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()])); + assert!(keymap.binding_enabled( + &bindings[2], + &[KeyContext::parse("editor mode=full").unwrap()] + )); + } -// #[track_caller] -// fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) { -// for binding in bindings.iter() { -// assert!( -// keymap.binding_disabled(binding), -// "{binding:?} should be disabled in the keymap" -// ); -// assert_eq!( -// keymap.bindings_for_action(binding.action().id()).count(), -// 0, -// "{binding:?} should have no actions in the keymap where it was disabled" -// ); -// } -// } -// } + #[test] + fn test_keymap_disabled() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")), + KeyBinding::new("ctrl-b", NoAction {}, None), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // binding is only enabled in a specific context + assert!(!keymap.binding_enabled(&bindings[0], &[KeyContext::parse("barf").unwrap()])); + assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("editor").unwrap()])); + + // binding is disabled in a more specific context + assert!(!keymap.binding_enabled( + &bindings[0], + &[KeyContext::parse("editor mode=full").unwrap()] + )); + + // binding is globally disabled + assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf").unwrap()])); + } +} diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index 9d74975c5635415ea3cf08dc8007f735fca981c3..ab42f1278c3a837c2895d77fdf388a842ed2433b 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/crates/gpui/src/keymap/matcher.rs @@ -1,6 +1,5 @@ use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; -use smallvec::SmallVec; use std::sync::Arc; pub struct KeystrokeMatcher { @@ -51,10 +50,14 @@ impl KeystrokeMatcher { let mut pending_key = None; let mut found_actions = Vec::new(); - for binding in keymap.bindings().iter().rev() { + for binding in keymap.bindings().rev() { + if !keymap.binding_enabled(binding, context_stack) { + continue; + } + for candidate in keystroke.match_candidates() { self.pending_keystrokes.push(candidate.clone()); - match binding.match_keystrokes(&self.pending_keystrokes, context_stack) { + match binding.match_keystrokes(&self.pending_keystrokes) { KeyMatch::Some(mut actions) => { found_actions.append(&mut actions); } @@ -82,19 +85,6 @@ impl KeystrokeMatcher { KeyMatch::Pending } } - - pub fn keystrokes_for_action( - &self, - action: &dyn Action, - contexts: &[KeyContext], - ) -> Option> { - self.keymap - .lock() - .bindings() - .iter() - .rev() - .find_map(|binding| binding.keystrokes_for_action(action, contexts)) - } } #[derive(Debug)] diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 70455767933f5de1df61603dbfce621c56162bc8..ff89f91730ae5d33e6720f1c9723ed8f2741f0ad 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -260,8 +260,8 @@ impl MacPlatform { os_action, } => { let keystrokes = keymap - .bindings_for_action(action.type_id()) - .find(|binding| binding.action().partial_eq(action.as_ref())) + .bindings_for_action(action.as_ref()) + .next() .map(|binding| binding.keystrokes()); let selector = match os_action { From 81d707adbc20010f8d15c7dfd41d8ae8b57748ba Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Jan 2024 14:23:07 -0700 Subject: [PATCH 02/43] Port 1.00 following tests Co-Authored-By: Max --- crates/collab/src/db/queries/rooms.rs | 8 + crates/collab/src/tests/following_tests.rs | 1110 ++++++++++---------- crates/terminal_view/src/terminal_panel.rs | 1 - crates/workspace/src/dock.rs | 29 +- crates/workspace/src/item.rs | 2 +- 5 files changed, 575 insertions(+), 575 deletions(-) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 40fdf5d58f184a0444f0c82938ab8fcd2a7bbb69..ac9cb261ad491a429a71e407d171f46905265535 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -855,6 +855,14 @@ impl Database { .exec(&*tx) .await?; + follower::Entity::delete_many() + .filter( + Condition::all() + .add(follower::Column::FollowerConnectionId.eq(connection.id as i32)), + ) + .exec(&*tx) + .await?; + // Unshare projects. project::Entity::delete_many() .filter( diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 5178df408f95b8809495836576c3f1c74159cf85..b2a9a3f95e1d584b21bee9f6c2dc009191c779eb 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,549 +1,521 @@ -//todo!(workspace) - -// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -// use call::ActiveCall; -// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; -// use editor::{Editor, ExcerptRange, MultiBuffer}; -// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext}; -// use live_kit_client::MacOSDisplay; -// use project::project_settings::ProjectSettings; -// use rpc::proto::PeerId; -// use serde_json::json; -// use settings::SettingsStore; -// use std::borrow::Cow; -// use workspace::{ -// dock::{test::TestPanel, DockPosition}, -// item::{test::TestItem, ItemHandle as _}, -// shared_screen::SharedScreen, -// SplitDirection, Workspace, -// }; - -// #[gpui::test(iterations = 10)] -// async fn test_basic_following( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// cx_d: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; -// let client_d = server.create_client(cx_d, "user_d").await; -// server -// .create_room(&mut [ -// (&client_a, cx_a), -// (&client_b, cx_b), -// (&client_c, cx_c), -// (&client_d, cx_d), -// ]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one\none\none", -// "2.txt": "two\ntwo\ntwo", -// "3.txt": "three\nthree\nthree", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let window_a = client_a.build_workspace(&project_a, cx_a); -// let workspace_a = window_a.root(cx_a).unwrap(); -// let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b).unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; -// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c); -// let cx_c = &mut cx_c; -// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d); -// let cx_d = &mut cx_d; - -// // Client A opens some editors. -// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); -// let editor_a1 = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_a2 = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client B opens an editor. -// let editor_b1 = workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// let peer_id_a = client_a.peer_id().unwrap(); -// let peer_id_b = client_b.peer_id().unwrap(); -// let peer_id_c = client_c.peer_id().unwrap(); -// let peer_id_d = client_d.peer_id().unwrap(); - -// // Client A updates their selections in those editors -// editor_a1.update(cx_a, |editor, cx| { -// editor.handle_input("a", cx); -// editor.handle_input("b", cx); -// editor.handle_input("c", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![3..2]); -// }); -// editor_a2.update(cx_a, |editor, cx| { -// editor.handle_input("d", cx); -// editor.handle_input("e", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![2..1]); -// }); - -// // When client B starts following client A, all visible view states are replicated to client B. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// cx_c.executor().run_until_parked(); -// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// cx_b.read(|cx| editor_b2.project_path(cx)), -// Some((worktree_id, "2.txt").into()) -// ); -// assert_eq!( -// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![2..1] -// ); -// assert_eq!( -// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![3..2] -// ); - -// cx_c.executor().run_until_parked(); -// let active_call_c = cx_c.read(ActiveCall::global); -// let project_c = client_c.build_remote_project(project_id, cx_c).await; -// let window_c = client_c.build_workspace(&project_c, cx_c); -// let workspace_c = window_c.root(cx_c).unwrap(); -// active_call_c -// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) -// .await -// .unwrap(); -// drop(project_c); - -// // Client C also follows client A. -// workspace_c -// .update(cx_c, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// cx_d.executor().run_until_parked(); -// let active_call_d = cx_d.read(ActiveCall::global); -// let project_d = client_d.build_remote_project(project_id, cx_d).await; -// let workspace_d = client_d -// .build_workspace(&project_d, cx_d) -// .root(cx_d) -// .unwrap(); -// active_call_d -// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) -// .await -// .unwrap(); -// drop(project_d); - -// // All clients see that clients B and C are following client A. -// cx_c.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b, peer_id_c])], -// "followers seen by {name}" -// ); -// } - -// // Client C unfollows client A. -// workspace_c.update(cx_c, |workspace, cx| { -// workspace.unfollow(&workspace.active_pane().clone(), cx); -// }); - -// // All clients see that clients B is following client A. -// cx_c.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b])], -// "followers seen by {name}" -// ); -// } - -// // Client C re-follows client A. -// workspace_c -// .update(cx_c, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// // All clients see that clients B and C are following client A. -// cx_c.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b, peer_id_c])], -// "followers seen by {name}" -// ); -// } - -// // Client D follows client B, then switches to following client C. -// workspace_d -// .update(cx_d, |workspace, cx| { -// workspace.follow(peer_id_b, cx).unwrap() -// }) -// .await -// .unwrap(); -// workspace_d -// .update(cx_d, |workspace, cx| { -// workspace.follow(peer_id_c, cx).unwrap() -// }) -// .await -// .unwrap(); - -// // All clients see that D is following C -// cx_d.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[ -// (peer_id_a, vec![peer_id_b, peer_id_c]), -// (peer_id_c, vec![peer_id_d]) -// ], -// "followers seen by {name}" -// ); -// } - -// // Client C closes the project. -// window_c.remove(cx_c); -// cx_c.drop_last(workspace_c); - -// // Clients A and B see that client B is following A, and client C is not present in the followers. -// cx_c.executor().run_until_parked(); -// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { -// assert_eq!( -// followers_by_leader(project_id, cx), -// &[(peer_id_a, vec![peer_id_b]),], -// "followers seen by {name}" -// ); -// } - -// // When client A activates a different editor, client B does so as well. -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.activate_item(&editor_a1, cx) -// }); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// // When client A opens a multibuffer, client B does so as well. -// let multibuffer_a = cx_a.build_model(|cx| { -// let buffer_a1 = project_a.update(cx, |project, cx| { -// project -// .get_open_buffer(&(worktree_id, "1.txt").into(), cx) -// .unwrap() -// }); -// let buffer_a2 = project_a.update(cx, |project, cx| { -// project -// .get_open_buffer(&(worktree_id, "2.txt").into(), cx) -// .unwrap() -// }); -// let mut result = MultiBuffer::new(0); -// result.push_excerpts( -// buffer_a1, -// [ExcerptRange { -// context: 0..3, -// primary: None, -// }], -// cx, -// ); -// result.push_excerpts( -// buffer_a2, -// [ExcerptRange { -// context: 4..7, -// primary: None, -// }], -// cx, -// ); -// result -// }); -// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { -// let editor = -// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); -// workspace.add_item(Box::new(editor.clone()), cx); -// editor -// }); -// executor.run_until_parked(); -// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), -// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), -// ); - -// // When client A navigates back and forth, client B does so as well. -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b2.item_id() -// ); -// }); - -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.go_forward(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_b.update(cx_b, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_b1.item_id() -// ); -// }); - -// // Changes to client A's editor are reflected on client B. -// editor_a1.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); -// }); -// executor.run_until_parked(); -// editor_b1.update(cx_b, |editor, cx| { -// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); -// }); - -// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); -// executor.run_until_parked(); -// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); - -// editor_a1.update(cx_a, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([3..3])); -// editor.set_scroll_position(point(0., 100.), cx); -// }); -// executor.run_until_parked(); -// editor_b1.update(cx_b, |editor, cx| { -// assert_eq!(editor.selections.ranges(cx), &[3..3]); -// }); - -// // After unfollowing, client B stops receiving updates from client A. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.unfollow(&workspace.active_pane().clone(), cx) -// }); -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.activate_item(&editor_a2, cx) -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// editor_b1.item_id() -// ); - -// // Client A starts following client B. -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.follow(peer_id_b, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), -// Some(peer_id_b) -// ); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// editor_a1.item_id() -// ); - -// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. -// let display = MacOSDisplay::new(); -// active_call_b -// .update(cx_b, |call, cx| call.set_location(None, cx)) -// .await -// .unwrap(); -// active_call_b -// .update(cx_b, |call, cx| { -// call.room().unwrap().update(cx, |room, cx| { -// room.set_display_sources(vec![display.clone()]); -// room.share_screen(cx) -// }) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); -// let shared_screen = workspace_a.update(cx_a, |workspace, cx| { -// workspace -// .active_item(cx) -// .expect("no active item") -// .downcast::() -// .expect("active item isn't a shared screen") -// }); - -// // Client B activates Zed again, which causes the previous editor to become focused again. -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// editor_a1.item_id() -// ) -// }); - -// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.activate_item(&multibuffer_editor_b, cx) -// }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// multibuffer_editor_a.item_id() -// ) -// }); - -// // Client B activates a panel, and the previously-opened screen-sharing item gets activated. -// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left)); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.add_panel(panel, cx); -// workspace.toggle_panel_focus::(cx); -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// shared_screen.item_id() -// ); - -// // Toggling the focus back to the pane causes client A to return to the multibuffer. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.toggle_panel_focus::(cx); -// }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert_eq!( -// workspace.active_item(cx).unwrap().item_id(), -// multibuffer_editor_a.item_id() -// ) -// }); - -// // Client B activates an item that doesn't implement following, -// // so the previously-opened screen-sharing item gets activated. -// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new()); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.add_item(Box::new(unfollowable_item), true, true, None, cx) -// }) -// }); -// executor.run_until_parked(); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, cx| workspace -// .active_item(cx) -// .unwrap() -// .item_id()), -// shared_screen.item_id() -// ); - -// // Following interrupts when client B disconnects. -// client_b.disconnect(&cx_b.to_async()); -// executor.advance_clock(RECONNECT_TIMEOUT); -// assert_eq!( -// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), -// None -// ); -// } +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; +use editor::{Editor, ExcerptRange, MultiBuffer}; +use gpui::{ + point, BackgroundExecutor, Context, TestAppContext, View, VisualContext, VisualTestContext, + WindowContext, +}; +use live_kit_client::MacOSDisplay; +use project::project_settings::ProjectSettings; +use rpc::proto::PeerId; +use serde_json::json; +use settings::SettingsStore; +use std::borrow::Cow; +use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::{test::TestItem, ItemHandle as _}, + shared_screen::SharedScreen, + SplitDirection, Workspace, +}; + +#[gpui::test(iterations = 10)] +async fn test_basic_following( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_d: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + let client_d = server.create_client(cx_d, "user_d").await; + server + .create_room(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Client A opens some editors. + let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let peer_id_a = client_a.peer_id().unwrap(); + let peer_id_b = client_b.peer_id().unwrap(); + let peer_id_c = client_c.peer_id().unwrap(); + let peer_id_d = client_d.peer_id().unwrap(); + + // Client A updates their selections in those editors + editor_a1.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_a2.update(cx_a, |editor, cx| { + editor.handle_input("d", cx); + editor.handle_input("e", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![2..1]); + }); + + // When client B starts following client A, all visible view states are replicated to client B. + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + + cx_c.executor().run_until_parked(); + let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_b2.project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); + assert_eq!( + editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![2..1] + ); + assert_eq!( + editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![3..2] + ); + + executor.run_until_parked(); + let active_call_c = cx_c.read(ActiveCall::global); + let project_c = client_c.build_remote_project(project_id, cx_c).await; + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + let weak_project_c = project_c.downgrade(); + drop(project_c); + + // Client C also follows client A. + workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx)); + + cx_d.executor().run_until_parked(); + let active_call_d = cx_d.read(ActiveCall::global); + let project_d = client_d.build_remote_project(project_id, cx_d).await; + let (workspace_d, cx_d) = client_d.build_workspace(&project_d, cx_d); + active_call_d + .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) + .await + .unwrap(); + drop(project_d); + + // All clients see that clients B and C are following client A. + cx_c.executor().run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b, peer_id_c])], + "followers seen by {name}" + ); + } + + // Client C unfollows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx); + }); + + // All clients see that clients B is following client A. + cx_c.executor().run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b])], + "followers seen by {name}" + ); + } + + // Client C re-follows client A. + workspace_c.update(cx_c, |workspace, cx| workspace.follow(peer_id_a, cx)); + + // All clients see that clients B and C are following client A. + cx_c.executor().run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b, peer_id_c])], + "followers seen by {name}" + ); + } + + // Client D follows client B, then switches to following client C. + workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_b, cx)); + cx_a.executor().run_until_parked(); + workspace_d.update(cx_d, |workspace, cx| workspace.follow(peer_id_c, cx)); + + // All clients see that D is following C + cx_a.executor().run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[ + (peer_id_a, vec![peer_id_b, peer_id_c]), + (peer_id_c, vec![peer_id_d]) + ], + "followers seen by {name}" + ); + } + + // Client C closes the project. + let weak_workspace_c = workspace_c.downgrade(); + workspace_c.update(cx_c, |workspace, cx| { + workspace.close_window(&Default::default(), cx); + }); + cx_c.update(|_| { + drop(workspace_c); + }); + cx_b.executor().run_until_parked(); + // are you sure you want to leave the call? + cx_c.simulate_prompt_answer(0); + cx_b.executor().run_until_parked(); + executor.run_until_parked(); + + weak_workspace_c.assert_dropped(); + weak_project_c.assert_dropped(); + + // Clients A and B see that client B is following A, and client C is not present in the followers. + executor.run_until_parked(); + for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("D", &cx_d)] { + assert_eq!( + followers_by_leader(project_id, cx), + &[(peer_id_a, vec![peer_id_b]),], + "followers seen by {name}" + ); + } + + // When client A activates a different editor, client B does so as well. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a1, cx) + }); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); + }); + + // When client A opens a multibuffer, client B does so as well. + let multibuffer_a = cx_a.new_model(|cx| { + let buffer_a1 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "1.txt").into(), cx) + .unwrap() + }); + let buffer_a2 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "2.txt").into(), cx) + .unwrap() + }); + let mut result = MultiBuffer::new(0); + result.push_excerpts( + buffer_a1, + [ExcerptRange { + context: 0..3, + primary: None, + }], + cx, + ); + result.push_excerpts( + buffer_a2, + [ExcerptRange { + context: 4..7, + primary: None, + }], + cx, + ); + result + }); + let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { + let editor = + cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor + }); + executor.run_until_parked(); + let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), + ); + + // When client A navigates back and forth, client B does so as well. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b2.item_id() + ); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_forward(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_b1.item_id() + ); + }); + + // Changes to client A's editor are reflected on client B. + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); + }); + executor.run_until_parked(); + cx_b.background_executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + }); + + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); + + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.set_scroll_position(point(0., 100.), cx); + }); + executor.run_until_parked(); + editor_b1.update(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[3..3]); + }); + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx) + }); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + editor_b1.item_id() + ); + + // Client A starts following client B. + workspace_a.update(cx_a, |workspace, cx| workspace.follow(peer_id_b, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(peer_id_b) + ); + assert_eq!( + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + editor_a1.item_id() + ); + + // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + let display = MacOSDisplay::new(); + active_call_b + .update(cx_b, |call, cx| call.set_location(None, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_display_sources(vec![display.clone()]); + room.share_screen(cx) + }) + }) + .await + .unwrap(); + executor.run_until_parked(); + let shared_screen = workspace_a.update(cx_a, |workspace, cx| { + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item isn't a shared screen") + }); + + // Client B activates Zed again, which causes the previous editor to become focused again. + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_a1.item_id() + ) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, cx) + }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_id() + ) + }); + + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + executor.run_until_parked(); + assert_eq!( + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + shared_screen.item_id() + ); + + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = cx_b.new_view(|cx| TestItem::new(cx)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); + executor.run_until_parked(); + assert_eq!( + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + shared_screen.item_id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()); + executor.advance_clock(RECONNECT_TIMEOUT); + assert_eq!( + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); +} // #[gpui::test] // async fn test_following_tab_order( @@ -1834,29 +1806,29 @@ // items: Vec<(bool, String)>, // } -// fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { -// cx.read(|cx| { -// let active_call = ActiveCall::global(cx).read(cx); -// let peer_id = active_call.client().peer_id(); -// let room = active_call.room().unwrap().read(cx); -// let mut result = room -// .remote_participants() -// .values() -// .map(|participant| participant.peer_id) -// .chain(peer_id) -// .filter_map(|peer_id| { -// let followers = room.followers_for(peer_id, project_id); -// if followers.is_empty() { -// None -// } else { -// Some((peer_id, followers.to_vec())) -// } -// }) -// .collect::>(); -// result.sort_by_key(|e| e.0); -// result -// }) -// } +fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { + cx.read(|cx| { + let active_call = ActiveCall::global(cx).read(cx); + let peer_id = active_call.client().peer_id(); + let room = active_call.room().unwrap().read(cx); + let mut result = room + .remote_participants() + .values() + .map(|participant| participant.peer_id) + .chain(peer_id) + .filter_map(|peer_id| { + let followers = room.followers_for(peer_id, project_id); + if followers.is_empty() { + None + } else { + Some((peer_id, followers.to_vec())) + } + }) + .collect::>(); + result.sort_by_key(|e| e.0); + result + }) +} // fn pane_summaries(workspace: &View, cx: &mut WindowContext<'_>) -> Vec { // workspace.update(cx, |workspace, cx| { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 82d7208ef88ba625d8430e8f5d314a9b00cb7719..cfef718a203fded2db0ec95d170317eaeaab4dcc 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -214,7 +214,6 @@ impl TerminalPanel { pane::Event::Remove => cx.emit(PanelEvent::Close), pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), - pane::Event::Focus => cx.emit(PanelEvent::Focus), pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index bd965f63d4cba742abdf3a405c04fd1929054ae3..4adf38882d040e51d30a89e68914c6b043e4774f 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -19,7 +19,6 @@ pub enum PanelEvent { ZoomOut, Activate, Close, - Focus, } pub trait Panel: FocusableView + EventEmitter { @@ -216,6 +215,28 @@ impl Dock { } }); + cx.on_focus_in(&focus_handle, { + let dock = dock.downgrade(); + move |workspace, cx| { + let Some(dock) = dock.upgrade() else { + return; + }; + let Some(panel) = dock.read(cx).active_panel() else { + return; + }; + if panel.is_zoomed(cx) { + workspace.zoomed = Some(panel.to_any().downgrade().into()); + workspace.zoomed_position = Some(position); + } else { + workspace.zoomed = None; + workspace.zoomed_position = None; + } + workspace.dismiss_zoomed_items_to_reveal(Some(position), cx); + workspace.update_active_view_for_followers(cx) + } + }) + .detach(); + cx.observe(&dock, move |workspace, dock, cx| { if dock.read(cx).is_open() { if let Some(panel) = dock.read(cx).active_panel() { @@ -394,7 +415,6 @@ impl Dock { this.set_open(false, cx); } } - PanelEvent::Focus => {} }), ]; @@ -561,6 +581,7 @@ impl Render for Dock { } div() + .track_focus(&self.focus_handle) .flex() .bg(cx.theme().colors().panel_background) .border_color(cx.theme().colors().border) @@ -584,7 +605,7 @@ impl Render for Dock { ) .child(handle) } else { - div() + div().track_focus(&self.focus_handle) } } } @@ -720,7 +741,7 @@ pub mod test { impl Render for TestPanel { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div() + div().id("test").track_focus(&self.focus_handle) } } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 38b76630307faee9f015c61e04f75d9fd703f153..45f6141df2f172ccad176c48bc1f83eab6174c3e 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -442,7 +442,7 @@ impl ItemHandle for View { ) && !pending_update_scheduled.load(Ordering::SeqCst) { pending_update_scheduled.store(true, Ordering::SeqCst); - cx.on_next_frame({ + cx.defer({ let pending_update = pending_update.clone(); let pending_update_scheduled = pending_update_scheduled.clone(); move |this, cx| { From f239a8292ea9fc638be74c7c29757078bb34aa3b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Jan 2024 16:13:40 -0700 Subject: [PATCH 03/43] More following tests --- crates/collab/src/tests/following_tests.rs | 2095 ++++++++++---------- crates/gpui/src/app/test_context.rs | 4 + crates/gpui/src/platform/test/platform.rs | 4 +- crates/workspace/src/workspace.rs | 10 +- 4 files changed, 1017 insertions(+), 1096 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index b2a9a3f95e1d584b21bee9f6c2dc009191c779eb..5dc145bf168f1c9e4926ac3e0556457ccaafcb11 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -3,8 +3,8 @@ use call::ActiveCall; use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{ - point, BackgroundExecutor, Context, TestAppContext, View, VisualContext, VisualTestContext, - WindowContext, + point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext, + VisualTestContext, WindowContext, }; use live_kit_client::MacOSDisplay; use project::project_settings::ProjectSettings; @@ -517,1130 +517,1051 @@ async fn test_basic_following( ); } -// #[gpui::test] -// async fn test_following_tab_order( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); +#[gpui::test] +async fn test_following_tab_order( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); -// cx_a.update(editor::init); -// cx_b.update(editor::init); + cx_a.update(editor::init); + cx_b.update(editor::init); -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one", -// "2.txt": "two", -// "3.txt": "three", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); -// let client_b_id = project_a.update(cx_a, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); + let client_b_id = project_a.update(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); -// //Open 1, 3 in that order on client A -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "3.txt"), None, true, cx) -// }) -// .await -// .unwrap(); + //Open 1, 3 in that order on client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); -// let pane_paths = |pane: &View, cx: &mut TestAppContext| { -// pane.update(cx, |pane, cx| { -// pane.items() -// .map(|item| { -// item.project_path(cx) -// .unwrap() -// .path -// .to_str() -// .unwrap() -// .to_owned() -// }) -// .collect::>() -// }) -// }; + let pane_paths = |pane: &View, cx: &mut VisualTestContext| { + pane.update(cx, |pane, cx| { + pane.items() + .map(|item| { + item.project_path(cx) + .unwrap() + .path + .to_str() + .unwrap() + .to_owned() + }) + .collect::>() + }) + }; -// //Verify that the tabs opened in the order we expect -// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); + //Verify that the tabs opened in the order we expect + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); -// //Follow client B as client A -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.follow(client_b_id, cx).unwrap() -// }) -// .await -// .unwrap(); + //Follow client B as client A + workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + executor.run_until_parked(); -// //Open just 2 on client B -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); + //Open just 2 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); -// // Verify that newly opened followed file is at the end -// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); + // Verify that newly opened followed file is at the end + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); -// //Open just 1 on client B -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); -// executor.run_until_parked(); + //Open just 1 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); + executor.run_until_parked(); -// // Verify that following into 1 did not reorder -// assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); -// } + // Verify that following into 1 did not reorder + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); +} -// #[gpui::test(iterations = 10)] -// async fn test_peers_following_each_other( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); +#[gpui::test(iterations = 10)] +async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); -// cx_a.update(editor::init); -// cx_b.update(editor::init); + cx_a.update(editor::init); + cx_b.update(editor::init); -// // Client A shares a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one", -// "2.txt": "two", -// "3.txt": "three", -// "4.txt": "four", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + "4.txt": "four", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); -// // Client B joins the project. -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); + // Client B joins the project. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); -// // Client A opens a file. -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + // Client A opens a file. + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// // Client B opens a different file. -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + // Client B opens a different file. + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// // Clients A and B follow each other in split panes -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); -// }); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.follow(client_b.peer_id().unwrap(), cx).unwrap() -// }) -// .await -// .unwrap(); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); -// }); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// }) -// .await -// .unwrap(); + // Clients A and B follow each other in split panes + workspace_a.update(cx_a, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.follow(client_b.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); -// // Clients A and B return focus to the original files they had open -// workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); -// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); -// executor.run_until_parked(); + // Clients A and B return focus to the original files they had open + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + executor.run_until_parked(); -// // Both clients see the other client's focused file in their right pane. -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: true, -// leader: None, -// items: vec![(true, "1.txt".into())] -// }, -// PaneSummary { -// active: false, -// leader: client_b.peer_id(), -// items: vec![(false, "1.txt".into()), (true, "2.txt".into())] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: true, -// leader: None, -// items: vec![(true, "2.txt".into())] -// }, -// PaneSummary { -// active: false, -// leader: client_a.peer_id(), -// items: vec![(false, "2.txt".into()), (true, "1.txt".into())] -// }, -// ] -// ); - -// // Clients A and B each open a new file. -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "3.txt"), None, true, cx) -// }) -// .await -// .unwrap(); - -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "4.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// executor.run_until_parked(); + // Both clients see the other client's focused file in their right pane. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(true, "1.txt".into())] + }, + PaneSummary { + active: false, + leader: client_b.peer_id(), + items: vec![(false, "1.txt".into()), (true, "2.txt".into())] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(true, "2.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![(false, "2.txt".into()), (true, "1.txt".into())] + }, + ] + ); -// // Both client's see the other client open the new file, but keep their -// // focus on their own active pane. -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: true, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: false, -// leader: client_b.peer_id(), -// items: vec![ -// (false, "1.txt".into()), -// (false, "2.txt".into()), -// (true, "4.txt".into()) -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: true, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: false, -// leader: client_a.peer_id(), -// items: vec![ -// (false, "2.txt".into()), -// (false, "1.txt".into()), -// (true, "3.txt".into()) -// ] -// }, -// ] -// ); + // Clients A and B each open a new file. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); -// // Client A focuses their right pane, in which they're following client B. -// workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); -// executor.run_until_parked(); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "4.txt"), None, true, cx) + }) + .await + .unwrap(); + executor.run_until_parked(); -// // Client B sees that client A is now looking at the same file as them. -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_b.peer_id(), -// items: vec![ -// (false, "1.txt".into()), -// (false, "2.txt".into()), -// (true, "4.txt".into()) -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: true, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: false, -// leader: client_a.peer_id(), -// items: vec![ -// (false, "2.txt".into()), -// (false, "1.txt".into()), -// (false, "3.txt".into()), -// (true, "4.txt".into()) -// ] -// }, -// ] -// ); + // Both client's see the other client open the new file, but keep their + // focus on their own active pane. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: false, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (true, "3.txt".into()) + ] + }, + ] + ); -// // Client B focuses their right pane, in which they're following client A, -// // who is following them. -// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); -// executor.run_until_parked(); + // Client A focuses their right pane, in which they're following client B. + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + executor.run_until_parked(); -// // Client A sees that client B is now looking at the same file as them. -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_a.peer_id(), -// items: vec![ -// (false, "2.txt".into()), -// (false, "1.txt".into()), -// (false, "3.txt".into()), -// (true, "4.txt".into()) -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_b.peer_id(), -// items: vec![ -// (false, "1.txt".into()), -// (false, "2.txt".into()), -// (true, "4.txt".into()) -// ] -// }, -// ] -// ); + // Client B sees that client A is now looking at the same file as them. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (false, "3.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); -// // Client B focuses a file that they previously followed A to, breaking -// // the follow. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.activate_prev_item(true, cx); -// }); -// }); -// executor.run_until_parked(); + // Client B focuses their right pane, in which they're following client A, + // who is following them. + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + executor.run_until_parked(); -// // Both clients see that client B is looking at that previous file. -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: None, -// items: vec![ -// (false, "2.txt".into()), -// (false, "1.txt".into()), -// (true, "3.txt".into()), -// (false, "4.txt".into()) -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_b.peer_id(), -// items: vec![ -// (false, "1.txt".into()), -// (false, "2.txt".into()), -// (false, "4.txt".into()), -// (true, "3.txt".into()), -// ] -// }, -// ] -// ); + // Client A sees that client B is now looking at the same file as them. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (false, "3.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); -// // Client B closes tabs, some of which were originally opened by client A, -// // and some of which were originally opened by client B. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.close_inactive_items(&Default::default(), cx) -// .unwrap() -// .detach(); -// }); -// }); + // Client B focuses a file that they previously followed A to, breaking + // the follow. + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + executor.run_until_parked(); -// executor.run_until_parked(); + // Both clients see that client B is looking at that previous file. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (true, "3.txt".into()), + (false, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (true, "3.txt".into()), + ] + }, + ] + ); -// // Both clients see that Client B is looking at the previous tab. -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: None, -// items: vec![(true, "3.txt".into()),] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_b.peer_id(), -// items: vec![ -// (false, "1.txt".into()), -// (false, "2.txt".into()), -// (false, "4.txt".into()), -// (true, "3.txt".into()), -// ] -// }, -// ] -// ); + // Client B closes tabs, some of which were originally opened by client A, + // and some of which were originally opened by client B. + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_inactive_items(&Default::default(), cx) + .unwrap() + .detach(); + }); + }); -// // Client B follows client A again. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// }) -// .await -// .unwrap(); + executor.run_until_parked(); -// // Client A cycles through some tabs. -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.activate_prev_item(true, cx); -// }); -// }); -// executor.run_until_parked(); + // Both clients see that Client B is looking at the previous tab. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![(true, "3.txt".into()),] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (true, "3.txt".into()), + ] + }, + ] + ); -// // Client B follows client A into those tabs. -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: None, -// items: vec![ -// (false, "1.txt".into()), -// (false, "2.txt".into()), -// (true, "4.txt".into()), -// (false, "3.txt".into()), -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_a.peer_id(), -// items: vec![(false, "3.txt".into()), (true, "4.txt".into())] -// }, -// ] -// ); + // Client B follows client A again. + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); + executor.run_until_parked(); + // Client A cycles through some tabs. + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.activate_prev_item(true, cx); -// }); -// }); -// executor.run_until_parked(); + // Client B follows client A into those tabs. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![(false, "3.txt".into()), (true, "4.txt".into())] + }, + ] + ); -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: None, -// items: vec![ -// (false, "1.txt".into()), -// (true, "2.txt".into()), -// (false, "4.txt".into()), -// (false, "3.txt".into()), -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_a.peer_id(), -// items: vec![ -// (false, "3.txt".into()), -// (false, "4.txt".into()), -// (true, "2.txt".into()) -// ] -// }, -// ] -// ); + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// workspace.active_pane().update(cx, |pane, cx| { -// pane.activate_prev_item(true, cx); -// }); -// }); -// executor.run_until_parked(); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "1.txt".into()), + (true, "2.txt".into()), + (false, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "3.txt".into()), + (false, "4.txt".into()), + (true, "2.txt".into()) + ] + }, + ] + ); -// assert_eq!( -// pane_summaries(&workspace_a, cx_a), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "1.txt".into()), (true, "3.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: None, -// items: vec![ -// (true, "1.txt".into()), -// (false, "2.txt".into()), -// (false, "4.txt".into()), -// (false, "3.txt".into()), -// ] -// }, -// ] -// ); -// assert_eq!( -// pane_summaries(&workspace_b, cx_b), -// &[ -// PaneSummary { -// active: false, -// leader: None, -// items: vec![(false, "2.txt".into()), (true, "4.txt".into())] -// }, -// PaneSummary { -// active: true, -// leader: client_a.peer_id(), -// items: vec![ -// (false, "3.txt".into()), -// (false, "4.txt".into()), -// (false, "2.txt".into()), -// (true, "1.txt".into()), -// ] -// }, -// ] -// ); -// } + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + executor.run_until_parked(); -// #[gpui::test(iterations = 10)] -// async fn test_auto_unfollowing( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// // 2 clients connect to a server. -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (true, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "3.txt".into()), + (false, "4.txt".into()), + (false, "2.txt".into()), + (true, "1.txt".into()), + ] + }, + ] + ); +} -// cx_a.update(editor::init); -// cx_b.update(editor::init); +#[gpui::test(iterations = 10)] +async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + // 2 clients connect to a server. + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); -// // Client A shares a project. -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "1.txt": "one", -// "2.txt": "two", -// "3.txt": "three", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); + cx_a.update(editor::init); + cx_b.update(editor::init); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); -// // Client A opens some editors. -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let _editor_a1 = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); -// // Client B starts following client A. -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); -// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); -// let leader_id = project_b.update(cx_b, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); -// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); -// // When client B moves, it automatically stops following client A. -// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); + // Client B starts following client A. + let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.update(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); -// // When client B edits, it automatically stops following client A. -// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); -// // When client B scrolls, it automatically stops following client A. -// editor_b2.update(cx_b, |editor, cx| { -// editor.set_scroll_position(point(0., 3.), cx) -// }); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(leader_id, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(point(0., 3.), cx) + }); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); -// // When client B activates a different pane, it continues following client A in the original pane. -// workspace_b.update(cx_b, |workspace, cx| { -// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) -// }); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(leader_id, cx)); + executor.run_until_parked(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); -// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// Some(leader_id) -// ); + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); -// // When client B activates a different item in the original pane, it automatically stops following client A. -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id, "2.txt"), None, true, cx) -// }) -// .await -// .unwrap(); -// assert_eq!( -// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), -// None -// ); -// } + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); -// #[gpui::test(iterations = 10)] -// async fn test_peers_simultaneously_following_each_other( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); +} -// cx_a.update(editor::init); -// cx_b.update(editor::init); +#[gpui::test(iterations = 10)] +async fn test_peers_simultaneously_following_each_other( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); -// client_a.fs().insert_tree("/a", json!({})).await; -// let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); + cx_a.update(editor::init); + cx_b.update(editor::init); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); + client_a.fs().insert_tree("/a", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); -// executor.run_until_parked(); -// let client_a_id = project_b.update(cx_b, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); -// let client_b_id = project_a.update(cx_a, |project, _| { -// project.collaborators().values().next().unwrap().peer_id -// }); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); -// let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { -// workspace.follow(client_b_id, cx).unwrap() -// }); -// let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { -// workspace.follow(client_a_id, cx).unwrap() -// }); + executor.run_until_parked(); + let client_a_id = project_b.update(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.update(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); -// futures::try_join!(a_follow_b, b_follow_a).unwrap(); -// workspace_a.update(cx_a, |workspace, _| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_b_id) -// ); -// }); -// workspace_b.update(cx_b, |workspace, _| { -// assert_eq!( -// workspace.leader_for_pane(workspace.active_pane()), -// Some(client_a_id) -// ); -// }); -// } + workspace_a.update(cx_a, |workspace, cx| workspace.follow(client_b_id, cx)); + workspace_b.update(cx_b, |workspace, cx| workspace.follow(client_a_id, cx)); + executor.run_until_parked(); -// #[gpui::test(iterations = 10)] -// async fn test_following_across_workspaces( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// // a and b join a channel/call -// // a shares project 1 -// // b shares project 2 -// // -// // b follows a: causes project 2 to be joined, and b to follow a. -// // b opens a different file in project 2, a follows b -// // b opens a different file in project 1, a cannot follow b -// // b shares the project, a joins the project and follows b -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// cx_a.update(editor::init); -// cx_b.update(editor::init); + workspace_a.update(cx_a, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_b_id) + ); + }); + workspace_b.update(cx_b, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a_id) + ); + }); +} -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// "w.rs": "", -// "x.rs": "", -// }), -// ) -// .await; +#[gpui::test(iterations = 10)] +async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + // a and b join a channel/call + // a shares project 1 + // b shares project 2 + // + // b follows a: causes project 2 to be joined, and b to follow a. + // b opens a different file in project 2, a follows b + // b opens a different file in project 1, a cannot follow b + // b shares the project, a joins the project and follows b + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); -// client_b -// .fs() -// .insert_tree( -// "/b", -// json!({ -// "y.rs": "", -// "z.rs": "", -// }), -// ) -// .await; + client_a + .fs() + .insert_tree( + "/a", + json!({ + "w.rs": "", + "x.rs": "", + }), + ) + .await; -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); + client_b + .fs() + .insert_tree( + "/b", + json!({ + "y.rs": "", + "z.rs": "", + }), + ) + .await; -// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; -// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); -// let workspace_a = client_a -// .build_workspace(&project_a, cx_a) -// .root(cx_a) -// .unwrap(); -// let workspace_b = client_b -// .build_workspace(&project_b, cx_b) -// .root(cx_b) -// .unwrap(); + let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; + let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; -// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); -// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); -// active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); + cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); + cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); -// workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) -// }) -// .await -// .unwrap(); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) + }) + .await + .unwrap(); -// executor.run_until_parked(); -// assert_eq!(visible_push_notifications(cx_b).len(), 1); + executor.run_until_parked(); + assert_eq!(visible_push_notifications(cx_b).len(), 1); -// workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .follow(client_a.peer_id().unwrap(), cx) -// .unwrap() -// .detach() -// }); + workspace_b.update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx) + }); -// executor.run_until_parked(); -// let workspace_b_project_a = cx_b -// .windows() -// .iter() -// .max_by_key(|window| window.item_id()) -// .unwrap() -// .downcast::() -// .unwrap() -// .root(cx_b) -// .unwrap(); + executor.run_until_parked(); + let workspace_b_project_a = cx_b + .windows() + .iter() + .max_by_key(|window| window.window_id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_b) + .unwrap(); -// // assert that b is following a in project a in w.rs -// workspace_b_project_a.update(cx_b, |workspace, cx| { -// assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); -// assert_eq!( -// client_a.peer_id(), -// workspace.leader_for_pane(workspace.active_pane()) -// ); -// let item = workspace.active_item(cx).unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs")); -// }); + // assert that b is following a in project a in w.rs + workspace_b_project_a.update(cx_b, |workspace, cx| { + assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); + assert_eq!( + client_a.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("w.rs") + ); + }); -// // TODO: in app code, this would be done by the collab_ui. -// active_call_b -// .update(cx_b, |call, cx| { -// let project = workspace_b_project_a.read(cx).project().clone(); -// call.set_location(Some(&project), cx) -// }) -// .await -// .unwrap(); + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b_project_a.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); -// // assert that there are no share notifications open -// assert_eq!(visible_push_notifications(cx_b).len(), 0); + // assert that there are no share notifications open + assert_eq!(visible_push_notifications(cx_b).len(), 0); -// // b moves to x.rs in a's project, and a follows -// workspace_b_project_a -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) -// }) -// .await -// .unwrap(); + // b moves to x.rs in a's project, and a follows + workspace_b_project_a + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) + }) + .await + .unwrap(); -// executor.run_until_parked(); -// workspace_b_project_a.update(cx_b, |workspace, cx| { -// let item = workspace.active_item(cx).unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); -// }); + executor.run_until_parked(); + workspace_b_project_a.update(cx_b, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("x.rs") + ); + }); -// workspace_a.update(cx_a, |workspace, cx| { -// workspace -// .follow(client_b.peer_id().unwrap(), cx) -// .unwrap() -// .detach() -// }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.follow(client_b.peer_id().unwrap(), cx) + }); -// executor.run_until_parked(); -// workspace_a.update(cx_a, |workspace, cx| { -// assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); -// assert_eq!( -// client_b.peer_id(), -// workspace.leader_for_pane(workspace.active_pane()) -// ); -// let item = workspace.active_pane().read(cx).active_item().unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into()); -// }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_pane().read(cx).active_item().unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs"); + }); -// // b moves to y.rs in b's project, a is still following but can't yet see -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) -// }) -// .await -// .unwrap(); + // b moves to y.rs in b's project, a is still following but can't yet see + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) + }) + .await + .unwrap(); -// // TODO: in app code, this would be done by the collab_ui. -// active_call_b -// .update(cx_b, |call, cx| { -// let project = workspace_b.read(cx).project().clone(); -// call.set_location(Some(&project), cx) -// }) -// .await -// .unwrap(); + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); -// let project_b_id = active_call_b -// .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) -// .await -// .unwrap(); + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); -// executor.run_until_parked(); -// assert_eq!(visible_push_notifications(cx_a).len(), 1); -// cx_a.update(|cx| { -// workspace::join_remote_project( -// project_b_id, -// client_b.user_id().unwrap(), -// client_a.app_state.clone(), -// cx, -// ) -// }) -// .await -// .unwrap(); + executor.run_until_parked(); + assert_eq!(visible_push_notifications(cx_a).len(), 1); + cx_a.update(|cx| { + workspace::join_remote_project( + project_b_id, + client_b.user_id().unwrap(), + client_a.app_state.clone(), + cx, + ) + }) + .await + .unwrap(); -// executor.run_until_parked(); + executor.run_until_parked(); -// assert_eq!(visible_push_notifications(cx_a).len(), 0); -// let workspace_a_project_b = cx_a -// .windows() -// .iter() -// .max_by_key(|window| window.item_id()) -// .unwrap() -// .downcast::() -// .unwrap() -// .root(cx_a) -// .unwrap(); + assert_eq!(visible_push_notifications(cx_a).len(), 0); + let workspace_a_project_b = cx_a + .windows() + .iter() + .max_by_key(|window| window.window_id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_a) + .unwrap(); -// workspace_a_project_b.update(cx_a, |workspace, cx| { -// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); -// assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); -// assert_eq!( -// client_b.peer_id(), -// workspace.leader_for_pane(workspace.active_pane()) -// ); -// let item = workspace.active_item(cx).unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); -// }); -// } + workspace_a_project_b.update(cx_a, |workspace, cx| { + assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!( + item.tab_description(0, cx).unwrap(), + SharedString::from("y.rs") + ); + }); +} // #[gpui::test] // async fn test_following_into_excluded_file( @@ -1781,30 +1702,28 @@ async fn test_basic_following( // }); // } -// fn visible_push_notifications( -// cx: &mut TestAppContext, -// ) -> Vec> { -// let mut ret = Vec::new(); -// for window in cx.windows() { -// window.update(cx, |window| { -// if let Some(handle) = window -// .root_view() -// .clone() -// .downcast::() -// { -// ret.push(handle) -// } -// }); -// } -// ret -// } +fn visible_push_notifications( + cx: &mut TestAppContext, +) -> Vec> { + let mut ret = Vec::new(); + for window in cx.windows() { + window + .update(cx, |window, _| { + if let Ok(handle) = window.downcast::() { + ret.push(handle) + } + }) + .unwrap(); + } + ret +} -// #[derive(Debug, PartialEq, Eq)] -// struct PaneSummary { -// active: bool, -// leader: Option, -// items: Vec<(bool, String)>, -// } +#[derive(Debug, PartialEq, Eq)] +struct PaneSummary { + active: bool, + leader: Option, + items: Vec<(bool, String)>, +} fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec)> { cx.read(|cx| { @@ -1830,33 +1749,33 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec }) } -// fn pane_summaries(workspace: &View, cx: &mut WindowContext<'_>) -> Vec { -// workspace.update(cx, |workspace, cx| { -// let active_pane = workspace.active_pane(); -// workspace -// .panes() -// .iter() -// .map(|pane| { -// let leader = workspace.leader_for_pane(pane); -// let active = pane == active_pane; -// let pane = pane.read(cx); -// let active_ix = pane.active_item_index(); -// PaneSummary { -// active, -// leader, -// items: pane -// .items() -// .enumerate() -// .map(|(ix, item)| { -// ( -// ix == active_ix, -// item.tab_description(0, cx) -// .map_or(String::new(), |s| s.to_string()), -// ) -// }) -// .collect(), -// } -// }) -// .collect() -// }) -// } +fn pane_summaries(workspace: &View, cx: &mut VisualTestContext<'_>) -> Vec { + workspace.update(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + workspace + .panes() + .iter() + .map(|pane| { + let leader = workspace.leader_for_pane(pane); + let active = pane == active_pane; + let pane = pane.read(cx); + let active_ix = pane.active_item_index(); + PaneSummary { + active, + leader, + items: pane + .items() + .enumerate() + .map(|(ix, item)| { + ( + ix == active_ix, + item.tab_description(0, cx) + .map_or(String::new(), |s| s.to_string()), + ) + }) + .collect(), + } + }) + .collect() + }) +} diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 44f303ac0bebfde1ded9c4deb1eebef40ad06633..cc87147692c5fc6f6c2bde5dcc0cd4a20392fbf9 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -223,6 +223,10 @@ impl TestAppContext { .unwrap(); } + pub fn windows(&self) -> Vec { + self.app.borrow().windows().clone() + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index cc683cacb68f0e471ef9b1fa5596581d81d3d0f3..e9b91c0810332b106217eee2d0005c39014a1f15 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -105,9 +105,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn activate(&self, _ignoring_other_apps: bool) { - unimplemented!() - } + fn activate(&self, _ignoring_other_apps: bool) {} fn hide(&self) { unimplemented!() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 69e30a6ccb65c215cd5a59efcb6821549233378d..76adc718f33f140e670c63a67317372c2a9b6bb4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2451,11 +2451,11 @@ impl Workspace { Some(leader_id) } - // pub fn is_being_followed(&self, peer_id: PeerId) -> bool { - // self.follower_states - // .values() - // .any(|state| state.leader_id == peer_id) - // } + pub fn is_being_followed(&self, peer_id: PeerId) -> bool { + self.follower_states + .values() + .any(|state| state.leader_id == peer_id) + } fn active_item_path_changed(&mut self, cx: &mut ViewContext) { let active_entry = self.active_project_path(cx); From c7568a7d371453ed98e21a4e87fe61300aae3c11 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Jan 2024 16:17:11 -0700 Subject: [PATCH 04/43] All the following tests Co-Authored-By: Max --- crates/collab/src/tests/following_tests.rs | 267 ++++++++++----------- 1 file changed, 128 insertions(+), 139 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 5dc145bf168f1c9e4926ac3e0556457ccaafcb11..01c3302e4d3b1e6d5cf02d65b03ad095acf1b893 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -6,6 +6,7 @@ use gpui::{ point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext, VisualTestContext, WindowContext, }; +use language::Capability; use live_kit_client::MacOSDisplay; use project::project_settings::ProjectSettings; use rpc::proto::PeerId; @@ -280,7 +281,7 @@ async fn test_basic_following( .get_open_buffer(&(worktree_id, "2.txt").into(), cx) .unwrap() }); - let mut result = MultiBuffer::new(0); + let mut result = MultiBuffer::new(0, Capability::ReadWrite); result.push_excerpts( buffer_a1, [ExcerptRange { @@ -1563,144 +1564,132 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut }); } -// #[gpui::test] -// async fn test_following_into_excluded_file( -// executor: BackgroundExecutor, -// mut cx_a: &mut TestAppContext, -// mut cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// for cx in [&mut cx_a, &mut cx_b] { -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, |project_settings| { -// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); -// }); -// }); -// }); -// } -// server -// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) -// .await; -// let active_call_a = cx_a.read(ActiveCall::global); -// let active_call_b = cx_b.read(ActiveCall::global); - -// cx_a.update(editor::init); -// cx_b.update(editor::init); - -// client_a -// .fs() -// .insert_tree( -// "/a", -// json!({ -// ".git": { -// "COMMIT_EDITMSG": "write your commit message here", -// }, -// "1.txt": "one\none\none", -// "2.txt": "two\ntwo\ntwo", -// "3.txt": "three\nthree\nthree", -// }), -// ) -// .await; -// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; -// active_call_a -// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// .await -// .unwrap(); - -// let project_id = active_call_a -// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// .await -// .unwrap(); -// let project_b = client_b.build_remote_project(project_id, cx_b).await; -// active_call_b -// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) -// .await -// .unwrap(); - -// let window_a = client_a.build_workspace(&project_a, cx_a); -// let workspace_a = window_a.root(cx_a).unwrap(); -// let peer_id_a = client_a.peer_id().unwrap(); -// let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b).unwrap(); - -// todo!("could be wrong") -// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); -// let cx_a = &mut cx_a; -// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); -// let cx_b = &mut cx_b; - -// // Client A opens editors for a regular file and an excluded file. -// let editor_for_regular = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, "1.txt"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor_for_excluded_a = workspace_a -// .update(cx_a, |workspace, cx| { -// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// // Client A updates their selections in those editors -// editor_for_regular.update(cx_a, |editor, cx| { -// editor.handle_input("a", cx); -// editor.handle_input("b", cx); -// editor.handle_input("c", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![3..2]); -// }); -// editor_for_excluded_a.update(cx_a, |editor, cx| { -// editor.select_all(&Default::default(), cx); -// editor.handle_input("new commit message", cx); -// editor.select_left(&Default::default(), cx); -// assert_eq!(editor.selections.ranges(cx), vec![18..17]); -// }); - -// // When client B starts following client A, currently visible file is replicated -// workspace_b -// .update(cx_b, |workspace, cx| { -// workspace.follow(peer_id_a, cx).unwrap() -// }) -// .await -// .unwrap(); - -// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert_eq!( -// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), -// Some((worktree_id, ".git/COMMIT_EDITMSG").into()) -// ); -// assert_eq!( -// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), -// vec![18..17] -// ); - -// // Changes from B to the excluded file are replicated in A's editor -// editor_for_excluded_b.update(cx_b, |editor, cx| { -// editor.handle_input("\nCo-Authored-By: B ", cx); -// }); -// executor.run_until_parked(); -// editor_for_excluded_a.update(cx_a, |editor, cx| { -// assert_eq!( -// editor.text(cx), -// "new commit messag\nCo-Authored-By: B " -// ); -// }); -// } +#[gpui::test] +async fn test_following_into_excluded_file( + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + for cx in [&mut cx_a, &mut cx_b] { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); + }); + }); + }); + } + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let peer_id_a = client_a.peer_id().unwrap(); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".git": { + "COMMIT_EDITMSG": "write your commit message here", + }, + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Client A opens editors for a regular file and an excluded file. + let editor_for_regular = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_for_excluded_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client A updates their selections in those editors + editor_for_regular.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_for_excluded_a.update(cx_a, |editor, cx| { + editor.select_all(&Default::default(), cx); + editor.handle_input("new commit message", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![18..17]); + }); + + // When client B starts following client A, currently visible file is replicated + workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx)); + executor.run_until_parked(); + + let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), + Some((worktree_id, ".git/COMMIT_EDITMSG").into()) + ); + assert_eq!( + editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![18..17] + ); + + // Changes from B to the excluded file are replicated in A's editor + editor_for_excluded_b.update(cx_b, |editor, cx| { + editor.handle_input("\nCo-Authored-By: B ", cx); + }); + executor.run_until_parked(); + editor_for_excluded_a.update(cx_a, |editor, cx| { + assert_eq!( + editor.text(cx), + "new commit messag\nCo-Authored-By: B " + ); + }); +} fn visible_push_notifications( cx: &mut TestAppContext, From 709682e8bcb7e1f9f71d63b450d0455f162e1e85 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Jan 2024 16:28:40 -0700 Subject: [PATCH 05/43] Tidy up TestContext lifecycle Co-Authored-By: Max --- crates/collab/src/tests/following_tests.rs | 18 ++++++++---- crates/editor/src/editor_tests.rs | 8 ++--- .../src/test/editor_lsp_test_context.rs | 24 +++++++-------- crates/editor/src/test/editor_test_context.rs | 12 ++++---- crates/file_finder/src/file_finder.rs | 2 +- crates/gpui/src/app/test_context.rs | 29 ++++++++++--------- crates/search/src/buffer_search.rs | 7 ++--- .../neovim_backed_binding_test_context.rs | 21 ++++++-------- .../src/test/neovim_backed_test_context.rs | 21 +++++++------- crates/vim/src/test/vim_test_context.rs | 18 ++++++------ 10 files changed, 82 insertions(+), 78 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 01c3302e4d3b1e6d5cf02d65b03ad095acf1b893..c9be683ee6e5a1cae53ed8933b69a7da39dcbe4f 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -4,7 +4,7 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{ point, BackgroundExecutor, Context, SharedString, TestAppContext, View, VisualContext, - VisualTestContext, WindowContext, + VisualTestContext, }; use language::Capability; use live_kit_client::MacOSDisplay; @@ -12,7 +12,6 @@ use project::project_settings::ProjectSettings; use rpc::proto::PeerId; use serde_json::json; use settings::SettingsStore; -use std::borrow::Cow; use workspace::{ dock::{test::TestPanel, DockPosition}, item::{test::TestItem, ItemHandle as _}, @@ -1433,6 +1432,15 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut }); executor.run_until_parked(); + let window_b_project_a = cx_b + .windows() + .iter() + .max_by_key(|window| window.window_id()) + .unwrap() + .clone(); + + let mut cx_b_project_a = VisualTestContext::from_window(window_b_project_a, cx_b); + let workspace_b_project_a = cx_b .windows() .iter() @@ -1444,7 +1452,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut .unwrap(); // assert that b is following a in project a in w.rs - workspace_b_project_a.update(cx_b, |workspace, cx| { + workspace_b_project_a.update(&mut cx_b_project_a, |workspace, cx| { assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); assert_eq!( client_a.peer_id(), @@ -1459,7 +1467,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut // TODO: in app code, this would be done by the collab_ui. active_call_b - .update(cx_b, |call, cx| { + .update(&mut cx_b_project_a, |call, cx| { let project = workspace_b_project_a.read(cx).project().clone(); call.set_location(Some(&project), cx) }) @@ -1738,7 +1746,7 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec }) } -fn pane_summaries(workspace: &View, cx: &mut VisualTestContext<'_>) -> Vec { +fn pane_summaries(workspace: &View, cx: &mut VisualTestContext) -> Vec { workspace.update(cx, |workspace, cx| { let active_pane = workspace.active_pane(); workspace diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a84b866e1f8139a8ca558ccd54ac57c1d6e08bdd..b64e3cccc3e6330429eefb54a410024cbc5e1ce6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8131,8 +8131,8 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo /// Handle completion request passing a marked string specifying where the completion /// should be triggered from using '|' character, what range should be replaced, and what completions /// should be returned using '<' and '>' to delimit the range -pub fn handle_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, +pub fn handle_completion_request( + cx: &mut EditorLspTestContext, marked_string: &str, completions: Vec<&'static str>, ) -> impl Future { @@ -8177,8 +8177,8 @@ pub fn handle_completion_request<'a>( } } -fn handle_resolve_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, +fn handle_resolve_completion_request( + cx: &mut EditorLspTestContext, edits: Option>, ) -> impl Future { let edits = edits.map(|edits| { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 7ee55cddba1edba9356be2c6773c3f097f57c1c8..70c1699b83d090ef24c74f393cd8530502b7ce02 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -21,19 +21,19 @@ use workspace::{AppState, Workspace, WorkspaceHandle}; use super::editor_test_context::{AssertionContextManager, EditorTestContext}; -pub struct EditorLspTestContext<'a> { - pub cx: EditorTestContext<'a>, +pub struct EditorLspTestContext { + pub cx: EditorTestContext, pub lsp: lsp::FakeLanguageServer, pub workspace: View, pub buffer_lsp_url: lsp::Url, } -impl<'a> EditorLspTestContext<'a> { +impl EditorLspTestContext { pub async fn new( mut language: Language, capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { let app_state = cx.update(AppState::test); cx.update(|cx| { @@ -110,8 +110,8 @@ impl<'a> EditorLspTestContext<'a> { pub async fn new_rust( capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { let language = Language::new( LanguageConfig { name: "Rust".into(), @@ -152,8 +152,8 @@ impl<'a> EditorLspTestContext<'a> { pub async fn new_typescript( capabilities: lsp::ServerCapabilities, - cx: &'a mut gpui::TestAppContext, - ) -> EditorLspTestContext<'a> { + cx: &mut gpui::TestAppContext, + ) -> EditorLspTestContext { let mut word_characters: HashSet = Default::default(); word_characters.insert('$'); word_characters.insert('#'); @@ -283,15 +283,15 @@ impl<'a> EditorLspTestContext<'a> { } } -impl<'a> Deref for EditorLspTestContext<'a> { - type Target = EditorTestContext<'a>; +impl Deref for EditorLspTestContext { + type Target = EditorTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a> DerefMut for EditorLspTestContext<'a> { +impl DerefMut for EditorLspTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index bd5acb99459c131d316a5376688f3d4bcb81da93..18916f844cec8dc379de5f56259940e183aad447 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -26,15 +26,15 @@ use util::{ use super::build_editor_with_project; -pub struct EditorTestContext<'a> { - pub cx: gpui::VisualTestContext<'a>, +pub struct EditorTestContext { + pub cx: gpui::VisualTestContext, pub window: AnyWindowHandle, pub editor: View, pub assertion_cx: AssertionContextManager, } -impl<'a> EditorTestContext<'a> { - pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { +impl EditorTestContext { + pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext { let fs = FakeFs::new(cx.executor()); // fs.insert_file("/file", "".to_owned()).await; fs.insert_tree( @@ -342,7 +342,7 @@ impl<'a> EditorTestContext<'a> { } } -impl<'a> Deref for EditorTestContext<'a> { +impl Deref for EditorTestContext { type Target = gpui::TestAppContext; fn deref(&self) -> &Self::Target { @@ -350,7 +350,7 @@ impl<'a> Deref for EditorTestContext<'a> { } } -impl<'a> DerefMut for EditorTestContext<'a> { +impl DerefMut for EditorTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 589f634d01b7db8c5889f134e9707eabad4943bc..323d4555671f1020b970eff091d28dd694977293 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1843,7 +1843,7 @@ mod tests { expected_matches: usize, expected_editor_title: &str, workspace: &View, - cx: &mut gpui::VisualTestContext<'_>, + cx: &mut gpui::VisualTestContext, ) -> Vec { let picker = open_file_picker(&workspace, cx); cx.simulate_input(input); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index cf6ecb62b6f8adb023c8ceb9a1b9ace5277b4a40..470315f887c6fd5e4c6d3523498dbe496ff041a8 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -483,21 +483,24 @@ impl View { } use derive_more::{Deref, DerefMut}; -#[derive(Deref, DerefMut)] -pub struct VisualTestContext<'a> { +#[derive(Deref, DerefMut, Clone)] +pub struct VisualTestContext { #[deref] #[deref_mut] - cx: &'a mut TestAppContext, + cx: TestAppContext, window: AnyWindowHandle, } -impl<'a> VisualTestContext<'a> { +impl<'a> VisualTestContext { pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() } - pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { - Self { cx, window } + pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { + Self { + cx: cx.clone(), + window, + } } pub fn run_until_parked(&self) { @@ -531,7 +534,7 @@ impl<'a> VisualTestContext<'a> { } } -impl<'a> Context for VisualTestContext<'a> { +impl Context for VisualTestContext { type Result = ::Result; fn new_model( @@ -582,7 +585,7 @@ impl<'a> Context for VisualTestContext<'a> { } } -impl<'a> VisualContext for VisualTestContext<'a> { +impl VisualContext for VisualTestContext { fn new_view( &mut self, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, @@ -591,7 +594,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { V: 'static + Render, { self.window - .update(self.cx, |_, cx| cx.new_view(build_view)) + .update(&mut self.cx, |_, cx| cx.new_view(build_view)) .unwrap() } @@ -601,7 +604,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R, ) -> Self::Result { self.window - .update(self.cx, |_, cx| cx.update_view(view, update)) + .update(&mut self.cx, |_, cx| cx.update_view(view, update)) .unwrap() } @@ -613,13 +616,13 @@ impl<'a> VisualContext for VisualTestContext<'a> { V: 'static + Render, { self.window - .update(self.cx, |_, cx| cx.replace_root_view(build_view)) + .update(&mut self.cx, |_, cx| cx.replace_root_view(build_view)) .unwrap() } fn focus_view(&mut self, view: &View) -> Self::Result<()> { self.window - .update(self.cx, |_, cx| { + .update(&mut self.cx, |_, cx| { view.read(cx).focus_handle(cx).clone().focus(cx) }) .unwrap() @@ -630,7 +633,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { V: crate::ManagedView, { self.window - .update(self.cx, |_, cx| { + .update(&mut self.cx, |_, cx| { view.update(cx, |_, cx| cx.emit(crate::DismissEvent)) }) .unwrap() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 3f1cbce1bded6c7b7517597606f176062c34e06b..c889f0a4a4c11d3f104e130e34e5b87c092565d6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1091,13 +1091,10 @@ mod tests { theme::init(theme::LoadThemes::JustBase, cx); }); } + fn init_test( cx: &mut TestAppContext, - ) -> ( - View, - View, - &mut VisualTestContext<'_>, - ) { + ) -> (View, View, &mut VisualTestContext) { init_globals(cx); let buffer = cx.new_model(|cx| { Buffer::new( diff --git a/crates/vim/src/test/neovim_backed_binding_test_context.rs b/crates/vim/src/test/neovim_backed_binding_test_context.rs index 15fce99aad3f4ea0e03129342a4bca48fba4166f..0f64a6c849a32b6764cd5d605b5568c64759d89d 100644 --- a/crates/vim/src/test/neovim_backed_binding_test_context.rs +++ b/crates/vim/src/test/neovim_backed_binding_test_context.rs @@ -4,30 +4,27 @@ use crate::state::Mode; use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES}; -pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> { - cx: NeovimBackedTestContext<'a>, +pub struct NeovimBackedBindingTestContext { + cx: NeovimBackedTestContext, keystrokes_under_test: [&'static str; COUNT], } -impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> { - pub fn new( - keystrokes_under_test: [&'static str; COUNT], - cx: NeovimBackedTestContext<'a>, - ) -> Self { +impl NeovimBackedBindingTestContext { + pub fn new(keystrokes_under_test: [&'static str; COUNT], cx: NeovimBackedTestContext) -> Self { Self { cx, keystrokes_under_test, } } - pub fn consume(self) -> NeovimBackedTestContext<'a> { + pub fn consume(self) -> NeovimBackedTestContext { self.cx } pub fn binding( self, keystrokes: [&'static str; NEW_COUNT], - ) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> { + ) -> NeovimBackedBindingTestContext { self.consume().binding(keystrokes) } @@ -80,15 +77,15 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> { } } -impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> { - type Target = NeovimBackedTestContext<'a>; +impl Deref for NeovimBackedBindingTestContext { + type Target = NeovimBackedTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> { +impl DerefMut for NeovimBackedBindingTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 7380537655b1e2765003aeaec37d7918c2607bfb..fe5c5db62f831a3725e753ef4df3d448c28c4e68 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -47,8 +47,8 @@ impl ExemptionFeatures { } } -pub struct NeovimBackedTestContext<'a> { - cx: VimTestContext<'a>, +pub struct NeovimBackedTestContext { + cx: VimTestContext, // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which // bindings are exempted. If None, all bindings are ignored for that insertion text. exemptions: HashMap>>, @@ -60,8 +60,8 @@ pub struct NeovimBackedTestContext<'a> { is_dirty: bool, } -impl<'a> NeovimBackedTestContext<'a> { - pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> { +impl NeovimBackedTestContext { + pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { // rust stores the name of the test on the current thread. // We use this to automatically name a file that will store // the neovim connection's requests/responses so that we can @@ -393,20 +393,20 @@ impl<'a> NeovimBackedTestContext<'a> { pub fn binding( self, keystrokes: [&'static str; COUNT], - ) -> NeovimBackedBindingTestContext<'a, COUNT> { + ) -> NeovimBackedBindingTestContext { NeovimBackedBindingTestContext::new(keystrokes, self) } } -impl<'a> Deref for NeovimBackedTestContext<'a> { - type Target = VimTestContext<'a>; +impl Deref for NeovimBackedTestContext { + type Target = VimTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a> DerefMut for NeovimBackedTestContext<'a> { +impl DerefMut for NeovimBackedTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } @@ -415,7 +415,7 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> { // a common mistake in tests is to call set_shared_state when // you mean asswert_shared_state. This notices that and lets // you know. -impl<'a> Drop for NeovimBackedTestContext<'a> { +impl Drop for NeovimBackedTestContext { fn drop(&mut self) { if self.is_dirty { panic!("Test context was dropped after set_shared_state before assert_shared_state") @@ -425,9 +425,8 @@ impl<'a> Drop for NeovimBackedTestContext<'a> { #[cfg(test)] mod test { - use gpui::TestAppContext; - use crate::test::NeovimBackedTestContext; + use gpui::TestAppContext; #[gpui::test] async fn neovim_backed_test_context_works(cx: &mut TestAppContext) { diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 21b041b2451f5dce4d930ebbc9c66705ed5a22a2..5ed5296bff44d3e76c32f2a4b768afd760d1d121 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -10,11 +10,11 @@ use search::BufferSearchBar; use crate::{state::Operator, *}; -pub struct VimTestContext<'a> { - cx: EditorLspTestContext<'a>, +pub struct VimTestContext { + cx: EditorLspTestContext, } -impl<'a> VimTestContext<'a> { +impl VimTestContext { pub fn init(cx: &mut gpui::TestAppContext) { if cx.has_global::() { dbg!("OOPS"); @@ -29,13 +29,13 @@ impl<'a> VimTestContext<'a> { }); } - pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> { + pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext { Self::init(cx); let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await; Self::new_with_lsp(lsp, enabled) } - pub async fn new_typescript(cx: &'a mut gpui::TestAppContext) -> VimTestContext<'a> { + pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext { Self::init(cx); Self::new_with_lsp( EditorLspTestContext::new_typescript(Default::default(), cx).await, @@ -43,7 +43,7 @@ impl<'a> VimTestContext<'a> { ) } - pub fn new_with_lsp(mut cx: EditorLspTestContext<'a>, enabled: bool) -> VimTestContext<'a> { + pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext { cx.update(|cx| { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); @@ -162,15 +162,15 @@ impl<'a> VimTestContext<'a> { } } -impl<'a> Deref for VimTestContext<'a> { - type Target = EditorTestContext<'a>; +impl Deref for VimTestContext { + type Target = EditorTestContext; fn deref(&self) -> &Self::Target { &self.cx } } -impl<'a> DerefMut for VimTestContext<'a> { +impl DerefMut for VimTestContext { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cx } From 385cbfea2d9065ea1f9ca4bce595d777574c5c63 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 5 Jan 2024 16:35:20 -0700 Subject: [PATCH 06/43] Tidy up context usage Co-Authored-By: Max --- crates/collab/src/tests/following_tests.rs | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index c9be683ee6e5a1cae53ed8933b69a7da39dcbe4f..0486e294619fd4fd34604c6cc4113fb03f1057ad 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1439,20 +1439,16 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut .unwrap() .clone(); - let mut cx_b_project_a = VisualTestContext::from_window(window_b_project_a, cx_b); + let mut cx_b2 = VisualTestContext::from_window(window_b_project_a.clone(), cx_b); - let workspace_b_project_a = cx_b - .windows() - .iter() - .max_by_key(|window| window.window_id()) - .unwrap() + let workspace_b_project_a = window_b_project_a .downcast::() .unwrap() .root(cx_b) .unwrap(); // assert that b is following a in project a in w.rs - workspace_b_project_a.update(&mut cx_b_project_a, |workspace, cx| { + workspace_b_project_a.update(&mut cx_b2, |workspace, cx| { assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); assert_eq!( client_a.peer_id(), @@ -1467,7 +1463,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut // TODO: in app code, this would be done by the collab_ui. active_call_b - .update(&mut cx_b_project_a, |call, cx| { + .update(&mut cx_b2, |call, cx| { let project = workspace_b_project_a.read(cx).project().clone(); call.set_location(Some(&project), cx) }) @@ -1479,14 +1475,14 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut // b moves to x.rs in a's project, and a follows workspace_b_project_a - .update(cx_b, |workspace, cx| { + .update(&mut cx_b2, |workspace, cx| { workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) }) .await .unwrap(); executor.run_until_parked(); - workspace_b_project_a.update(cx_b, |workspace, cx| { + workspace_b_project_a.update(&mut cx_b2, |workspace, cx| { let item = workspace.active_item(cx).unwrap(); assert_eq!( item.tab_description(0, cx).unwrap(), @@ -1547,17 +1543,20 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 0); - let workspace_a_project_b = cx_a + let window_a_project_b = cx_a .windows() .iter() .max_by_key(|window| window.window_id()) .unwrap() + .clone(); + let cx_a2 = &mut VisualTestContext::from_window(window_a_project_b.clone(), cx_a); + let workspace_a_project_b = window_a_project_b .downcast::() .unwrap() .root(cx_a) .unwrap(); - workspace_a_project_b.update(cx_a, |workspace, cx| { + workspace_a_project_b.update(cx_a2, |workspace, cx| { assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); assert_eq!( From e549ef0ee99ff48572f2dbbd310a0d8fd9605665 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 01:50:59 +0200 Subject: [PATCH 07/43] Restore tooltipts for all collab buttons --- crates/collab_ui/src/collab_titlebar_item.rs | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 132410a62a833d80fed864253fafc0e7268e11a1..b8e36fbb741e9a9edc9c690aa079c6545e382715 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -184,6 +184,16 @@ impl Render for CollabTitlebarItem { "toggle_sharing", if is_shared { "Unshare" } else { "Share" }, ) + .tooltip(move |cx| { + Tooltip::text( + if is_shared { + "Stop sharing project with call participants" + } else { + "Share project with call participants" + }, + cx, + ) + }) .style(ButtonStyle::Subtle) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) .selected(is_shared) @@ -202,6 +212,7 @@ impl Render for CollabTitlebarItem { .child( IconButton::new("leave-call", ui::Icon::Exit) .style(ButtonStyle::Subtle) + .tooltip(|cx| Tooltip::text("Leave call", cx)) .icon_size(IconSize::Small) .on_click(move |_, cx| { ActiveCall::global(cx) @@ -219,6 +230,16 @@ impl Render for CollabTitlebarItem { ui::Icon::Mic }, ) + .tooltip(move |cx| { + Tooltip::text( + if is_muted { + "Unmute microphone" + } else { + "Mute microphone" + }, + cx, + ) + }) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .selected(is_muted) @@ -260,6 +281,16 @@ impl Render for CollabTitlebarItem { .icon_size(IconSize::Small) .selected(is_screen_sharing) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .tooltip(move |cx| { + Tooltip::text( + if is_screen_sharing { + "Stop Sharing Screen" + } else { + "Share Screen" + }, + cx, + ) + }) .on_click(move |_, cx| { crate::toggle_screen_sharing(&Default::default(), cx) }), From 669293e749382eb65b01444bdd5ece92e187ee68 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 02:01:12 +0200 Subject: [PATCH 08/43] Screenshare item background is now of editor background's color --- crates/workspace/src/shared_screen.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index dbbe7de6a1897d0049ef65e500e7ab4483452a13..edfabed60d3a03e2290fb94dc9c8482a7e9b4a5e 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -66,12 +66,16 @@ impl FocusableView for SharedScreen { } } impl Render for SharedScreen { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - div().track_focus(&self.focus).size_full().children( - self.frame - .as_ref() - .map(|frame| img(frame.image()).size_full()), - ) + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus) + .size_full() + .children( + self.frame + .as_ref() + .map(|frame| img(frame.image()).size_full()), + ) } } From 436a2817561aa1e3ee152efdeb2e9afbc46135fd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Jan 2024 17:18:52 -0700 Subject: [PATCH 09/43] Align the assistant message headers with the editable message content Since the message headers are buttons, we need to shift them relatively to compensate for the fact that the background is only visible when hovered. I'm ok with the background not being aligned so long as the unhovered text is. --- crates/assistant/src/assistant_panel.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 452538b9103e99f1f8dd281ffcfb26f35e2f9540..58e7ddc38292b6b88a601e0b23483621a3bb046a 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -29,7 +29,7 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, + canvas, div, point, relative, rems, rgba, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, @@ -2325,12 +2325,16 @@ impl ConversationEditor { } }); - h_stack() + div() + .h_flex() .id(("message_header", message_id.0)) .h_11() + .relative() .gap_1() - .p_1() - .child(sender) + // Sender is a button with a padding of 1, but only has a background on hover, + // so we shift it left by the same amount to align the text with the content + // in the un-hovered state. + .child(div().child(sender).relative().neg_left_1()) // TODO: Only show this if the message if the message has been sent .child( Label::new( @@ -2538,7 +2542,7 @@ impl Render for ConversationEditor { .child( div() .size_full() - .pl_2() + .pl_4() .bg(cx.theme().colors().editor_background) .child(self.editor.clone()), ) From aaada7d5087ba75f5a16cee1f3a0230f138749e8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 5 Jan 2024 17:22:59 -0700 Subject: [PATCH 10/43] Implement From for Fill --- crates/assistant/src/assistant_panel.rs | 2 +- crates/gpui/src/style.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 58e7ddc38292b6b88a601e0b23483621a3bb046a..385c6f5239435d968b2fd9baa28c96b069d3eab9 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -29,7 +29,7 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - canvas, div, point, relative, rems, rgba, uniform_list, Action, AnyElement, AppContext, + canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 7e9504d43466570d96ad5441263c8d02b2e555e2..244ccebf2498fb9ff275d0818215a9ba658ffc02 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -561,6 +561,12 @@ impl From for Fill { } } +impl From for Fill { + fn from(color: Rgba) -> Self { + Self::Color(color.into()) + } +} + impl From for HighlightStyle { fn from(other: TextStyle) -> Self { Self::from(&other) From 0d7f3ef278e0f5483e5741488f97418ade147b50 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 11:39:27 +0200 Subject: [PATCH 11/43] Fix base keymap selector keyboard shortcuts --- crates/welcome/src/base_keymap_picker.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 9a8edf0eb33b21b4d324c8390e638febe777e643..b798325473be65091a1c17a4cd6d4b1e435c015d 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -36,13 +36,12 @@ pub fn toggle( } pub struct BaseKeymapSelector { - focus_handle: gpui::FocusHandle, picker: View>, } impl FocusableView for BaseKeymapSelector { - fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.picker.focus_handle(cx) } } @@ -55,17 +54,13 @@ impl BaseKeymapSelector { cx: &mut ViewContext, ) -> Self { let picker = cx.new_view(|cx| Picker::new(delegate, cx)); - let focus_handle = cx.focus_handle(); - Self { - focus_handle, - picker, - } + Self { picker } } } impl Render for BaseKeymapSelector { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - self.picker.clone() + v_stack().w(rems(34.)).child(self.picker.clone()) } } @@ -184,7 +179,13 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { .ok(); } - fn dismissed(&mut self, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.view + .update(cx, |_, cx| { + cx.emit(DismissEvent); + }) + .log_err(); + } fn render_match( &self, From ee336cb87f83ca4ddf27c0fcd659a3c5ae3457fc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 11:56:01 +0200 Subject: [PATCH 12/43] Add spaces between leave call and call status icons, and call status icons and user menu --- crates/collab_ui/src/collab_titlebar_item.rs | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index b8e36fbb741e9a9edc9c690aa079c6545e382715..beda9c78e8f0e69795f6f89bbc4e413b79e9484a 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -210,15 +210,19 @@ impl Render for CollabTitlebarItem { ) }) .child( - IconButton::new("leave-call", ui::Icon::Exit) - .style(ButtonStyle::Subtle) - .tooltip(|cx| Tooltip::text("Leave call", cx)) - .icon_size(IconSize::Small) - .on_click(move |_, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }), + div() + .child( + IconButton::new("leave-call", ui::Icon::Exit) + .style(ButtonStyle::Subtle) + .tooltip(|cx| Tooltip::text("Leave call", cx)) + .icon_size(IconSize::Small) + .on_click(move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }), + ) + .pr_2(), ) .when(!read_only, |this| { this.child( @@ -296,6 +300,7 @@ impl Render for CollabTitlebarItem { }), ) }) + .child(div().pr_2()) }) .map(|el| { let status = self.client.status(); From ae14f7bd92f99c080ab40fdb1ff9725336f3d7bd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 12:07:48 +0200 Subject: [PATCH 13/43] Add space between menus and player stack --- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index beda9c78e8f0e69795f6f89bbc4e413b79e9484a..60506f2bbbaf5ace32a60546d9677c74a1429d0b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -92,7 +92,7 @@ impl Render for CollabTitlebarItem { .gap_1() .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) - .children(self.render_project_branch(cx)) + .child(div().pr_1().children(self.render_project_branch(cx))) .when_some( current_user.clone().zip(client.peer_id()).zip(room.clone()), |this, ((current_user, peer_id), room)| { From d86ccb1afcc56518e498e71f178272f22df91812 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 12:54:40 +0200 Subject: [PATCH 14/43] Reduce the height of the collaborators' color ribbon --- crates/collab_ui/src/collab_titlebar_item.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 60506f2bbbaf5ace32a60546d9677c74a1429d0b..6ccad2db0d107f4ee877ecdfd38563e880a79be5 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -41,6 +41,7 @@ pub fn init(cx: &mut AppContext) { workspace.set_titlebar_item(titlebar_item.into(), cx) }) .detach(); + // todo!() // cx.add_action(CollabTitlebarItem::share_project); // cx.add_action(CollabTitlebarItem::unshare_project); // cx.add_action(CollabTitlebarItem::toggle_user_menu); @@ -320,11 +321,19 @@ impl Render for CollabTitlebarItem { fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas { let color = colors.color_for_participant(participant_index.0).cursor; canvas(move |bounds, cx| { - let mut path = Path::new(bounds.lower_left()); let height = bounds.size.height; - path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin); - path.line_to(bounds.upper_right() - point(height, px(0.))); - path.curve_to(bounds.lower_right(), bounds.upper_right()); + let horizontal_offset = height; + let vertical_offset = px(height.0 / 2.0); + let mut path = Path::new(bounds.lower_left()); + path.curve_to( + bounds.origin + point(horizontal_offset, vertical_offset), + bounds.origin + point(px(0.0), vertical_offset), + ); + path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset)); + path.curve_to( + bounds.lower_right(), + bounds.upper_right() + point(px(0.0), vertical_offset), + ); path.line_to(bounds.lower_left()); cx.paint_path(path, color); }) From 23414d185c0e9303f24af448effbe4ea2ac1ee94 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 6 Jan 2024 18:56:55 +0100 Subject: [PATCH 15/43] Fix bug that was causing `Editor` to notify on every mouse move --- crates/editor/src/display_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 8703f1ba40aa06aa0d346606af6098896fab3dfe..4511ffe407849162b603c4b3a44d51e8d552c1c9 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -240,7 +240,7 @@ impl DisplayMap { } pub fn clear_highlights(&mut self, type_id: TypeId) -> bool { let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some(); - cleared |= self.inlay_highlights.remove(&type_id).is_none(); + cleared |= self.inlay_highlights.remove(&type_id).is_some(); cleared } From cdd5cb16ed896b2ee3bbb041983ee7cb812f6991 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 14:41:35 -0500 Subject: [PATCH 16/43] WIP --- crates/assistant/src/assistant_panel.rs | 2 +- crates/call/src/call.rs | 16 +- crates/client/src/client.rs | 3 +- crates/client/src/telemetry.rs | 123 +++--- crates/client/src/user.rs | 1 - crates/collab_ui/src/collab_ui.rs | 4 +- crates/editor/src/editor.rs | 3 +- crates/editor/src/items.rs | 1 + crates/theme_selector/src/theme_selector.rs | 2 +- crates/welcome/src/base_keymap_picker.rs | 11 +- crates/welcome/src/base_keymap_setting.rs | 14 + crates/welcome/src/welcome.rs | 400 ++++++++++++-------- crates/workspace/src/workspace.rs | 2 +- crates/zed/src/main.rs | 24 +- 14 files changed, 348 insertions(+), 258 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 385c6f5239435d968b2fd9baa28c96b069d3eab9..6e2ab02cad603e297d1392e89af65dc4fe516970 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -3542,5 +3542,5 @@ fn report_assistant_event( .default_open_ai_model .clone(); - telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name(), cx) + telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name()) } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index c419043a722b35fb34f33a224502057e53f3a16b..4f9ec080596b1a2ec962585e3c6e92ac1808389b 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -310,14 +310,14 @@ impl ActiveCall { }) } - pub fn decline_incoming(&mut self, cx: &mut ModelContext) -> Result<()> { + pub fn decline_incoming(&mut self, _cx: &mut ModelContext) -> Result<()> { let call = self .incoming_call .0 .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); + report_call_event_for_room("decline incoming", call.room_id, None, &self.client); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -467,7 +467,7 @@ impl ActiveCall { pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { if let Some(room) = self.room() { let room = room.read(cx); - report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client); } } } @@ -477,11 +477,10 @@ pub fn report_call_event_for_room( room_id: u64, channel_id: Option, client: &Arc, - cx: &mut AppContext, ) { let telemetry = client.telemetry(); - telemetry.report_call_event(operation, Some(room_id), channel_id, cx) + telemetry.report_call_event(operation, Some(room_id), channel_id) } pub fn report_call_event_for_channel( @@ -494,12 +493,7 @@ pub fn report_call_event_for_channel( let telemetry = client.telemetry(); - telemetry.report_call_event( - operation, - room.map(|r| r.read(cx).id()), - Some(channel_id), - cx, - ) + telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id)) } #[cfg(test)] diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 2f1e234b7303c1787fe2e6ec806ad94218e9e196..d070cae37547aa4c908635dbe42f88406bd75301 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -501,8 +501,7 @@ impl Client { })); } Status::SignedOut | Status::UpgradeRequired => { - cx.update(|cx| self.telemetry.set_authenticated_user_info(None, false, cx)) - .log_err(); + self.telemetry.set_authenticated_user_info(None, false); state._reconnect_task.take(); } _ => {} diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 2391c5f3b55a0c96133ed82ae964ecd969cc68e2..6b6b4b0a08d24f8a147076558a2cd46f835607b0 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -5,7 +5,7 @@ use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; -use settings::Settings; +use settings::{Settings, SettingsStore}; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, @@ -17,10 +17,11 @@ use util::{channel::ReleaseChannel, TryFutureExt}; pub struct Telemetry { http_client: Arc, executor: BackgroundExecutor, - state: Mutex, + state: Arc>, } struct TelemetryState { + settings: TelemetrySettings, metrics_id: Option>, // Per logged-in user installation_id: Option>, // Per app installation (different for dev, nightly, preview, and stable) session_id: Option>, // Per app launch @@ -139,45 +140,60 @@ impl Telemetry { None }; + TelemetrySettings::register(cx); + + let state = Arc::new(Mutex::new(TelemetryState { + settings: TelemetrySettings::get_global(cx).clone(), + app_metadata: cx.app_metadata(), + architecture: env::consts::ARCH, + release_channel, + installation_id: None, + metrics_id: None, + session_id: None, + clickhouse_events_queue: Default::default(), + flush_clickhouse_events_task: Default::default(), + log_file: None, + is_staff: None, + first_event_datetime: None, + })); + + cx.observe_global::({ + let state = state.clone(); + + move |cx| { + let mut state = state.lock(); + state.settings = TelemetrySettings::get_global(cx).clone(); + } + }) + .detach(); + // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { http_client: client, executor: cx.background_executor().clone(), - state: Mutex::new(TelemetryState { - app_metadata: cx.app_metadata(), - architecture: env::consts::ARCH, - release_channel, - installation_id: None, - metrics_id: None, - session_id: None, - clickhouse_events_queue: Default::default(), - flush_clickhouse_events_task: Default::default(), - log_file: None, - is_staff: None, - first_event_datetime: None, - }), + state, }); // We should only ever have one instance of Telemetry, leak the subscription to keep it alive // rather than store in TelemetryState, complicating spawn as subscriptions are not Send std::mem::forget(cx.on_app_quit({ let this = this.clone(); - move |cx| this.shutdown_telemetry(cx) + move |_| this.shutdown_telemetry() })); this } #[cfg(any(test, feature = "test-support"))] - fn shutdown_telemetry(self: &Arc, _: &mut AppContext) -> impl Future { + fn shutdown_telemetry(self: &Arc) -> impl Future { Task::ready(()) } // Skip calling this function in tests. // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings #[cfg(not(any(test, feature = "test-support")))] - fn shutdown_telemetry(self: &Arc, cx: &mut AppContext) -> impl Future { - self.report_app_event("close", true, cx); + fn shutdown_telemetry(self: &Arc) -> impl Future { + self.report_app_event("close", true); Task::ready(()) } @@ -197,7 +213,7 @@ impl Telemetry { drop(state); let this = self.clone(); - cx.spawn(|cx| async move { + cx.spawn(|_cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it let refresh_kind = RefreshKind::new() .with_memory() // For memory usage @@ -226,11 +242,8 @@ impl Telemetry { return; }; - cx.update(|cx| { - this.report_memory_event(process.memory(), process.virtual_memory(), cx); - this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32, cx); - }) - .ok(); + this.report_memory_event(process.memory(), process.virtual_memory()); + this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32); } }) .detach(); @@ -240,13 +253,13 @@ impl Telemetry { self: &Arc, metrics_id: Option, is_staff: bool, - cx: &AppContext, ) { - if !TelemetrySettings::get_global(cx).metrics { + let mut state = self.state.lock(); + + if !state.settings.metrics { return; } - let mut state = self.state.lock(); let metrics_id: Option> = metrics_id.map(|id| id.into()); state.metrics_id = metrics_id.clone(); state.is_staff = Some(is_staff); @@ -260,7 +273,6 @@ impl Telemetry { operation: &'static str, copilot_enabled: bool, copilot_enabled_for_language: bool, - cx: &AppContext, ) { let event = ClickhouseEvent::Editor { file_extension, @@ -271,7 +283,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } pub fn report_copilot_event( @@ -279,7 +291,6 @@ impl Telemetry { suggestion_id: Option, suggestion_accepted: bool, file_extension: Option, - cx: &AppContext, ) { let event = ClickhouseEvent::Copilot { suggestion_id, @@ -288,7 +299,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } pub fn report_assistant_event( @@ -296,7 +307,6 @@ impl Telemetry { conversation_id: Option, kind: AssistantKind, model: &'static str, - cx: &AppContext, ) { let event = ClickhouseEvent::Assistant { conversation_id, @@ -305,7 +315,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } pub fn report_call_event( @@ -313,7 +323,6 @@ impl Telemetry { operation: &'static str, room_id: Option, channel_id: Option, - cx: &AppContext, ) { let event = ClickhouseEvent::Call { operation, @@ -322,29 +331,23 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } - pub fn report_cpu_event( - self: &Arc, - usage_as_percentage: f32, - core_count: u32, - cx: &AppContext, - ) { + pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { let event = ClickhouseEvent::Cpu { usage_as_percentage, core_count, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } pub fn report_memory_event( self: &Arc, memory_in_bytes: u64, virtual_memory_in_bytes: u64, - cx: &AppContext, ) { let event = ClickhouseEvent::Memory { memory_in_bytes, @@ -352,36 +355,26 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } - pub fn report_app_event( - self: &Arc, - operation: &'static str, - immediate_flush: bool, - cx: &AppContext, - ) { + pub fn report_app_event(self: &Arc, operation: &'static str, immediate_flush: bool) { let event = ClickhouseEvent::App { operation, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, immediate_flush, cx) + self.report_clickhouse_event(event, immediate_flush) } - pub fn report_setting_event( - self: &Arc, - setting: &'static str, - value: String, - cx: &AppContext, - ) { + pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { let event = ClickhouseEvent::Setting { setting, value, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false, cx) + self.report_clickhouse_event(event, false) } fn milliseconds_since_first_event(&self) -> i64 { @@ -398,17 +391,13 @@ impl Telemetry { } } - fn report_clickhouse_event( - self: &Arc, - event: ClickhouseEvent, - immediate_flush: bool, - cx: &AppContext, - ) { - if !TelemetrySettings::get_global(cx).metrics { + fn report_clickhouse_event(self: &Arc, event: ClickhouseEvent, immediate_flush: bool) { + let mut state = self.state.lock(); + + if !state.settings.metrics { return; } - let mut state = self.state.lock(); let signed_in = state.metrics_id.is_some(); state .clickhouse_events_queue diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index b08d423cae0fb48a5ed2f06c9461662a46522ee1..1c288c875db39e3d3d7aed81ed836c1a69b41f5e 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -164,7 +164,6 @@ impl UserStore { client.telemetry.set_authenticated_user_info( Some(info.metrics_id.clone()), info.staff, - cx, ) } })?; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 6b81998a8adf0828f93e4cb74bed1aee78b61054..3c0473e67d0a687308c00097c554fc87f47645c1 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -58,7 +58,6 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { room.id(), room.channel_id(), &client, - cx, ); Task::ready(room.unshare_screen(cx)) } else { @@ -67,7 +66,6 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { room.id(), room.channel_id(), &client, - cx, ); room.share_screen(cx) } @@ -86,7 +84,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { } else { "disable microphone" }; - report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &client); room.toggle_mute(cx) }) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 455db1d7153d70a6f80ddbb48f880ea5addda39f..231f76218a44125e6c42f2a99f98027f98414ab1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8874,7 +8874,7 @@ impl Editor { let telemetry = project.read(cx).client().telemetry().clone(); - telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension, cx) + telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension) } #[cfg(any(test, feature = "test-support"))] @@ -8926,7 +8926,6 @@ impl Editor { operation, copilot_enabled, copilot_enabled_for_language, - cx, ) } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 78f9b150512e6ee153b3165e88bc162ea015aa14..a3f247b7b9233903d7b2b28e065b245b4a3d035c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -866,6 +866,7 @@ impl Item for Editor { } fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { + dbg!(event); match event { EditorEvent::Closed => f(ItemEvent::CloseItem), diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index cfb98ccd7455fd17c81a3de65a82b55f5d4bd877..2bb8c6648cab0ff1ef79f7f79cf5ee85da8af07c 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -182,7 +182,7 @@ impl PickerDelegate for ThemeSelectorDelegate { let theme_name = cx.theme().name.clone(); self.telemetry - .report_setting_event("theme", theme_name.to_string(), cx); + .report_setting_event("theme", theme_name.to_string()); update_settings_file::(self.fs.clone(), cx, move |settings| { settings.theme = Some(theme_name.to_string()); diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 9a8edf0eb33b21b4d324c8390e638febe777e643..58ad777757f102820f462b43d19e7398178b6f55 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -1,4 +1,5 @@ use super::base_keymap_setting::BaseKeymap; +use client::telemetry::Telemetry; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View, @@ -27,9 +28,10 @@ pub fn toggle( cx: &mut ViewContext, ) { let fs = workspace.app_state().fs.clone(); + let telemetry = workspace.client().telemetry().clone(); workspace.toggle_modal(cx, |cx| { BaseKeymapSelector::new( - BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx), + BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, telemetry, cx), cx, ) }); @@ -73,6 +75,7 @@ pub struct BaseKeymapSelectorDelegate { view: WeakView, matches: Vec, selected_index: usize, + telemetry: Arc, fs: Arc, } @@ -80,6 +83,7 @@ impl BaseKeymapSelectorDelegate { fn new( weak_view: WeakView, fs: Arc, + telemetry: Arc, cx: &mut ViewContext, ) -> Self { let base = BaseKeymap::get(None, cx); @@ -91,6 +95,7 @@ impl BaseKeymapSelectorDelegate { view: weak_view, matches: Vec::new(), selected_index, + telemetry, fs, } } @@ -172,6 +177,10 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { if let Some(selection) = self.matches.get(self.selected_index) { let base_keymap = BaseKeymap::from_names(&selection.string); + + self.telemetry + .report_setting_event("keymap", base_keymap.to_string()); + update_settings_file::(self.fs.clone(), cx, move |setting| { *setting = Some(base_keymap) }); diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs index cad6e894f95d7c0621f703fa28ed34a6d7726764..411caa820e34e2cc080c160c39f4053161901e92 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/welcome/src/base_keymap_setting.rs @@ -1,3 +1,5 @@ +use std::fmt::{Display, Formatter}; + use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -12,6 +14,18 @@ pub enum BaseKeymap { TextMate, } +impl Display for BaseKeymap { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BaseKeymap::VSCode => write!(f, "VSCode"), + BaseKeymap::JetBrains => write!(f, "JetBrains"), + BaseKeymap::SublimeText => write!(f, "Sublime Text"), + BaseKeymap::Atom => write!(f, "Atom"), + BaseKeymap::TextMate => write!(f, "TextMate"), + } + } +} + impl BaseKeymap { pub const OPTIONS: [(&'static str, Self); 5] = [ ("VSCode (Default)", Self::VSCode), diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index d096248a28935224de782dcdd9b62d440d50730f..71d98e6c9d42d13c86d61a84ca85ff97340ac737 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,7 +1,7 @@ mod base_keymap_picker; mod base_keymap_setting; -use client::TelemetrySettings; +use client::{telemetry::Telemetry, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use gpui::{ svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, @@ -14,7 +14,7 @@ use ui::{prelude::*, Checkbox}; use vim::VimModeSetting; use workspace::{ dock::DockPosition, - item::{Item, ItemEvent}, + item::{Item, ItemEvent, ItemHandle}, open_new, AppState, Welcome, Workspace, WorkspaceId, }; @@ -27,7 +27,7 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(|workspace, _: &Welcome, cx| { - let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx)); + let welcome_page = WelcomePage::new(workspace, cx); workspace.add_item(Box::new(welcome_page), cx) }); }) @@ -39,7 +39,7 @@ pub fn init(cx: &mut AppContext) { pub fn show_welcome_view(app_state: &Arc, cx: &mut AppContext) { open_new(&app_state, cx, |workspace, cx| { workspace.toggle_dock(DockPosition::Left, cx); - let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx)); + let welcome_page = WelcomePage::new(workspace, cx); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); cx.focus_view(&welcome_page); cx.notify(); @@ -54,174 +54,248 @@ pub fn show_welcome_view(app_state: &Arc, cx: &mut AppContext) { pub struct WelcomePage { workspace: WeakView, focus_handle: FocusHandle, + telemetry: Arc, _settings_subscription: Subscription, } impl Render for WelcomePage { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - h_stack() - .full() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus_handle) - .child( - v_stack() - .w_96() - .gap_4() - .mx_auto() - .child( - svg() - .path("icons/logo_96.svg") - .text_color(gpui::white()) - .w(px(96.)) - .h(px(96.)) - .mx_auto(), - ) - .child( - h_stack() - .justify_center() - .child(Label::new("Code at the speed of thought")), - ) - .child( - v_stack() - .gap_2() - .child( - Button::new("choose-theme", "Choose a theme") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - })), - ) - .child( - Button::new("choose-keymap", "Choose a keymap") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.workspace - .update(cx, |workspace, cx| { - base_keymap_picker::toggle( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - })), - ) - .child( - Button::new("install-cli", "Install the CLI") - .full_width() - .on_click(cx.listener(|_, _, cx| { - cx.app_mut() - .spawn(|cx| async move { - install_cli::install_cli(&cx).await - }) - .detach_and_log_err(cx); - })), - ), - ) - .child( - v_stack() - .p_3() - .gap_2() - .bg(cx.theme().colors().elevated_surface_background) - .border_1() - .border_color(cx.theme().colors().border) - .rounded_md() - .child( - h_stack() - .gap_2() - .child( - Checkbox::new( - "enable-vim", - if VimModeSetting::get_global(cx).0 { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, + h_stack().full().track_focus(&self.focus_handle).child( + v_stack() + .w_96() + .gap_4() + .mx_auto() + .child( + svg() + .path("icons/logo_96.svg") + .text_color(gpui::white()) + .w(px(96.)) + .h(px(96.)) + .mx_auto(), + ) + .child( + h_stack() + .justify_center() + .child(Label::new("Code at the speed of thought")), + ) + .child( + v_stack() + .gap_2() + .child( + Button::new("choose-theme", "Choose a theme") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry + .report_app_event("welcome page button: theme", false); + this.workspace + .update(cx, |workspace, cx| { + theme_selector::toggle( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + })), + ) + .child( + Button::new("choose-keymap", "Choose a keymap") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry + .report_app_event("welcome page button: keymap", false); + this.workspace + .update(cx, |workspace, cx| { + base_keymap_picker::toggle( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + })), + ) + .child( + Button::new("install-cli", "Install the CLI") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry.report_app_event( + "welcome page button: install cli", + false, + ); + cx.app_mut() + .spawn( + |cx| async move { install_cli::install_cli(&cx).await }, ) - .on_click( - cx.listener(move |this, selection, cx| { - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }), - ), + .detach_and_log_err(cx); + })), + ), + ) + .child( + v_stack() + .p_3() + .gap_2() + .bg(cx.theme().colors().elevated_surface_background) + .border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .child( + h_stack() + .gap_2() + .child( + Checkbox::new( + "enable-vim", + if VimModeSetting::get_global(cx).0 { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, ) - .child(Label::new("Enable vim mode")), - ) - .child( - h_stack() - .gap_2() - .child( - Checkbox::new( - "enable-telemetry", - if TelemetrySettings::get_global(cx).metrics { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, - ) - .on_click( - cx.listener(move |this, selection, cx| { - this.update_settings::( - selection, - cx, - |settings, value| { - settings.metrics = Some(value) - }, - ); - }), - ), + .on_click(cx.listener( + move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page button: vim", + false, + ); + this.update_settings::( + selection, + cx, + |setting, value| *setting = Some(value), + ); + }, + )), + ) + .child(Label::new("Enable vim mode")), + ) + .child( + h_stack() + .gap_2() + .child( + Checkbox::new( + "enable-telemetry", + if TelemetrySettings::get_global(cx).metrics { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, ) - .child(Label::new("Send anonymous usage data")), - ) - .child( - h_stack() - .gap_2() - .child( - Checkbox::new( - "enable-crash", - if TelemetrySettings::get_global(cx).diagnostics { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, - ) - .on_click( - cx.listener(move |this, selection, cx| { - this.update_settings::( - selection, - cx, - |settings, value| { - settings.diagnostics = Some(value) - }, - ); - }), - ), + .on_click(cx.listener( + move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page button: user telemetry", + false, + ); + this.update_settings::( + selection, + cx, + { + let telemetry = this.telemetry.clone(); + + move |settings, value| { + settings.metrics = Some(value); + + telemetry.report_setting_event( + "user telemetry", + value.to_string(), + ); + } + }, + ); + }, + )), + ) + .child(Label::new("Send anonymous usage data")), + ) + .child( + h_stack() + .gap_2() + .child( + Checkbox::new( + "enable-crash", + if TelemetrySettings::get_global(cx).diagnostics { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, ) - .child(Label::new("Send crash reports")), - ), - ), - ) + .on_click(cx.listener( + move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page button: crash diagnostics", + false, + ); + this.update_settings::( + selection, + cx, + { + let telemetry = this.telemetry.clone(); + + move |settings, value| { + settings.diagnostics = Some(value); + + telemetry.report_setting_event( + "crash diagnostics", + value.to_string(), + ); + } + }, + ); + }, + )), + ) + .child(Label::new("Send crash reports")), + ), + ), + ) } } impl WelcomePage { - pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - WelcomePage { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { + let this = cx.new_view(|cx| WelcomePage { focus_handle: cx.focus_handle(), workspace: workspace.weak_handle(), + telemetry: workspace.client().telemetry().clone(), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - } + }); + + this.on_release( + cx, + Box::new(|cx| { + this.update(cx, |this, _| { + this.telemetry.report_app_event("close welcome page", false); + }) + }), + ) + .detach(); + + // this.subscribe_to_item_events( + // cx, + // Box::new(|event: ItemEvent, cx| { + // // if event == ItemEvent::CloseItem { + // dbg!(event); + // // welcome.update(cx, |welcome, _| { + // // welcome + // // .telemetry + // // .report_app_event("close welcome page", false); + // // }) + // // } + // }), + // ) + // .detach(); + + cx.subscribe(&this, |_, welcome, event, cx| { + if *event == ItemEvent::CloseItem { + welcome.update(cx, |welcome, _| { + welcome + .telemetry + .report_app_event("close welcome page", false); + }) + } + }) + .detach(); + + this } fn update_settings( @@ -279,6 +353,7 @@ impl Item for WelcomePage { Some(cx.new_view(|cx| WelcomePage { focus_handle: cx.focus_handle(), workspace: self.workspace.clone(), + telemetry: self.telemetry.clone(), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), })) } @@ -287,3 +362,16 @@ impl Item for WelcomePage { f(*event) } } + +// TODO +// - [X] get theme value +// - [X] In selector +// - [X] In main +// - [ ] get value of keymap selector +// - [X] In selector +// - [X] In main +// - [ ] get all button clicks +// - [ ] get value of usage data enabled +// - [ ] get value of crash reports enabled +// - [ ] get welcome screen close +// - [ ] test all events diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6b29496f2cfd919a60e4947adb2f989fbb425486..653f777084228d06e272bc8e26575352da866a4b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1260,7 +1260,7 @@ impl Workspace { pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { self.client() .telemetry() - .report_app_event("open project", false, cx); + .report_app_event("open project", false); let paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e0da81edc4ae17702b55a306bae3ec8b9d7a2bfd..22436e10a973d676e80c641f704f416ccd8d17c5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -45,7 +45,7 @@ use util::{ paths, ResultExt, }; use uuid::Uuid; -use welcome::{show_welcome_view, FIRST_OPEN}; +use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed::{ app_menus, build_window_options, ensure_only_instance, handle_cli_connection, @@ -171,17 +171,17 @@ fn main() { }) .detach(); - client.telemetry().start(installation_id, session_id, cx); - client - .telemetry() - .report_setting_event("theme", cx.theme().name.to_string(), cx); - let event_operation = match existing_installation_id_found { - Some(false) => "first open", - _ => "open", - }; - client - .telemetry() - .report_app_event(event_operation, true, cx); + let telemetry = client.telemetry(); + telemetry.start(installation_id, session_id, cx); + telemetry.report_setting_event("theme", cx.theme().name.to_string()); + telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string()); + telemetry.report_app_event( + match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + }, + true, + ); let app_state = Arc::new(AppState { languages: languages.clone(), From 167a0b590f86cff809e535768e00f1d5a2480337 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 15:17:28 -0500 Subject: [PATCH 17/43] Add event for welcome page close --- crates/editor/src/items.rs | 1 - crates/welcome/src/welcome.rs | 54 +++++++++-------------------------- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a3f247b7b9233903d7b2b28e065b245b4a3d035c..78f9b150512e6ee153b3165e88bc162ea015aa14 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -866,7 +866,6 @@ impl Item for Editor { } fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { - dbg!(event); match event { EditorEvent::Closed => f(ItemEvent::CloseItem), diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 71d98e6c9d42d13c86d61a84ca85ff97340ac737..6650ff459485a5675719b4dfa804737b60eec70f 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -14,7 +14,7 @@ use ui::{prelude::*, Checkbox}; use vim::VimModeSetting; use workspace::{ dock::DockPosition, - item::{Item, ItemEvent, ItemHandle}, + item::{Item, ItemEvent}, open_new, AppState, Welcome, Workspace, WorkspaceId, }; @@ -252,48 +252,20 @@ impl Render for WelcomePage { impl WelcomePage { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { - let this = cx.new_view(|cx| WelcomePage { - focus_handle: cx.focus_handle(), - workspace: workspace.weak_handle(), - telemetry: workspace.client().telemetry().clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - }); - - this.on_release( - cx, - Box::new(|cx| { - this.update(cx, |this, _| { - this.telemetry.report_app_event("close welcome page", false); - }) - }), - ) - .detach(); - - // this.subscribe_to_item_events( - // cx, - // Box::new(|event: ItemEvent, cx| { - // // if event == ItemEvent::CloseItem { - // dbg!(event); - // // welcome.update(cx, |welcome, _| { - // // welcome - // // .telemetry - // // .report_app_event("close welcome page", false); - // // }) - // // } - // }), - // ) - // .detach(); + let this = cx.new_view(|cx| { + cx.on_release(|this: &mut Self, _, _| { + this.telemetry.report_app_event("close welcome page", false); + }) + .detach(); - cx.subscribe(&this, |_, welcome, event, cx| { - if *event == ItemEvent::CloseItem { - welcome.update(cx, |welcome, _| { - welcome - .telemetry - .report_app_event("close welcome page", false); - }) + WelcomePage { + focus_handle: cx.focus_handle(), + workspace: workspace.weak_handle(), + telemetry: workspace.client().telemetry().clone(), + _settings_subscription: cx + .observe_global::(move |_, cx| cx.notify()), } - }) - .detach(); + }); this } From 800c9958a377e95e7abfc3aea9071a9ae9461a71 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 15:31:16 -0500 Subject: [PATCH 18/43] Clean up code --- crates/call/src/call.rs | 2 +- crates/client/src/telemetry.rs | 2 +- crates/welcome/src/welcome.rs | 13 ------------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4f9ec080596b1a2ec962585e3c6e92ac1808389b..3561cc33852a84d78ed21371432743d9dc540862 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -310,7 +310,7 @@ impl ActiveCall { }) } - pub fn decline_incoming(&mut self, _cx: &mut ModelContext) -> Result<()> { + pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { let call = self .incoming_call .0 diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 6b6b4b0a08d24f8a147076558a2cd46f835607b0..076ac15710dc99ff697a9618eaee6570492535ef 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -213,7 +213,7 @@ impl Telemetry { drop(state); let this = self.clone(); - cx.spawn(|_cx| async move { + cx.spawn(|_| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it let refresh_kind = RefreshKind::new() .with_memory() // For memory usage diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 6650ff459485a5675719b4dfa804737b60eec70f..1c0b7472085767ee835d2b2c1171be3a7631e0a9 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -334,16 +334,3 @@ impl Item for WelcomePage { f(*event) } } - -// TODO -// - [X] get theme value -// - [X] In selector -// - [X] In main -// - [ ] get value of keymap selector -// - [X] In selector -// - [X] In main -// - [ ] get all button clicks -// - [ ] get value of usage data enabled -// - [ ] get value of crash reports enabled -// - [ ] get welcome screen close -// - [ ] test all events From 520c433af53c13517445b1949f537908f4f2cd50 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 16:10:40 -0500 Subject: [PATCH 19/43] Fix tests --- crates/channel/src/channel_store_tests.rs | 5 +++-- crates/client/src/client.rs | 14 ++++++++++++++ crates/collab_ui/src/chat_panel/message_editor.rs | 5 +++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 20413d7a76ff96a1052a828c1b08f1b884d56ed7..0b07918acfba7b9fe3ad87ef001f5fb7c5eafb30 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -343,12 +343,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) { } fn init_test(cx: &mut AppContext) -> Model { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + let http = FakeHttpClient::with_404_response(); let client = Client::new(http.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); client::init(&client, cx); crate::init(&client, user_store, cx); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index d070cae37547aa4c908635dbe42f88406bd75301..1451039b3a207bfdee0bb64ed973dcd7d34f4a82 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1404,11 +1404,13 @@ mod tests { use gpui::{BackgroundExecutor, Context, TestAppContext}; use parking_lot::Mutex; + use settings::SettingsStore; use std::future; use util::http::FakeHttpClient; #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; @@ -1443,6 +1445,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let mut status = client.status(); @@ -1514,6 +1517,7 @@ mod tests { cx: &mut TestAppContext, executor: BackgroundExecutor, ) { + init_test(cx); let auth_count = Arc::new(Mutex::new(0)); let dropped_auth_count = Arc::new(Mutex::new(0)); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); @@ -1562,6 +1566,7 @@ mod tests { #[gpui::test] async fn test_subscribing_to_entity(cx: &mut TestAppContext) { + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; @@ -1615,6 +1620,7 @@ mod tests { #[gpui::test] async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) { + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; @@ -1643,6 +1649,7 @@ mod tests { #[gpui::test] async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) { + init_test(cx); let user_id = 5; let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let server = FakeServer::for_client(user_id, &client, cx).await; @@ -1671,4 +1678,11 @@ mod tests { id: usize, subscription: Option, } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); + } } diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 522db1042d45a757be1541cbdb2bacb5e86266ef..517fac4fbb377f425210d2468d3f18bb8d1ebb6a 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -271,11 +271,12 @@ mod tests { fn init_test(cx: &mut TestAppContext) -> Arc { cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + let http = FakeHttpClient::with_404_response(); let client = Client::new(http.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); - let settings = SettingsStore::test(cx); - cx.set_global(settings); theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); editor::init(cx); From 5dca1b594333e5bf7abe19983e0f496e468bab76 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 6 Jan 2024 23:50:59 +0200 Subject: [PATCH 20/43] Properly detect file finder label positions in paths --- crates/file_finder/src/file_finder.rs | 41 +++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 323d4555671f1020b970eff091d28dd694977293..ce68819646c9911ff8d89037527e1743d8c59fb7 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -505,8 +505,7 @@ impl FileFinderDelegate { || path_match.path_prefix.to_string(), |file_name| file_name.to_string_lossy().to_string(), ); - let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count() - - file_name.chars().count(); + let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len(); let file_name_positions = path_positions .iter() .filter_map(|pos| { @@ -819,6 +818,44 @@ mod tests { } } + #[gpui::test] + async fn test_complex_path(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "其他": { + "S数据表格": { + "task.xlsx": "some content", + }, + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + + let (picker, workspace, cx) = build_find_picker(project, cx); + + cx.simulate_input("t"); + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 1); + assert_eq!( + collect_search_results(picker), + vec![PathBuf::from("其他/S数据表格/task.xlsx")], + ) + }); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!(active_editor.read(cx).title(cx), "task.xlsx"); + }); + } + #[gpui::test] async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { let app_state = init_test(cx); From dc7f9bbc5492cad63fbda8371298b85e296d287f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:47:51 +0100 Subject: [PATCH 21/43] gpui: Sweep through cargo doc output and mark dubious items as non-public (#3932) I essentially went through the publicly exported items and marked these that are e.g. leaky reexports as pub(crate). I expect that'd be done on Tuesday anyways. Release Notes: - N/A --- crates/gpui/src/gpui.rs | 2 +- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/mac.rs | 18 ------------------ crates/gpui/src/platform/mac/dispatcher.rs | 9 +++++++-- crates/gpui/src/platform/mac/display.rs | 2 +- crates/gpui/src/platform/mac/platform.rs | 5 +---- crates/gpui/src/platform/mac/text_system.rs | 4 ++-- crates/gpui/src/text_system.rs | 2 +- 8 files changed, 14 insertions(+), 30 deletions(-) diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 2697a622c350d9b485df61fd7f96c40bbc663cf2..868822d59bda9cd42e4ba9857fd7a95910bab59f 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -47,7 +47,7 @@ pub use element::*; pub use elements::*; pub use executor::*; pub use geometry::*; -pub use gpui_macros::*; +pub use gpui_macros::{register_action, test, IntoElement, Render}; pub use image_cache::*; pub use input::*; pub use interactive::*; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 0c4581904f42b19f99ac73300d8dd60721939fe4..0ef345d98d1de34bbb7c6b567b4716608e2e6dbf 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -37,7 +37,7 @@ pub use keystroke::*; pub use mac::*; #[cfg(any(test, feature = "test-support"))] pub use test::*; -pub use time::UtcOffset; +use time::UtcOffset; #[cfg(target_os = "macos")] pub(crate) fn current_platform() -> Rc { diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index d10793a618683ad3c1aa76bef860c468dfce2229..8f48b8ea94d8aa2193545267dd3d85a021a9f96c 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -106,11 +106,6 @@ impl From for Size { } } -pub trait NSRectExt { - fn size(&self) -> Size; - fn intersects(&self, other: Self) -> bool; -} - impl From for Size { fn from(rect: NSRect) -> Self { let NSSize { width, height } = rect.size; @@ -124,16 +119,3 @@ impl From for Size { size(width.into(), height.into()) } } - -// impl NSRectExt for NSRect { -// fn intersects(&self, other: Self) -> bool { -// self.size.width > 0. -// && self.size.height > 0. -// && other.size.width > 0. -// && other.size.height > 0. -// && self.origin.x <= other.origin.x + other.size.width -// && self.origin.x + self.size.width >= other.origin.x -// && self.origin.y <= other.origin.y + other.size.height -// && self.origin.y + self.size.height >= other.origin.y -// } -// } diff --git a/crates/gpui/src/platform/mac/dispatcher.rs b/crates/gpui/src/platform/mac/dispatcher.rs index 06bef49b7a96644695874a0aff2aca43f939a209..18e361885e71d58a8328bb6dc38c1b6b2d286041 100644 --- a/crates/gpui/src/platform/mac/dispatcher.rs +++ b/crates/gpui/src/platform/mac/dispatcher.rs @@ -13,9 +13,14 @@ use parking::{Parker, Unparker}; use parking_lot::Mutex; use std::{ffi::c_void, ptr::NonNull, sync::Arc, time::Duration}; -include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); +/// All items in the generated file are marked as pub, so we're gonna wrap it in a separate mod to prevent +/// these pub items from leaking into public API. +pub(crate) mod dispatch_sys { + include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs")); +} -pub fn dispatch_get_main_queue() -> dispatch_queue_t { +use dispatch_sys::*; +pub(crate) fn dispatch_get_main_queue() -> dispatch_queue_t { unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t } } diff --git a/crates/gpui/src/platform/mac/display.rs b/crates/gpui/src/platform/mac/display.rs index 2458533f6a3e45a6c0f7789135a8780669012d8c..123cbf8159be02b0496bf8e3656f837e63b03bd4 100644 --- a/crates/gpui/src/platform/mac/display.rs +++ b/crates/gpui/src/platform/mac/display.rs @@ -51,7 +51,7 @@ impl MacDisplay { #[link(name = "ApplicationServices", kind = "framework")] extern "C" { - pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; + fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; } /// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space. diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index ff89f91730ae5d33e6720f1c9723ed8f2741f0ad..8370e2a4953c1280a59d4a9cb74a93ae97214db2 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -56,9 +56,6 @@ use time::UtcOffset; #[allow(non_upper_case_globals)] const NSUTF8StringEncoding: NSUInteger = 4; -#[allow(non_upper_case_globals)] -pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2; - const MAC_PLATFORM_IVAR: &str = "platform"; static mut APP_CLASS: *const Class = ptr::null(); static mut APP_DELEGATE_CLASS: *const Class = ptr::null(); @@ -404,7 +401,7 @@ impl Platform for MacPlatform { // this, we make quitting the application asynchronous so that we aren't holding borrows to // the app state on the stack when we actually terminate the app. - use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue}; + use super::dispatcher::{dispatch_get_main_queue, dispatch_sys::dispatch_async_f}; unsafe { dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit)); diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 88ebfd83514949bbe56e661ec5c42da053814d50..d9f7936066b248a7037fb2ea810b7c4a5dc431d2 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -500,9 +500,9 @@ impl<'a> StringIndexConverter<'a> { } #[repr(C)] -pub struct __CFTypesetter(c_void); +pub(crate) struct __CFTypesetter(c_void); -pub type CTTypesetterRef = *const __CFTypesetter; +type CTTypesetterRef = *const __CFTypesetter; #[link(name = "CoreText", kind = "framework")] extern "C" { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 3106a5a961514157d2daf4d0360c395fae45c2df..0969560e95d62e6d74dc82e88eb4b13958a77480 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -33,7 +33,7 @@ pub struct FontId(pub usize); #[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] pub struct FontFamilyId(pub usize); -pub const SUBPIXEL_VARIANTS: u8 = 4; +pub(crate) const SUBPIXEL_VARIANTS: u8 = 4; pub struct TextSystem { line_layout_cache: Arc, From 8ff05c6a7227544cd36c208da8be1996e04c41dd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 01:17:49 +0200 Subject: [PATCH 22/43] Prepare for external file drop in pane --- crates/gpui/src/app.rs | 6 --- crates/gpui/src/interactive.rs | 2 +- crates/gpui/src/platform/mac/window.rs | 5 +- crates/gpui/src/window.rs | 6 +-- crates/terminal_view/src/terminal_element.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 5 +- crates/workspace/src/pane.rs | 49 ++++++++++++++++++-- crates/workspace/src/workspace.rs | 16 ++++--- 8 files changed, 63 insertions(+), 28 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4ad9540043e1058ee9ab9f5e9df9ef9bbce92057..2a0ff545e92ee6ea43a580a3a5c39e648b6c947f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1099,12 +1099,6 @@ impl AppContext { pub fn has_active_drag(&self) -> bool { self.active_drag.is_some() } - - pub fn active_drag(&self) -> Option<&T> { - self.active_drag - .as_ref() - .and_then(|drag| drag.value.downcast_ref()) - } } impl Context for AppContext { diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 0be917350df812ce07de2e95d1dac52cd59637ad..6f396d31aa481571d3331e816f30dbf788d47816 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -214,7 +214,7 @@ impl Render for ExternalPaths { pub enum FileDropEvent { Entered { position: Point, - files: ExternalPaths, + paths: ExternalPaths, }, Pending { position: Point, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 12ffc36afc08f5da22d244b7a3c14bebfb527acd..2beac528c18f53cfa9a39b008dbebf3825502b30 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1673,10 +1673,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr if send_new_event(&window_state, { let position = drag_event_position(&window_state, dragging_info); let paths = external_paths_from_event(dragging_info); - InputEvent::FileDrop(FileDropEvent::Entered { - position, - files: paths, - }) + InputEvent::FileDrop(FileDropEvent::Entered { position, paths }) }) { NSDragOperationCopy } else { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 71e6cb9e559a97634ee7d2df8610569838c32a5d..a0d3d0f886bbb7aeed136196740272c2da3e4f43 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1462,12 +1462,12 @@ impl<'a> WindowContext<'a> { // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. InputEvent::FileDrop(file_drop) => match file_drop { - FileDropEvent::Entered { position, files } => { + FileDropEvent::Entered { position, paths } => { self.window.mouse_position = position; if self.active_drag.is_none() { self.active_drag = Some(AnyDrag { - value: Box::new(files.clone()), - view: self.new_view(|_| files).into(), + value: Box::new(paths.clone()), + view: self.new_view(|_| paths).into(), cursor_offset: position, }); } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index ffdca7d8135d2702a3cbc9b4d42070e8ec4e41a4..4eb26bf50745dece3bb8e7c27b3b15bfbab8d624 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -693,9 +693,9 @@ impl TerminalElement { .join(""); new_text.push(' '); terminal.update(cx, |terminal, _| { - // todo!() long paths are not displayed properly albeit the text is there terminal.paste(&new_text); }); + cx.stop_propagation(); } }); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 5e3e4c3c23d27ab3f59f31b759573c9d1c2203d3..86ac4a9818e688edcd2838c9dc2d5526485686c8 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -65,11 +65,8 @@ impl TerminalPanel { return item.downcast::().is_some(); } } - if a.downcast_ref::().is_some() { - return true; - } - false + a.downcast_ref::().is_some() })), cx, ); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 21c5962bebb92904935b2a2c2207cda622e29dda..1798112fcc1f28655225d86fb349bb0446463170 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -8,9 +8,10 @@ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyElement, AppContext, - AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle, - FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render, - ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths, + FocusHandle, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, + PromptLevel, Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use parking_lot::Mutex; use project::{Project, ProjectEntryId, ProjectPath}; @@ -1555,6 +1556,10 @@ impl Pane { this.drag_split_direction = None; this.handle_project_entry_drop(entry_id, cx) })) + .on_drop(cx.listener(move |this, paths, cx| { + this.drag_split_direction = None; + this.handle_external_paths_drop(paths, cx) + })) .when_some(item.tab_tooltip_text(cx), |tab, text| { tab.tooltip(move |cx| Tooltip::text(text.clone(), cx)) }) @@ -1721,6 +1726,10 @@ impl Pane { .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| { this.drag_split_direction = None; this.handle_project_entry_drop(entry_id, cx) + })) + .on_drop(cx.listener(move |this, paths, cx| { + this.drag_split_direction = None; + this.handle_external_paths_drop(paths, cx) })), ) } @@ -1855,6 +1864,35 @@ impl Pane { .log_err(); } + fn handle_external_paths_drop( + &mut self, + paths: &ExternalPaths, + cx: &mut ViewContext<'_, Pane>, + ) { + // let mut to_pane = cx.view().clone(); + // let split_direction = self.drag_split_direction; + // let project_entry_id = *project_entry_id; + // self.workspace + // .update(cx, |_, cx| { + // cx.defer(move |workspace, cx| { + // if let Some(path) = workspace + // .project() + // .read(cx) + // .path_for_entry(project_entry_id, cx) + // { + // if let Some(split_direction) = split_direction { + // to_pane = workspace.split_pane(to_pane, split_direction, cx); + // } + // workspace + // .open_path(path, Some(to_pane.downgrade()), true, cx) + // .detach_and_log_err(cx); + // } + // }); + // }) + // .log_err(); + dbg!("@@@@@@@@@@@@@@", paths); + } + pub fn display_nav_history_buttons(&mut self, display: bool) { self.display_nav_history_buttons = display; } @@ -1956,6 +1994,7 @@ impl Render for Pane { .group("") .on_drag_move::(cx.listener(Self::handle_drag_move)) .on_drag_move::(cx.listener(Self::handle_drag_move)) + .on_drag_move::(cx.listener(Self::handle_drag_move)) .map(|div| { if let Some(item) = self.active_item() { div.v_flex() @@ -1985,6 +2024,7 @@ impl Render for Pane { )) .group_drag_over::("", |style| style.visible()) .group_drag_over::("", |style| style.visible()) + .group_drag_over::("", |style| style.visible()) .when_some(self.can_drop_predicate.clone(), |this, p| { this.can_drop(move |a, cx| p(a, cx)) }) @@ -1994,6 +2034,9 @@ impl Render for Pane { .on_drop(cx.listener(move |this, entry_id, cx| { this.handle_project_entry_drop(entry_id, cx) })) + .on_drop(cx.listener(move |this, paths, cx| { + this.handle_external_paths_drop(paths, cx) + })) .map(|div| match self.drag_split_direction { None => div.top_0().left_0().right_0().bottom_0(), Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6b29496f2cfd919a60e4947adb2f989fbb425486..78595d2c3126845cc06d5c14cc5e51084334d65e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,11 +27,11 @@ use futures::{ use gpui::{ actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, - Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, - ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, - Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowBounds, WindowContext, WindowHandle, WindowOptions, + Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, ExternalPaths, + FocusHandle, FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, + LayoutId, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, + PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, + WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -544,7 +544,11 @@ impl Workspace { weak_handle.clone(), project.clone(), pane_history_timestamp.clone(), - None, + Some(Arc::new(|a, _| { + a.downcast_ref::().is_some() + || a.downcast_ref::().is_some() + || a.downcast_ref::().is_some() + })), cx, ) }); From c4e306162cdb00707ba2a43091c89635b4f96544 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 01:18:02 +0200 Subject: [PATCH 23/43] Implement external file drop in pane --- crates/journal/src/journal.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/workspace/src/pane.rs | 37 +++++++++-------------- crates/workspace/src/workspace.rs | 12 +++++--- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index a1236297ed1e3f0af70ddc557994087304493c7c..a1620664c6342fcb45df78e0d8ff8487272b2b1c 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -100,7 +100,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut WindowContext) { let opened = workspace .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![entry_path], true, cx) + workspace.open_paths(vec![entry_path], true, None, cx) })? .await; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index d519523974f7a63ee210cd4cb4e2c1c06f2da8db..d788a1b62771c29560afa782a91d3f741a50a4ce 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -197,7 +197,7 @@ impl TerminalView { cx.spawn(|_, mut cx| async move { let opened_items = task_workspace .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![path.path_like], is_dir, cx) + workspace.open_paths(vec![path.path_like], is_dir, None, cx) }) .context("workspace update")? .await; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1798112fcc1f28655225d86fb349bb0446463170..fe41d0f8b2b38c673ddc6a4790f845ad65bd8b1b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1869,28 +1869,21 @@ impl Pane { paths: &ExternalPaths, cx: &mut ViewContext<'_, Pane>, ) { - // let mut to_pane = cx.view().clone(); - // let split_direction = self.drag_split_direction; - // let project_entry_id = *project_entry_id; - // self.workspace - // .update(cx, |_, cx| { - // cx.defer(move |workspace, cx| { - // if let Some(path) = workspace - // .project() - // .read(cx) - // .path_for_entry(project_entry_id, cx) - // { - // if let Some(split_direction) = split_direction { - // to_pane = workspace.split_pane(to_pane, split_direction, cx); - // } - // workspace - // .open_path(path, Some(to_pane.downgrade()), true, cx) - // .detach_and_log_err(cx); - // } - // }); - // }) - // .log_err(); - dbg!("@@@@@@@@@@@@@@", paths); + let mut to_pane = cx.view().clone(); + let split_direction = self.drag_split_direction; + let paths = paths.paths().to_vec(); + self.workspace + .update(cx, |_, cx| { + cx.defer(move |workspace, cx| { + if let Some(split_direction) = split_direction { + to_pane = workspace.split_pane(to_pane, split_direction, cx); + } + workspace + .open_paths(paths, true, Some(to_pane.downgrade()), cx) + .detach(); + }); + }) + .log_err(); } pub fn display_nav_history_buttons(&mut self, display: bool) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 78595d2c3126845cc06d5c14cc5e51084334d65e..f23dfca85754be24fd8792b3975a927b0ee4d331 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1322,6 +1322,7 @@ impl Workspace { &mut self, mut abs_paths: Vec, visible: bool, + pane: Option>, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { log::info!("open paths {abs_paths:?}"); @@ -1351,12 +1352,13 @@ impl Workspace { let this = this.clone(); let abs_path = abs_path.clone(); let fs = fs.clone(); + let pane = pane.clone(); let task = cx.spawn(move |mut cx| async move { let (worktree, project_path) = project_path?; if fs.is_file(&abs_path).await { Some( this.update(&mut cx, |this, cx| { - this.open_path(project_path, None, true, cx) + this.open_path(project_path, pane, true, cx) }) .log_err()? .await, @@ -1402,7 +1404,7 @@ impl Workspace { cx.spawn(|this, mut cx| async move { if let Some(paths) = paths.await.log_err().flatten() { let results = this - .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))? + .update(&mut cx, |this, cx| this.open_paths(paths, true, None, cx))? .await; for result in results.into_iter().flatten() { result.log_err(); @@ -1788,7 +1790,7 @@ impl Workspace { cx.spawn(|workspace, mut cx| async move { let open_paths_task_result = workspace .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![abs_path.clone()], visible, cx) + workspace.open_paths(vec![abs_path.clone()], visible, None, cx) }) .with_context(|| format!("open abs path {abs_path:?} task spawn"))? .await; @@ -4087,7 +4089,7 @@ pub fn open_paths( existing.clone(), existing .update(&mut cx, |workspace, cx| { - workspace.open_paths(abs_paths, true, cx) + workspace.open_paths(abs_paths, true, None, cx) })? .await, )) @@ -4135,7 +4137,7 @@ pub fn create_and_open_local_file( let mut items = workspace .update(&mut cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| { - workspace.open_paths(vec![path.to_path_buf()], false, cx) + workspace.open_paths(vec![path.to_path_buf()], false, None, cx) }) })? .await? From 518868a12f72caa7d4be54ff16cd5e6b5604dff0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 02:21:43 +0200 Subject: [PATCH 24/43] Implement terminal pane drag and drop overrides --- crates/terminal_view/src/terminal_element.rs | 32 ++------- crates/terminal_view/src/terminal_panel.rs | 74 ++++++++++++++++---- crates/workspace/src/pane.rs | 40 ++++++++--- crates/workspace/src/workspace.rs | 16 ++--- 4 files changed, 103 insertions(+), 59 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 4eb26bf50745dece3bb8e7c27b3b15bfbab8d624..d936716032a53b432d2f6f1a5dc6b79069656c8b 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,12 +1,12 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace, - BorrowWindow, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, - FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement, + BorrowWindow, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, + FontWeight, HighlightStyle, Hsla, InteractiveBounds, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, PlatformInputHandler, Point, - ShapedLine, StatefulInteractiveElement, StyleRefinement, Styled, TextRun, TextStyle, - TextSystem, UnderlineStyle, WhiteSpace, WindowContext, + ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, + WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -25,7 +25,7 @@ use terminal::{ use theme::{ActiveTheme, Theme, ThemeSettings}; use ui::Tooltip; -use std::{any::TypeId, mem}; +use std::mem; use std::{fmt::Debug, ops::RangeInclusive}; ///The information generated during layout that is necessary for painting @@ -677,28 +677,6 @@ impl TerminalElement { } }); - self.interactivity.drag_over_styles.push(( - TypeId::of::(), - StyleRefinement::default().bg(cx.theme().colors().drop_target_background), - )); - self.interactivity.on_drop::({ - let focus = focus.clone(); - let terminal = terminal.clone(); - move |external_paths, cx| { - cx.focus(&focus); - let mut new_text = external_paths - .paths() - .iter() - .map(|path| format!(" {path:?}")) - .join(""); - new_text.push(' '); - terminal.update(cx, |terminal, _| { - terminal.paste(&new_text); - }); - cx.stop_propagation(); - } - }); - // Mouse mode handlers: // All mouse modes need the extra click handlers if mode.intersects(TermMode::MOUSE_MODE) { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 86ac4a9818e688edcd2838c9dc2d5526485686c8..c19d0bfc6cbbf9e34548e5e2d22fa029cd0ce7d0 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; @@ -7,7 +7,8 @@ use gpui::{ FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; -use project::Fs; +use itertools::Itertools; +use project::{Fs, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -19,7 +20,7 @@ use workspace::{ item::Item, pane, ui::Icon, - Pane, Workspace, + DraggedTab, Pane, Workspace, }; use anyhow::Result; @@ -59,15 +60,7 @@ impl TerminalPanel { workspace.weak_handle(), workspace.project().clone(), Default::default(), - Some(Arc::new(|a, cx| { - if let Some(tab) = a.downcast_ref::() { - if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) { - return item.downcast::().is_some(); - } - } - - a.downcast_ref::().is_some() - })), + None, cx, ); pane.set_can_split(false, cx); @@ -102,6 +95,47 @@ impl TerminalPanel { }) .into_any_element() }); + + let workspace = workspace.weak_handle(); + pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { + if let Some(tab) = dropped_item.downcast_ref::() { + if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) { + if item.downcast::().is_some() { + return ControlFlow::Continue(()); + } else if let Some(project_path) = item.project_path(cx) { + if let Some(entry_path) = workspace + .update(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .absolute_path(&project_path, cx) + }) + .log_err() + .flatten() + { + add_paths_to_terminal(pane, &[entry_path], cx); + } + } + } + } else if let Some(&entry_id) = dropped_item.downcast_ref::() { + if let Some(entry_path) = workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + project + .path_for_entry(entry_id, cx) + .and_then(|project_path| project.absolute_path(&project_path, cx)) + }) + .log_err() + .flatten() + { + add_paths_to_terminal(pane, &[entry_path], cx); + } + } else if let Some(paths) = dropped_item.downcast_ref::() { + add_paths_to_terminal(pane, paths.paths(), cx); + } + + ControlFlow::Break(()) + }); let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); pane.toolbar() .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); @@ -326,6 +360,22 @@ impl TerminalPanel { } } +fn add_paths_to_terminal(pane: &mut Pane, paths: &[PathBuf], cx: &mut ViewContext<'_, Pane>) { + if let Some(terminal_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + cx.focus_view(&terminal_view); + let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join(""); + new_text.push(' '); + terminal_view.update(cx, |terminal_view, cx| { + terminal_view.terminal().update(cx, |terminal, _| { + terminal.paste(&new_text); + }); + }); + } +} + impl EventEmitter for TerminalPanel {} impl Render for TerminalPanel { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe41d0f8b2b38c673ddc6a4790f845ad65bd8b1b..985311b18a847be2fe1f3eff5763ea981f553382 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -20,6 +20,7 @@ use settings::Settings; use std::{ any::Any, cmp, fmt, mem, + ops::ControlFlow, path::{Path, PathBuf}, rc::Rc, sync::{ @@ -183,6 +184,8 @@ pub struct Pane { project: Model, drag_split_direction: Option, can_drop_predicate: Option bool>>, + custom_drop_handle: + Option) -> ControlFlow<(), ()>>>, can_split: bool, render_tab_bar_buttons: Rc) -> AnyElement>, _subscriptions: Vec, @@ -375,6 +378,7 @@ impl Pane { workspace, project, can_drop_predicate, + custom_drop_handle: None, can_split: true, render_tab_bar_buttons: Rc::new(move |pane, cx| { h_stack() @@ -501,13 +505,6 @@ impl Pane { self.active_item_index } - // pub fn on_can_drop(&mut self, can_drop: F) - // where - // F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, - // { - // self.can_drop = Rc::new(can_drop); - // } - pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { self.can_split = can_split; cx.notify(); @@ -528,6 +525,14 @@ impl Pane { cx.notify(); } + pub fn set_custom_drop_handle(&mut self, cx: &mut ViewContext, handle: F) + where + F: 'static + Fn(&mut Pane, &dyn Any, &mut ViewContext) -> ControlFlow<(), ()>, + { + self.custom_drop_handle = Some(Arc::new(handle)); + cx.notify(); + } + pub fn nav_history_for_item(&self, item: &View) -> ItemNavHistory { ItemNavHistory { history: self.nav_history.clone(), @@ -1818,8 +1823,13 @@ impl Pane { &mut self, dragged_tab: &DraggedTab, ix: usize, - cx: &mut ViewContext<'_, Pane>, + cx: &mut ViewContext<'_, Self>, ) { + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { + if let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, cx) { + return; + } + } let mut to_pane = cx.view().clone(); let split_direction = self.drag_split_direction; let item_id = dragged_tab.item_id; @@ -1839,8 +1849,13 @@ impl Pane { fn handle_project_entry_drop( &mut self, project_entry_id: &ProjectEntryId, - cx: &mut ViewContext<'_, Pane>, + cx: &mut ViewContext<'_, Self>, ) { + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { + if let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, cx) { + return; + } + } let mut to_pane = cx.view().clone(); let split_direction = self.drag_split_direction; let project_entry_id = *project_entry_id; @@ -1867,8 +1882,13 @@ impl Pane { fn handle_external_paths_drop( &mut self, paths: &ExternalPaths, - cx: &mut ViewContext<'_, Pane>, + cx: &mut ViewContext<'_, Self>, ) { + if let Some(custom_drop_handle) = self.custom_drop_handle.clone() { + if let ControlFlow::Break(()) = custom_drop_handle(self, paths, cx) { + return; + } + } let mut to_pane = cx.view().clone(); let split_direction = self.drag_split_direction; let paths = paths.paths().to_vec(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f23dfca85754be24fd8792b3975a927b0ee4d331..8e66f06b4ac510be07860d89e174c44b85822b29 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,11 +27,11 @@ use futures::{ use gpui::{ actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, - Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, ExternalPaths, - FocusHandle, FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, - LayoutId, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, - PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, - WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, + FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, + ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, + Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -544,11 +544,7 @@ impl Workspace { weak_handle.clone(), project.clone(), pane_history_timestamp.clone(), - Some(Arc::new(|a, _| { - a.downcast_ref::().is_some() - || a.downcast_ref::().is_some() - || a.downcast_ref::().is_some() - })), + None, cx, ) }); From c499e1ed3842b0e635d0c9da47deaf7e59a01680 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 02:32:15 +0200 Subject: [PATCH 25/43] Fix panic during terminal tab drag and drop --- crates/terminal_view/src/terminal_panel.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c19d0bfc6cbbf9e34548e5e2d22fa029cd0ce7d0..c0f8e6209b208cb8125b82a8626b85fac76cbac1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -99,7 +99,12 @@ impl TerminalPanel { let workspace = workspace.weak_handle(); pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { if let Some(tab) = dropped_item.downcast_ref::() { - if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) { + let item = if &tab.pane == cx.view() { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + if let Some(item) = item { if item.downcast::().is_some() { return ControlFlow::Continue(()); } else if let Some(project_path) = item.project_path(cx) { From 4f88a50aad07f4ec59996878c4a3a92008fd90d6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 02:59:56 +0200 Subject: [PATCH 26/43] On external file drop, add visible project entries for directories only --- crates/journal/src/journal.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 11 +++- crates/workspace/src/pane.rs | 9 ++- crates/workspace/src/workspace.rs | 76 ++++++++++++++++++----- 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index a1620664c6342fcb45df78e0d8ff8487272b2b1c..2ae74e7f5d5d60c9485c22235633b1a23f952605 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -11,7 +11,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use workspace::{AppState, Workspace}; +use workspace::{AppState, OpenVisible, Workspace}; actions!(journal, [NewJournalEntry]); @@ -100,7 +100,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut WindowContext) { let opened = workspace .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![entry_path], true, None, cx) + workspace.open_paths(vec![entry_path], OpenVisible::All, None, cx) })? .await; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index d788a1b62771c29560afa782a91d3f741a50a4ce..7c07d55585d25a770d1ada9147fe6249bff9dd58 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -29,7 +29,8 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, - CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, + CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace, + WorkspaceId, }; use anyhow::Context; @@ -192,12 +193,18 @@ impl TerminalView { } let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); if let Some(path) = potential_abs_paths.into_iter().next() { + // TODO kb wrong lib call let is_dir = path.path_like.is_dir(); let task_workspace = workspace.clone(); cx.spawn(|_, mut cx| async move { let opened_items = task_workspace .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![path.path_like], is_dir, None, cx) + workspace.open_paths( + vec![path.path_like], + OpenVisible::OnlyDirectories, + None, + cx, + ) }) .context("workspace update")? .await; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 985311b18a847be2fe1f3eff5763ea981f553382..04a51fc655be0d7b5d2b890479bef484b5cbc14a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,7 @@ use crate::{ item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle}, toolbar::Toolbar, workspace_settings::{AutosaveSetting, WorkspaceSettings}, - NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace, + NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; @@ -1899,7 +1899,12 @@ impl Pane { to_pane = workspace.split_pane(to_pane, split_direction, cx); } workspace - .open_paths(paths, true, Some(to_pane.downgrade()), cx) + .open_paths( + paths, + OpenVisible::OnlyDirectories, + Some(to_pane.downgrade()), + cx, + ) .detach(); }); }) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8e66f06b4ac510be07860d89e174c44b85822b29..4ad184ee79c9f4780fb6e33cc6f41c951d846351 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -431,6 +431,13 @@ pub enum Event { WorkspaceCreated(WeakView), } +pub enum OpenVisible { + All, + None, + OnlyFiles, + OnlyDirectories, +} + pub struct Workspace { weak_self: WeakView, workspace_actions: Vec) -> Div>>, @@ -1317,7 +1324,7 @@ impl Workspace { pub fn open_paths( &mut self, mut abs_paths: Vec, - visible: bool, + visible: OpenVisible, pane: Option>, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { @@ -1329,19 +1336,43 @@ impl Workspace { abs_paths.sort_unstable(); cx.spawn(move |this, mut cx| async move { let mut tasks = Vec::with_capacity(abs_paths.len()); + for abs_path in &abs_paths { - let project_path = match this - .update(&mut cx, |this, cx| { - Workspace::project_path_for_path( - this.project.clone(), - abs_path, - visible, - cx, - ) - }) - .log_err() - { - Some(project_path) => project_path.await.log_err(), + let visible = match visible { + OpenVisible::All => Some(true), + OpenVisible::None => Some(false), + OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() { + Some(Some(metadata)) => Some(!metadata.is_dir), + Some(None) => { + log::error!("No metadata for file {abs_path:?}"); + None + } + None => None, + }, + OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() { + Some(Some(metadata)) => Some(metadata.is_dir), + Some(None) => { + log::error!("No metadata for file {abs_path:?}"); + None + } + None => None, + }, + }; + let project_path = match visible { + Some(visible) => match this + .update(&mut cx, |this, cx| { + Workspace::project_path_for_path( + this.project.clone(), + abs_path, + visible, + cx, + ) + }) + .log_err() + { + Some(project_path) => project_path.await.log_err(), + None => None, + }, None => None, }; @@ -1400,7 +1431,9 @@ impl Workspace { cx.spawn(|this, mut cx| async move { if let Some(paths) = paths.await.log_err().flatten() { let results = this - .update(&mut cx, |this, cx| this.open_paths(paths, true, None, cx))? + .update(&mut cx, |this, cx| { + this.open_paths(paths, OpenVisible::All, None, cx) + })? .await; for result in results.into_iter().flatten() { result.log_err(); @@ -1786,7 +1819,16 @@ impl Workspace { cx.spawn(|workspace, mut cx| async move { let open_paths_task_result = workspace .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![abs_path.clone()], visible, None, cx) + workspace.open_paths( + vec![abs_path.clone()], + if visible { + OpenVisible::All + } else { + OpenVisible::None + }, + None, + cx, + ) }) .with_context(|| format!("open abs path {abs_path:?} task spawn"))? .await; @@ -4085,7 +4127,7 @@ pub fn open_paths( existing.clone(), existing .update(&mut cx, |workspace, cx| { - workspace.open_paths(abs_paths, true, None, cx) + workspace.open_paths(abs_paths, OpenVisible::All, None, cx) })? .await, )) @@ -4133,7 +4175,7 @@ pub fn create_and_open_local_file( let mut items = workspace .update(&mut cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| { - workspace.open_paths(vec![path.to_path_buf()], false, None, cx) + workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx) }) })? .await? From 5344296c9a6343192f5e3fe88e8b02d49be47152 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 20:27:30 -0500 Subject: [PATCH 27/43] Remove immediate flush mode Allow flush method to be called publicly. This is a better, simpler solution, that allows for better control over flushing. --- crates/client/src/telemetry.rs | 27 ++++++++++++++------------- crates/welcome/src/welcome.rs | 20 +++++++------------- crates/workspace/src/workspace.rs | 4 +--- crates/zed/src/main.rs | 12 +++++------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 076ac15710dc99ff697a9618eaee6570492535ef..d0a92b681d5da6f1257ad3eb6adc34f5a595aab9 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -193,7 +193,8 @@ impl Telemetry { // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings #[cfg(not(any(test, feature = "test-support")))] fn shutdown_telemetry(self: &Arc) -> impl Future { - self.report_app_event("close", true); + self.report_app_event("close"); + self.flush_clickhouse_events(); Task::ready(()) } @@ -283,7 +284,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } pub fn report_copilot_event( @@ -299,7 +300,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } pub fn report_assistant_event( @@ -315,7 +316,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } pub fn report_call_event( @@ -331,7 +332,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { @@ -341,7 +342,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } pub fn report_memory_event( @@ -355,16 +356,16 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } - pub fn report_app_event(self: &Arc, operation: &'static str, immediate_flush: bool) { + pub fn report_app_event(self: &Arc, operation: &'static str) { let event = ClickhouseEvent::App { operation, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, immediate_flush) + self.report_clickhouse_event(event) } pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { @@ -374,7 +375,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, false) + self.report_clickhouse_event(event) } fn milliseconds_since_first_event(&self) -> i64 { @@ -391,7 +392,7 @@ impl Telemetry { } } - fn report_clickhouse_event(self: &Arc, event: ClickhouseEvent, immediate_flush: bool) { + fn report_clickhouse_event(self: &Arc, event: ClickhouseEvent) { let mut state = self.state.lock(); if !state.settings.metrics { @@ -404,7 +405,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { @@ -430,7 +431,7 @@ impl Telemetry { self.state.lock().is_staff } - fn flush_clickhouse_events(self: &Arc) { + pub fn flush_clickhouse_events(self: &Arc) { let mut state = self.state.lock(); state.first_event_datetime = None; let mut events = mem::take(&mut state.clickhouse_events_queue); diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 1c0b7472085767ee835d2b2c1171be3a7631e0a9..3d89810679218e69b1aa88941c67326ed667b055 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -86,7 +86,7 @@ impl Render for WelcomePage { .full_width() .on_click(cx.listener(|this, _, cx| { this.telemetry - .report_app_event("welcome page button: theme", false); + .report_app_event("welcome page button: theme"); this.workspace .update(cx, |workspace, cx| { theme_selector::toggle( @@ -103,7 +103,7 @@ impl Render for WelcomePage { .full_width() .on_click(cx.listener(|this, _, cx| { this.telemetry - .report_app_event("welcome page button: keymap", false); + .report_app_event("welcome page button: keymap"); this.workspace .update(cx, |workspace, cx| { base_keymap_picker::toggle( @@ -119,10 +119,8 @@ impl Render for WelcomePage { Button::new("install-cli", "Install the CLI") .full_width() .on_click(cx.listener(|this, _, cx| { - this.telemetry.report_app_event( - "welcome page button: install cli", - false, - ); + this.telemetry + .report_app_event("welcome page button: install cli"); cx.app_mut() .spawn( |cx| async move { install_cli::install_cli(&cx).await }, @@ -153,10 +151,8 @@ impl Render for WelcomePage { ) .on_click(cx.listener( move |this, selection, cx| { - this.telemetry.report_app_event( - "welcome page button: vim", - false, - ); + this.telemetry + .report_app_event("welcome page button: vim"); this.update_settings::( selection, cx, @@ -183,7 +179,6 @@ impl Render for WelcomePage { move |this, selection, cx| { this.telemetry.report_app_event( "welcome page button: user telemetry", - false, ); this.update_settings::( selection, @@ -222,7 +217,6 @@ impl Render for WelcomePage { move |this, selection, cx| { this.telemetry.report_app_event( "welcome page button: crash diagnostics", - false, ); this.update_settings::( selection, @@ -254,7 +248,7 @@ impl WelcomePage { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { let this = cx.new_view(|cx| { cx.on_release(|this: &mut Self, _, _| { - this.telemetry.report_app_event("close welcome page", false); + this.telemetry.report_app_event("close welcome page"); }) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 653f777084228d06e272bc8e26575352da866a4b..967e76efd65dd049c0213720266f24d40af88df0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1258,9 +1258,7 @@ impl Workspace { } pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { - self.client() - .telemetry() - .report_app_event("open project", false); + self.client().telemetry().report_app_event("open project"); let paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 22436e10a973d676e80c641f704f416ccd8d17c5..ff07af03bd85838f59522f56364eb04689e36adc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -175,13 +175,11 @@ fn main() { telemetry.start(installation_id, session_id, cx); telemetry.report_setting_event("theme", cx.theme().name.to_string()); telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string()); - telemetry.report_app_event( - match existing_installation_id_found { - Some(false) => "first open", - _ => "open", - }, - true, - ); + telemetry.report_app_event(match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + }); + telemetry.flush_clickhouse_events(); let app_state = Arc::new(AppState { languages: languages.clone(), From f4c78d3f4000b3cdf5376d60170286d7cf8470f8 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 23:27:23 -0500 Subject: [PATCH 28/43] Remove "clickhouse" from telemetry code The client sends events to our end point, and the endpoint is what determines what analytics database is used to store the data. The client should be generic and not mention the name of the service being proxied to through our server. --- crates/client/src/client.rs | 2 +- crates/client/src/telemetry.rs | 81 ++++++++++++++++------------------ crates/zed/src/main.rs | 2 +- 3 files changed, 41 insertions(+), 44 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1451039b3a207bfdee0bb64ed973dcd7d34f4a82..b07dddc006d607720412e68203b8445d8c026a1e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -45,7 +45,7 @@ use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; pub use rpc::*; -pub use telemetry::ClickhouseEvent; +pub use telemetry::Event; pub use user::*; lazy_static! { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index d0a92b681d5da6f1257ad3eb6adc34f5a595aab9..94d8369a698419cb11bc8bddfb12b24145127952 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -28,22 +28,21 @@ struct TelemetryState { release_channel: Option<&'static str>, app_metadata: AppMetadata, architecture: &'static str, - clickhouse_events_queue: Vec, - flush_clickhouse_events_task: Option>, + events_queue: Vec, + flush_events_task: Option>, log_file: Option, is_staff: Option, first_event_datetime: Option>, } -const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; +const EVENTS_URL_PATH: &'static str = "/api/events"; lazy_static! { - static ref CLICKHOUSE_EVENTS_URL: String = - format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH); + static ref EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, EVENTS_URL_PATH); } #[derive(Serialize, Debug)] -struct ClickhouseEventRequestBody { +struct EventRequestBody { token: &'static str, installation_id: Option>, session_id: Option>, @@ -53,14 +52,14 @@ struct ClickhouseEventRequestBody { os_version: Option, architecture: &'static str, release_channel: Option<&'static str>, - events: Vec, + events: Vec, } #[derive(Serialize, Debug)] -struct ClickhouseEventWrapper { +struct EventWrapper { signed_in: bool, #[serde(flatten)] - event: ClickhouseEvent, + event: Event, } #[derive(Serialize, Debug)] @@ -72,7 +71,7 @@ pub enum AssistantKind { #[derive(Serialize, Debug)] #[serde(tag = "type")] -pub enum ClickhouseEvent { +pub enum Event { Editor { operation: &'static str, file_extension: Option, @@ -150,8 +149,8 @@ impl Telemetry { installation_id: None, metrics_id: None, session_id: None, - clickhouse_events_queue: Default::default(), - flush_clickhouse_events_task: Default::default(), + events_queue: Default::default(), + flush_events_task: Default::default(), log_file: None, is_staff: None, first_event_datetime: None, @@ -194,7 +193,7 @@ impl Telemetry { #[cfg(not(any(test, feature = "test-support")))] fn shutdown_telemetry(self: &Arc) -> impl Future { self.report_app_event("close"); - self.flush_clickhouse_events(); + self.flush_events(); Task::ready(()) } @@ -275,7 +274,7 @@ impl Telemetry { copilot_enabled: bool, copilot_enabled_for_language: bool, ) { - let event = ClickhouseEvent::Editor { + let event = Event::Editor { file_extension, vim_mode, operation, @@ -284,7 +283,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_copilot_event( @@ -293,14 +292,14 @@ impl Telemetry { suggestion_accepted: bool, file_extension: Option, ) { - let event = ClickhouseEvent::Copilot { + let event = Event::Copilot { suggestion_id, suggestion_accepted, file_extension, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_assistant_event( @@ -309,14 +308,14 @@ impl Telemetry { kind: AssistantKind, model: &'static str, ) { - let event = ClickhouseEvent::Assistant { + let event = Event::Assistant { conversation_id, kind, model, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_call_event( @@ -325,24 +324,24 @@ impl Telemetry { room_id: Option, channel_id: Option, ) { - let event = ClickhouseEvent::Call { + let event = Event::Call { operation, room_id, channel_id, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { - let event = ClickhouseEvent::Cpu { + let event = Event::Cpu { usage_as_percentage, core_count, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_memory_event( @@ -350,32 +349,32 @@ impl Telemetry { memory_in_bytes: u64, virtual_memory_in_bytes: u64, ) { - let event = ClickhouseEvent::Memory { + let event = Event::Memory { memory_in_bytes, virtual_memory_in_bytes, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_app_event(self: &Arc, operation: &'static str) { - let event = ClickhouseEvent::App { + let event = Event::App { operation, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } pub fn report_setting_event(self: &Arc, setting: &'static str, value: String) { - let event = ClickhouseEvent::Setting { + let event = Event::Setting { setting, value, milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event) + self.report_event(event) } fn milliseconds_since_first_event(&self) -> i64 { @@ -392,7 +391,7 @@ impl Telemetry { } } - fn report_clickhouse_event(self: &Arc, event: ClickhouseEvent) { + fn report_event(self: &Arc, event: Event) { let mut state = self.state.lock(); if !state.settings.metrics { @@ -400,20 +399,18 @@ impl Telemetry { } let signed_in = state.metrics_id.is_some(); - state - .clickhouse_events_queue - .push(ClickhouseEventWrapper { signed_in, event }); + state.events_queue.push(EventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if state.events_queue.len() >= MAX_QUEUE_LEN { drop(state); - self.flush_clickhouse_events(); + self.flush_events(); } else { let this = self.clone(); let executor = self.executor.clone(); - state.flush_clickhouse_events_task = Some(self.executor.spawn(async move { + state.flush_events_task = Some(self.executor.spawn(async move { executor.timer(DEBOUNCE_INTERVAL).await; - this.flush_clickhouse_events(); + this.flush_events(); })); } } @@ -431,11 +428,11 @@ impl Telemetry { self.state.lock().is_staff } - pub fn flush_clickhouse_events(self: &Arc) { + pub fn flush_events(self: &Arc) { let mut state = self.state.lock(); state.first_event_datetime = None; - let mut events = mem::take(&mut state.clickhouse_events_queue); - state.flush_clickhouse_events_task.take(); + let mut events = mem::take(&mut state.events_queue); + state.flush_events_task.take(); drop(state); let this = self.clone(); @@ -456,7 +453,7 @@ impl Telemetry { { let state = this.state.lock(); - let request_body = ClickhouseEventRequestBody { + let request_body = EventRequestBody { token: ZED_SECRET_CLIENT_TOKEN, installation_id: state.installation_id.clone(), session_id: state.session_id.clone(), @@ -480,7 +477,7 @@ impl Telemetry { } this.http_client - .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into()) + .post_json(EVENTS_URL.as_str(), json_bytes.into()) .await?; anyhow::Ok(()) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ff07af03bd85838f59522f56364eb04689e36adc..56109d9c9a532d97de0f8b76101b4057203879e2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -179,7 +179,7 @@ fn main() { Some(false) => "first open", _ => "open", }); - telemetry.flush_clickhouse_events(); + telemetry.flush_events(); let app_state = Arc::new(AppState { languages: languages.clone(), From 1bcee19ed5be667e27ca9b9b8aaa96aaf476471e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 23:30:53 -0500 Subject: [PATCH 29/43] Improve operation name consistency for welcome page --- crates/welcome/src/welcome.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 3d89810679218e69b1aa88941c67326ed667b055..79114a1aada6d60f460e130e1ef99a02bc382d5c 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -86,7 +86,7 @@ impl Render for WelcomePage { .full_width() .on_click(cx.listener(|this, _, cx| { this.telemetry - .report_app_event("welcome page button: theme"); + .report_app_event("welcome page: change theme"); this.workspace .update(cx, |workspace, cx| { theme_selector::toggle( @@ -103,7 +103,7 @@ impl Render for WelcomePage { .full_width() .on_click(cx.listener(|this, _, cx| { this.telemetry - .report_app_event("welcome page button: keymap"); + .report_app_event("welcome page: change keymap"); this.workspace .update(cx, |workspace, cx| { base_keymap_picker::toggle( @@ -119,8 +119,7 @@ impl Render for WelcomePage { Button::new("install-cli", "Install the CLI") .full_width() .on_click(cx.listener(|this, _, cx| { - this.telemetry - .report_app_event("welcome page button: install cli"); + this.telemetry.report_app_event("welcome page: install cli"); cx.app_mut() .spawn( |cx| async move { install_cli::install_cli(&cx).await }, @@ -152,7 +151,7 @@ impl Render for WelcomePage { .on_click(cx.listener( move |this, selection, cx| { this.telemetry - .report_app_event("welcome page button: vim"); + .report_app_event("welcome page: toggle vim"); this.update_settings::( selection, cx, @@ -178,7 +177,7 @@ impl Render for WelcomePage { .on_click(cx.listener( move |this, selection, cx| { this.telemetry.report_app_event( - "welcome page button: user telemetry", + "welcome page: toggle metric telemetry", ); this.update_settings::( selection, @@ -216,7 +215,7 @@ impl Render for WelcomePage { .on_click(cx.listener( move |this, selection, cx| { this.telemetry.report_app_event( - "welcome page button: crash diagnostics", + "welcome page: toggle diagnostic telemetry", ); this.update_settings::( selection, @@ -248,7 +247,7 @@ impl WelcomePage { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> View { let this = cx.new_view(|cx| { cx.on_release(|this: &mut Self, _, _| { - this.telemetry.report_app_event("close welcome page"); + this.telemetry.report_app_event("welcome page: close"); }) .detach(); From 44bc5aef391d10c4d1ba2f6b3a1fa78f2bc1c492 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 6 Jan 2024 23:34:46 -0500 Subject: [PATCH 30/43] Improve setting name consistency for welcome page --- crates/welcome/src/welcome.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 79114a1aada6d60f460e130e1ef99a02bc382d5c..76988fadb06b9124f1b197178cb0c89106670f7a 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -189,7 +189,7 @@ impl Render for WelcomePage { settings.metrics = Some(value); telemetry.report_setting_event( - "user telemetry", + "metric telemetry", value.to_string(), ); } @@ -227,7 +227,7 @@ impl Render for WelcomePage { settings.diagnostics = Some(value); telemetry.report_setting_event( - "crash diagnostics", + "diagnostic telemetry", value.to_string(), ); } From df937eaeebea2f5472dd91750f1582be7c586a57 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 13:32:09 +0200 Subject: [PATCH 31/43] Use fs to determine if file path is a dir --- crates/terminal_view/src/terminal_view.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7c07d55585d25a770d1ada9147fe6249bff9dd58..5372b90bee921565b8460cfe5af2f81d9fb65d4d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -193,10 +193,18 @@ impl TerminalView { } let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); if let Some(path) = potential_abs_paths.into_iter().next() { - // TODO kb wrong lib call - let is_dir = path.path_like.is_dir(); let task_workspace = workspace.clone(); cx.spawn(|_, mut cx| async move { + let fs = task_workspace.update(&mut cx, |workspace, cx| { + workspace.project().read(cx).fs().clone() + })?; + let is_dir = fs + .metadata(&path.path_like) + .await? + .with_context(|| { + format!("Missing metadata for file {:?}", path.path_like) + })? + .is_dir; let opened_items = task_workspace .update(&mut cx, |workspace, cx| { workspace.open_paths( From d566a0df5ab5646eeb39be3bf1d6c792efc9ebb1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 Jan 2024 13:46:44 +0200 Subject: [PATCH 32/43] Always show full command on terminal tab hover --- crates/terminal/src/terminal.rs | 47 +++++++++++------------ crates/terminal_view/src/terminal_view.rs | 5 +-- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d43508bdbe0100b6130b1a655e01160433962477..e1605eb4fb4e35b16c7e08bee6107537462cc2ed 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1311,34 +1311,33 @@ impl Terminal { }) } - pub fn title(&self) -> String { + pub fn title(&self, truncate: bool) -> String { self.foreground_process_info .as_ref() .map(|fpi| { - format!( - "{} — {}", - truncate_and_trailoff( - &fpi.cwd - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default(), - 25 - ), - truncate_and_trailoff( - &{ - format!( - "{}{}", - fpi.name, - if fpi.argv.len() >= 1 { - format!(" {}", (&fpi.argv[1..]).join(" ")) - } else { - "".to_string() - } - ) - }, - 25 + let process_file = fpi + .cwd + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(); + let process_name = format!( + "{}{}", + fpi.name, + if fpi.argv.len() >= 1 { + format!(" {}", (&fpi.argv[1..]).join(" ")) + } else { + "".to_string() + } + ); + let (process_file, process_name) = if truncate { + ( + truncate_and_trailoff(&process_file, 25), + truncate_and_trailoff(&process_name, 25), ) - ) + } else { + (process_file, process_name) + }; + format!("{process_file} — {process_name}") }) .unwrap_or_else(|| "Terminal".to_string()) } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index d519523974f7a63ee210cd4cb4e2c1c06f2da8db..2fdf12fa3dfc2903ba3963c624b48ef04192b646 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -665,7 +665,7 @@ impl Item for TerminalView { type Event = ItemEvent; fn tab_tooltip_text(&self, cx: &AppContext) -> Option { - Some(self.terminal().read(cx).title().into()) + Some(self.terminal().read(cx).title(false).into()) } fn tab_content( @@ -674,8 +674,7 @@ impl Item for TerminalView { selected: bool, cx: &WindowContext, ) -> AnyElement { - let title = self.terminal().read(cx).title(); - + let title = self.terminal().read(cx).title(true); h_stack() .gap_2() .child(IconElement::new(Icon::Terminal)) From d475f1373a0850da1381834cada03b4b0855c920 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 7 Jan 2024 14:14:21 +0100 Subject: [PATCH 33/43] gpui: Further docs refinement & moved some reexports into 'private' module (#3935) This commit mostly fixes invalid URLs in docstrings. It also encapsulates crates we reexport (serde stuff + linkme) into a public module named "private" in order to reduce the API surfaced through docs. Moreover, I fixed up a bunch of crates that were pulling serde_json in through gpui explicitly instead of using Cargo manifest. Release Notes: - N/A --- Cargo.lock | 7 +++++++ crates/ai/src/providers/open_ai/embedding.rs | 3 ++- crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 5 +++-- crates/client/src/telemetry.rs | 3 ++- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/chat_panel.rs | 6 +++--- crates/collab_ui/src/collab_panel.rs | 12 ++++++------ crates/collab_ui/src/notification_panel.rs | 10 +++++----- crates/editor/src/editor_tests.rs | 7 ++----- crates/editor/src/test/editor_test_context.rs | 2 +- crates/feedback/Cargo.toml | 1 + crates/feedback/src/feedback_modal.rs | 4 ++-- crates/gpui/src/action.rs | 12 ++++++------ crates/gpui/src/app.rs | 6 +++--- crates/gpui/src/app/entity_map.rs | 2 +- crates/gpui/src/geometry.rs | 5 +++-- crates/gpui/src/gpui.rs | 18 +++++++++++------- crates/gpui/src/input.rs | 8 +++++--- .../gpui/src/platform/mac/display_linker.rs | 2 +- crates/gpui/src/view.rs | 2 +- crates/gpui/src/window.rs | 19 ++++++++----------- crates/gpui/tests/action_macros.rs | 2 +- crates/gpui_macros/src/register_action.rs | 4 ++-- crates/language_tools/Cargo.toml | 1 + crates/language_tools/src/lsp_log_tests.rs | 3 ++- crates/project_symbols/Cargo.toml | 1 + crates/project_symbols/src/project_symbols.rs | 3 ++- crates/terminal_view/Cargo.toml | 1 + crates/terminal_view/src/terminal_panel.rs | 6 +++--- crates/theme_importer/Cargo.toml | 1 + crates/theme_importer/src/main.rs | 1 - crates/theme_importer/src/zed1/converter.rs | 2 +- crates/theme_importer/src/zed1/theme.rs | 2 +- 34 files changed, 91 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c22fe57b478db9e3dfbcc2fdf940a839910c3a55..54e2f483d8a655f3d5e5c6e200b918275f968a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1380,6 +1380,7 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings", "smol", "sum_tree", @@ -1545,6 +1546,7 @@ dependencies = [ "schemars", "serde", "serde_derive", + "serde_json", "settings", "smallvec", "theme", @@ -2490,6 +2492,7 @@ dependencies = [ "regex", "serde", "serde_derive", + "serde_json", "settings", "smallvec", "smol", @@ -3784,6 +3787,7 @@ dependencies = [ "lsp", "project", "serde", + "serde_json", "settings", "theme", "tree-sitter", @@ -5511,6 +5515,7 @@ dependencies = [ "picker", "postage", "project", + "serde_json", "settings", "smol", "text", @@ -7764,6 +7769,7 @@ dependencies = [ "search", "serde", "serde_derive", + "serde_json", "settings", "shellexpand", "smallvec", @@ -7844,6 +7850,7 @@ dependencies = [ "pathfinder_color", "rust-embed", "serde", + "serde_json", "simplelog", "strum", "theme", diff --git a/crates/ai/src/providers/open_ai/embedding.rs b/crates/ai/src/providers/open_ai/embedding.rs index d5fe4e8c5842709c587b9898862e0a2461461ed2..0a9b6ba969c7c519d337ae27db45af12252efa0b 100644 --- a/crates/ai/src/providers/open_ai/embedding.rs +++ b/crates/ai/src/providers/open_ai/embedding.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::AsyncReadExt; +use gpui::AppContext; use gpui::BackgroundExecutor; -use gpui::{serde_json, AppContext}; use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; @@ -11,6 +11,7 @@ use parking_lot::{Mutex, RwLock}; use parse_duration::parse; use postage::watch; use serde::{Deserialize, Serialize}; +use serde_json; use std::env; use std::ops::Add; use std::sync::Arc; diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index c24cbca35be25aeca198cd9178467bfb93db2969..03d6c06fe399842cad6a6de6057503d5f43a5bbb 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -36,6 +36,7 @@ rand.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true smol.workspace = true sysinfo.workspace = true tempfile = "3" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b07dddc006d607720412e68203b8445d8c026a1e..3eae9d92bb9f8d75cbf7ddb63eab26fdd70ecdc4 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -15,8 +15,8 @@ use futures::{ TryFutureExt as _, TryStreamExt, }; use gpui::{ - actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, - SemanticVersion, Task, WeakModel, + actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model, SemanticVersion, Task, + WeakModel, }; use lazy_static::lazy_static; use parking_lot::RwLock; @@ -25,6 +25,7 @@ use rand::prelude::*; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json; use settings::Settings; use std::{ any::TypeId, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 94d8369a698419cb11bc8bddfb12b24145127952..26b5748187ff735ebedf2cdb38753bdb6d9fcedc 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,10 +1,11 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use chrono::{DateTime, Utc}; use futures::Future; -use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; +use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; +use serde_json; use settings::{Settings, SettingsStore}; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{ diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 8206d89dce5901ba8954c55cc62b81a2b738847a..f845de3a939886fefa42343131e0c4ec18543fea 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -61,6 +61,7 @@ schemars.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true time.workspace = true smallvec.workspace = true diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index b142fcbe7f93146ede3d6ba6edff8e03de1540e7..a13c0ed384f934d35a64cd29a5ebbc53070a7281 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -7,9 +7,9 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ - actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext, - ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, - Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, + actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent, + ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render, + Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::LanguageRegistry; use menu::Confirm; diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 663883f9b4b86f6b0e70f28d31b345851a5be269..ee43b32f106e8228806a1ca6108b05b00038ca22 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -15,12 +15,12 @@ use editor::{Editor, EditorElement, EditorStyle}; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement, - AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, - FocusHandle, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset, - ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, - RenderOnce, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, - VisualContext, WeakView, WhiteSpace, + actions, canvas, div, fill, list, overlay, point, prelude::*, px, AnyElement, AppContext, + AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement, ListOffset, ListState, + Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, + SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, + WeakView, WhiteSpace, }; use menu::{Cancel, Confirm, SelectNext, SelectPrev}; use project::{Fs, Project}; diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index b7a18365bee610cbfa1cdebe1410fe9dda1f2891..e7c94984b229165aa26a43000221446d7b56e7a5 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -6,11 +6,11 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, - CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, - InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model, - ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext, - VisualContext, WeakView, WindowContext, + actions, div, img, list, px, AnyElement, AppContext, AsyncWindowContext, CursorStyle, + DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, + StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b64e3cccc3e6330429eefb54a410024cbc5e1ce6..66f28db3e463d39d14cff2ab3ab1890e6e98995f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9,11 +9,7 @@ use crate::{ }; use futures::StreamExt; -use gpui::{ - div, - serde_json::{self, json}, - TestAppContext, VisualTestContext, WindowBounds, WindowOptions, -}; +use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, @@ -24,6 +20,7 @@ use language::{ use parking_lot::Mutex; use project::project_settings::{LspSettings, ProjectSettings}; use project::FakeFs; +use serde_json::{self, json}; use std::sync::atomic; use std::sync::atomic::AtomicUsize; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 18916f844cec8dc379de5f56259940e183aad447..3289471e81c51c72586e21af1747c6ab8d376930 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -39,7 +39,7 @@ impl EditorTestContext { // fs.insert_file("/file", "".to_owned()).await; fs.insert_tree( "/root", - gpui::serde_json::json!({ + serde_json::json!({ "file": "", }), ) diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index e14df6a2d48264b37686a5bb942cc9ce9dcb7504..32ecee529c2bcd1de88b778105cffa5e58706e29 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -36,6 +36,7 @@ postage.workspace = true regex.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true smallvec.workspace = true smol.workspace = true sysinfo.workspace = true diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 87d39186711f8e054c8eb433c4e88f62ce4a388e..6c5308c1c64e3b6339dbdfff714c5a317dbe7842 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -7,8 +7,8 @@ use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorEvent}; use futures::AsyncReadExt; use gpui::{ - div, red, rems, serde_json, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, - Model, PromptLevel, Render, Task, View, ViewContext, + div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, + PromptLevel, Render, Task, View, ViewContext, }; use isahc::Request; use language::Buffer; diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 54682a30ef95419af76c09a9a31cae52df7b4496..e335c4255e4deb0d6c5720b03ce017952a6fa229 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -29,7 +29,7 @@ use std::any::{Any, TypeId}; /// macro, which only generates the code needed to register your action before `main`. /// /// ``` -/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)] +/// #[derive(gpui::private::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)] /// pub struct Paste { /// pub content: SharedString, /// } @@ -158,12 +158,12 @@ impl ActionRegistry { macro_rules! actions { ($namespace:path, [ $($name:ident),* $(,)? ]) => { $( - #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)] - #[serde(crate = "gpui::serde")] + #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::private::serde_derive::Deserialize)] + #[serde(crate = "gpui::private::serde")] pub struct $name; gpui::__impl_action!($namespace, $name, - fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box> { + fn build(_: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box> { Ok(Box::new(Self)) } ); @@ -179,8 +179,8 @@ macro_rules! impl_actions { ($namespace:path, [ $($name:ident),* $(,)? ]) => { $( gpui::__impl_action!($namespace, $name, - fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box> { - Ok(std::boxed::Box::new(gpui::serde_json::from_value::(value)?)) + fn build(value: gpui::private::serde_json::Value) -> gpui::Result<::std::boxed::Box> { + Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::(value)?)) } ); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2a0ff545e92ee6ea43a580a3a5c39e648b6c947f..638396abc51e97f58a93fabec816bd8d48a50135 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -43,7 +43,7 @@ use util::{ ResultExt, }; -/// Temporary(?) wrapper around RefCell to help us debug any double borrows. +/// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. /// Strongly consider removing after stabilization. pub struct AppCell { app: RefCell, @@ -964,7 +964,7 @@ impl AppContext { /// Event handlers propagate events by default. Call this method to stop dispatching to /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is - /// the opposite of [propagate]. It's also possible to cancel a call to [propagate] by + /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by /// calling this method before effects are flushed. pub fn stop_propagation(&mut self) { self.propagate_event = false; @@ -972,7 +972,7 @@ impl AppContext { /// Action handlers stop propagation by default during the bubble phase of action dispatch /// dispatching to action handlers higher in the element tree. This is the opposite of - /// [stop_propagation]. It's also possible to cancel a call to [stop_propagate] by calling + /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling /// this method before effects are flushed. pub fn propagate(&mut self) { self.propagate_event = true; diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 97f680560a3cabd0e7fb88e5da8ca691fd31364e..17f6e47ddfec85de1c82b1fc0b8cd6ffc285aa32 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -1,4 +1,4 @@ -use crate::{private::Sealed, AppContext, Context, Entity, ModelContext}; +use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext}; use anyhow::{anyhow, Result}; use derive_more::{Deref, DerefMut}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index d68ab89f6c8c35d577f7be223db6f198546f717c..a50de8c344247c705d24c8d9a0e94c7c799278bb 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -1582,7 +1582,6 @@ impl From for Edges { /// Represents the corners of a box in a 2D space, such as border radius. /// /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. -/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -2263,7 +2262,7 @@ impl From for GlobalPixels { } } -/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [WindowContext::set_rem_size]. +/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`WindowContext::set_rem_size`][set_rem_size]. /// /// Rems are used for defining lengths that are scalable and consistent across different UI elements. /// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), @@ -2271,6 +2270,8 @@ impl From for GlobalPixels { /// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. /// /// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. +/// +/// [set_rem_size]: crate::WindowContext::set_rem_size #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] pub struct Rems(pub f32); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 868822d59bda9cd42e4ba9857fd7a95910bab59f..d5236d8f08c7f2fa4cb3551ee5a58695634f7e8a 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -30,7 +30,16 @@ mod util; mod view; mod window; -mod private { +/// Do not touch, here be dragons for use by gpui_macros and such. +#[doc(hidden)] +pub mod private { + pub use linkme; + pub use serde; + pub use serde_derive; + pub use serde_json; +} + +mod seal { /// A mechanism for restricting implementations of a trait to only those in GPUI. /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ pub trait Sealed {} @@ -53,16 +62,11 @@ pub use input::*; pub use interactive::*; pub use key_dispatch::*; pub use keymap::*; -pub use linkme; pub use platform::*; -use private::Sealed; pub use refineable::*; pub use scene::*; -pub use serde; -pub use serde_derive; -pub use serde_json; +use seal::Sealed; pub use shared_string::*; -pub use smallvec; pub use smol::Timer; pub use style::*; pub use styled::*; diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index 8592eeffeb3573e4d77e55e252a814630d3c959d..da240a77a8b633683d11d96356451dd889e17818 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -5,8 +5,8 @@ use std::ops::Range; /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. /// -/// Once your view `V` implements this trait, you can use it to construct an [ElementInputHandler]. -/// This input handler can then be assigned during paint by calling [WindowContext::handle_input]. +/// Once your view `V` implements this trait, you can use it to construct an [`ElementInputHandler`]. +/// This input handler can then be assigned during paint by calling [`WindowContext::handle_input`]. pub trait InputHandler: 'static + Sized { fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) -> Option; @@ -43,8 +43,10 @@ pub struct ElementInputHandler { } impl ElementInputHandler { - /// Used in [Element::paint] with the element's bounds and a view context for its + /// Used in [`Element::paint`][element_paint] with the element's bounds and a view context for its /// containing view. + /// + /// [element_paint]: crate::Element::paint pub fn new(element_bounds: Bounds, view: View, cx: &mut WindowContext) -> Self { ElementInputHandler { view, diff --git a/crates/gpui/src/platform/mac/display_linker.rs b/crates/gpui/src/platform/mac/display_linker.rs index e367d22b3d7099cdc84fb67a97b013d3c63503e8..8f1b233046fd85ffc4d3b875e73e5f891841c75a 100644 --- a/crates/gpui/src/platform/mac/display_linker.rs +++ b/crates/gpui/src/platform/mac/display_linker.rs @@ -94,7 +94,7 @@ unsafe extern "C" fn trampoline( mod sys { //! Derived from display-link crate under the fololwing license: - //! https://github.com/BrainiumLLC/display-link/blob/master/LICENSE-MIT + //! //! Apple docs: [CVDisplayLink](https://developer.apple.com/documentation/corevideo/cvdisplaylinkoutputcallback?language=objc) #![allow(dead_code, non_upper_case_globals)] diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 88e564d27fe27b77967a5af86da10fa3b7798ea5..6e6223cbbea97f74e9ce57b5e207069c1821cc02 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,5 +1,5 @@ use crate::{ - private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, + seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel, WindowContext, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index a0d3d0f886bbb7aeed136196740272c2da3e4f43..7e4c5f93f95e6ea770d404a63a3e6795d9a4be7d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1826,9 +1826,11 @@ impl<'a> WindowContext<'a> { result } - /// Set an input handler, such as [ElementInputHandler], which interfaces with the + /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the /// platform to receive textual input with proper integration with concerns such /// as IME interactions. + /// + /// [element_input_handler]: crate::ElementInputHandler pub fn handle_input( &mut self, focus_handle: &FocusHandle, @@ -2500,8 +2502,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Register a listener to be called when the given focus handle receives focus. - /// Unlike [on_focus_changed], returns a subscription and persists until the subscription - /// is dropped. + /// Returns a subscription and persists until the subscription is dropped. pub fn on_focus( &mut self, handle: &FocusHandle, @@ -2527,8 +2528,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Register a listener to be called when the given focus handle or one of its descendants receives focus. - /// Unlike [on_focus_changed], returns a subscription and persists until the subscription - /// is dropped. + /// Returns a subscription and persists until the subscription is dropped. pub fn on_focus_in( &mut self, handle: &FocusHandle, @@ -2554,8 +2554,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Register a listener to be called when the given focus handle loses focus. - /// Unlike [on_focus_changed], returns a subscription and persists until the subscription - /// is dropped. + /// Returns a subscription and persists until the subscription is dropped. pub fn on_blur( &mut self, handle: &FocusHandle, @@ -2581,8 +2580,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Register a listener to be called when the window loses focus. - /// Unlike [on_focus_changed], returns a subscription and persists until the subscription - /// is dropped. + /// Returns a subscription and persists until the subscription is dropped. pub fn on_blur_window( &mut self, mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, @@ -2597,8 +2595,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Register a listener to be called when the given focus handle or one of its descendants loses focus. - /// Unlike [on_focus_changed], returns a subscription and persists until the subscription - /// is dropped. + /// Returns a subscription and persists until the subscription is dropped. pub fn on_focus_out( &mut self, handle: &FocusHandle, diff --git a/crates/gpui/tests/action_macros.rs b/crates/gpui/tests/action_macros.rs index f77d8a2d0da31d64ba8e0dd818520ac91d430011..9e5f6dea16ca6ad0ad7a1eb7f7098b61e0fd4cea 100644 --- a/crates/gpui/tests/action_macros.rs +++ b/crates/gpui/tests/action_macros.rs @@ -11,7 +11,7 @@ fn test_action_macros() { impl_actions!(test, [AnotherTestAction]); - #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)] + #[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)] struct RegisterableAction {} register_action!(RegisterableAction); diff --git a/crates/gpui_macros/src/register_action.rs b/crates/gpui_macros/src/register_action.rs index 9e3a473843df734b58190675e6f4fb78110b5c6f..c18e4f4b89a68859b1c413357649cf6ad025e8d5 100644 --- a/crates/gpui_macros/src/register_action.rs +++ b/crates/gpui_macros/src/register_action.rs @@ -36,8 +36,8 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream { quote! { #[doc(hidden)] - #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)] - #[linkme(crate = gpui::linkme)] + #[gpui::private::linkme::distributed_slice(gpui::__GPUI_ACTIONS)] + #[linkme(crate = gpui::private::linkme)] static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name; /// This is an auto generated function, do not use. diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 0bf064092e4787c96b1b3c475f3fd09f72685a5d..1681e78425fa012fd08042a660e0283797d9879f 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -24,6 +24,7 @@ futures.workspace = true serde.workspace = true anyhow.workspace = true tree-sitter.workspace = true +serde_json.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index 194b6d1ae8d4f0a83dd1d98a8c8b0b360c7fdd78..14683ae8069771b730c4e008c19801b669160154 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -4,9 +4,10 @@ use crate::lsp_log::LogMenuItem; use super::*; use futures::StreamExt; -use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext}; +use gpui::{Context, TestAppContext, VisualTestContext}; use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; use project::{FakeFs, Project}; +use serde_json::json; use settings::SettingsStore; #[gpui::test] diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index 91cc882e41e126fc8fb297866b3ffa4f9394b3a0..da96fb2db56418e117f4c62c982c4d39884f86aa 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -24,6 +24,7 @@ anyhow.workspace = true ordered-float.workspace = true postage.workspace = true smol.workspace = true +serde_json.workspace = true [dev-dependencies] futures.workspace = true diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 3c2760f720c02f411da1ef08f1d8e8bad926716f..ed31ebd94997bd01d53b56df4f62dfff0518f737 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -260,9 +260,10 @@ impl PickerDelegate for ProjectSymbolsDelegate { mod tests { use super::*; use futures::StreamExt; - use gpui::{serde_json::json, TestAppContext, VisualContext}; + use gpui::{TestAppContext, VisualContext}; use language::{FakeLspAdapter, Language, LanguageConfig}; use project::FakeFs; + use serde_json::json; use settings::SettingsStore; use std::{path::Path, sync::Arc}; diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index d84846018346dc530465eb66835f9a9cc894ac59..811d420045dce27505cccf9b63fd0d49491501d9 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -36,6 +36,7 @@ thiserror.workspace = true lazy_static.workspace = true serde.workspace = true serde_derive.workspace = true +serde_json.workspace = true [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index c0f8e6209b208cb8125b82a8626b85fac76cbac1..99929535700e2badbe0c447ac6dfc4ee5998e7e2 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -3,9 +3,9 @@ use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ - actions, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, - FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, + FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; use project::{Fs, ProjectEntryId}; diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index 1b0c39fee72c2cb1fdf6b5d419934441b2643c0f..7bcb2daa633e714ed18ef1a78e2aab4c55e885cd 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -18,6 +18,7 @@ palette = { version = "0.7.3", default-features = false, features = ["std"] } pathfinder_color = "0.5" rust-embed.workspace = true serde.workspace = true +serde_json.workspace = true simplelog = "0.9" strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme", features = ["importing-themes"] } diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index 3a7e0b20d55e2ecd92ff627cb878af3faaae1775..ff20d36a5df6ad3ffebbe0dc4f58a85ec1383707 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -16,7 +16,6 @@ use any_ascii::any_ascii; use anyhow::{anyhow, Context, Result}; use clap::Parser; use convert_case::{Case, Casing}; -use gpui::serde_json; use indexmap::IndexMap; use indoc::formatdoc; use json_comments::StripComments; diff --git a/crates/theme_importer/src/zed1/converter.rs b/crates/theme_importer/src/zed1/converter.rs index 86c40dfde55a29cb7bb0c6745ae433ecba6553d3..9f40c3695fcc6773e549d829e84a922aceef567c 100644 --- a/crates/theme_importer/src/zed1/converter.rs +++ b/crates/theme_importer/src/zed1/converter.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use gpui::{serde_json, Hsla, Rgba}; +use gpui::{Hsla, Rgba}; use theme::{ color_alpha, Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement, UserFontStyle, UserFontWeight, UserHighlightStyle, UserSyntaxTheme, diff --git a/crates/theme_importer/src/zed1/theme.rs b/crates/theme_importer/src/zed1/theme.rs index 7743fba0b1be00719f5303decedbaa7f027fc2bc..70efb6ab2bfaef988cc668b6f30ce4981a3471f8 100644 --- a/crates/theme_importer/src/zed1/theme.rs +++ b/crates/theme_importer/src/zed1/theme.rs @@ -6,10 +6,10 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::sync::Arc; -use gpui::serde_json::{self, Value}; use pathfinder_color::ColorU; use serde::de::{self, DeserializeOwned, Unexpected}; use serde::{Deserialize, Deserializer}; +use serde_json::{self, Value}; #[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(transparent)] From 53564fb2696f0e1ff6cbc2a0e8b8a08802db15a5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 8 Jan 2024 12:29:54 +0100 Subject: [PATCH 34/43] Bring back zed.rs tests (#3907) At present 3 tests still fail; 2 are related to keymap issues that (I believe) @maxbrunsfeld is working on. The other one (`test_open_paths_action`) I'll look into. edit: done This PR also fixes workspace unregistration, as we've put the code to do that behind `debug_assert` (https://github.com/zed-industries/zed/pull/3907/files#diff-041673bbd1947a35d45945636c0055429dfc8b5985faf93f8a8a960c9ad31e28L649). Release Notes: - N/A --- crates/gpui/src/app/test_context.rs | 27 + crates/gpui/src/platform/test/platform.rs | 2 +- crates/gpui/src/platform/test/window.rs | 11 +- crates/gpui/src/view.rs | 10 +- crates/workspace/src/workspace.rs | 2 +- crates/zed/Cargo.toml | 5 +- crates/zed/src/zed.rs | 3944 +++++++++++---------- 7 files changed, 2132 insertions(+), 1869 deletions(-) diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 470315f887c6fd5e4c6d3523498dbe496ff041a8..0f71ea61a9ec347b01e601f5e3c1a2237b80e691 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -532,6 +532,33 @@ impl<'a> VisualTestContext { } self.background_executor.run_until_parked(); } + /// Returns true if the window was closed. + pub fn simulate_close(&mut self) -> bool { + let handler = self + .cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .0 + .lock() + .should_close_handler + .take() + }) + .unwrap(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.cx + .update_window(self.window, |_, cx| { + cx.window.platform_window.on_should_close(handler); + }) + .unwrap(); + should_close + } else { + false + } + } } impl Context for VisualTestContext { diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 111fb839211b89c7c47b6d65f7448a92a5997675..695323e9c46b8e2a8f4260a682d8e214f58c43f4 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -266,7 +266,7 @@ impl Platform for TestPlatform { } fn local_timezone(&self) -> time::UtcOffset { - unimplemented!() + time::UtcOffset::UTC } fn path_for_auxiliary_executable(&self, _name: &str) -> Result { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index f089531b0c94b556d48a1c9a8c6777b863649a10..91f965c10ac2987f4f6c8c15cb40a7cd94171370 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -18,7 +18,7 @@ pub struct TestWindowState { pub(crate) edited: bool, platform: Weak, sprite_atlas: Arc, - + pub(crate) should_close_handler: Option bool>>, input_callback: Option bool>>, active_status_change_callback: Option>, resize_callback: Option, f32)>>, @@ -44,7 +44,7 @@ impl TestWindow { sprite_atlas: Arc::new(TestAtlas::new()), title: Default::default(), edited: false, - + should_close_handler: None, input_callback: None, active_status_change_callback: None, resize_callback: None, @@ -117,6 +117,9 @@ impl TestWindow { self.0.lock().input_handler = Some(input_handler); } + pub fn edited(&self) -> bool { + self.0.lock().edited + } } impl PlatformWindow for TestWindow { @@ -235,8 +238,8 @@ impl PlatformWindow for TestWindow { self.0.lock().moved_callback = Some(callback) } - fn on_should_close(&self, _callback: Box bool>) { - unimplemented!() + fn on_should_close(&self, callback: Box bool>) { + self.0.lock().should_close_handler = Some(callback); } fn on_close(&self, _callback: Box) { diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 6e6223cbbea97f74e9ce57b5e207069c1821cc02..4472da02e71fda1bb17d4353056b67ad58639813 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::{Context, Result}; use std::{ - any::TypeId, + any::{type_name, TypeId}, fmt, hash::{Hash, Hasher}, }; @@ -104,6 +104,14 @@ impl Clone for View { } } +impl std::fmt::Debug for View { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(&format!("View<{}>", type_name::())) + .field("entity_id", &self.model.entity_id) + .finish_non_exhaustive() + } +} + impl Hash for View { fn hash(&self, state: &mut H) { self.model.hash(state); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 84b84677d1c13e25be0b3fa1fc63725cb93e5c89..826a6693d7ca350a85efa4589e5c87ba90bbf94c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -658,7 +658,7 @@ impl Workspace { cx.on_release(|this, window, cx| { this.app_state.workspace_store.update(cx, |store, _| { let window = window.downcast::().unwrap(); - debug_assert!(store.workspaces.remove(&window)); + store.workspaces.remove(&window); }) }), ]; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 39ab5e285b77672e31d8b33555f83bd768e4be68..c17d9c781c217641330847b6000c9c4676fb5bd4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -146,8 +146,7 @@ uuid.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } # client = { path = "../client", features = ["test-support"] } -# editor = { path = "../editor", features = ["test-support"] } -# gpui = { path = "../gpui", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } # lsp = { path = "../lsp", features = ["test-support"] } @@ -156,7 +155,7 @@ project = { path = "../project", features = ["test-support"] } # settings = { path = "../settings", features = ["test-support"] } text = { path = "../text", features = ["test-support"] } # util = { path = "../util", features = ["test-support"] } -# workspace = { path = "../workspace", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true [package.metadata.bundle-dev] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c7d30230ea035b29121ceb487e4ea3ce483d5d54..702c815d34600b8502e55f83f60b17c572dc869e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -33,11 +33,11 @@ use util::{ }; use uuid::Uuid; use welcome::BaseKeymap; -use workspace::Pane; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; +use workspace::{dock::Panel, Pane}; use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit}; actions!( @@ -114,9 +114,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .detach(); - // cx.emit(workspace2::Event::PaneAdded( - // workspace.active_pane().clone(), - // )); + // cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); // let collab_titlebar_item = // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); @@ -187,6 +185,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { )?; workspace_handle.update(&mut cx, |workspace, cx| { + let position = project_panel.read(cx).position(cx); workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); workspace.add_panel(assistant_panel, cx); @@ -194,19 +193,18 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace.add_panel(chat_panel, cx); workspace.add_panel(notification_panel, cx); - // if !was_deserialized - // && workspace - // .project() - // .read(cx) - // .visible_worktrees(cx) - // .any(|tree| { - // tree.read(cx) - // .root_entry() - // .map_or(false, |entry| entry.is_dir()) - // }) - // { - // workspace.toggle_dock(project_panel_position, cx); - // } + if workspace + .project() + .read(cx) + .visible_worktrees(cx) + .any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) + }) + { + workspace.toggle_dock(position, cx); + } cx.focus_self(); }) }) @@ -587,7 +585,6 @@ pub fn handle_keymap_file_changes( } } } - cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok(); } }) @@ -770,1844 +767,2073 @@ fn open_bundled_file( } // todo!() -// #[cfg(test)] -// mod tests { -// use super::*; -// use assets::Assets; -// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; -// use fs::{FakeFs, Fs}; -// use gpui::{ -// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle, -// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, -// }; -// use language::LanguageRegistry; -// use project::{project_settings::ProjectSettings, Project, ProjectPath}; -// use serde_json::json; -// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; -// use std::{ -// collections::HashSet, -// path::{Path, PathBuf}, -// }; -// use theme::{ThemeRegistry, ThemeSettings}; -// use workspace::{ -// item::{Item, ItemHandle}, -// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle, -// }; - -// #[gpui::test] -// async fn test_open_paths_action(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "aa": null, -// "ab": null, -// }, -// "b": { -// "ba": null, -// "bb": null, -// }, -// "c": { -// "ca": null, -// "cb": null, -// }, -// "d": { -// "da": null, -// "db": null, -// }, -// }), -// ) -// .await; - -// cx.update(|cx| { -// open_paths( -// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], -// &app_state, -// None, -// cx, -// ) -// }) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); - -// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); -// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); -// workspace_1.update(cx, |workspace, cx| { -// assert_eq!(workspace.worktrees(cx).count(), 2); -// assert!(workspace.left_dock().read(cx).is_open()); -// assert!(workspace.active_pane().is_focused(cx)); -// }); - -// cx.update(|cx| { -// open_paths( -// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], -// &app_state, -// None, -// cx, -// ) -// }) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 2); - -// // Replace existing windows -// let window = cx.windows()[0].downcast::().unwrap(); -// cx.update(|cx| { -// open_paths( -// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], -// &app_state, -// Some(window), -// cx, -// ) -// }) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 2); -// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); -// workspace_1.update(cx, |workspace, cx| { -// assert_eq!( -// workspace -// .worktrees(cx) -// .map(|w| w.read(cx).abs_path()) -// .collect::>(), -// &[Path::new("/root/c").into(), Path::new("/root/d").into()] -// ); -// assert!(workspace.left_dock().read(cx).is_open()); -// assert!(workspace.active_pane().is_focused(cx)); -// }); -// } - -// #[gpui::test] -// async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({"a": "hey"})) -// .await; - -// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); - -// // When opening the workspace, the window is not in a edited state. -// let window = cx.windows()[0].downcast::().unwrap(); -// let workspace = window.root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert!(!window.is_edited(cx)); - -// // Editing a buffer marks the window as edited. -// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); -// assert!(window.is_edited(cx)); - -// // Undoing the edit restores the window's edited state. -// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); -// assert!(!window.is_edited(cx)); - -// // Redoing the edit marks the window as edited again. -// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); -// assert!(window.is_edited(cx)); - -// // Closing the item restores the window's edited state. -// let close = pane.update(cx, |pane, cx| { -// drop(editor); -// pane.close_active_item(&Default::default(), cx).unwrap() -// }); -// executor.run_until_parked(); - -// window.simulate_prompt_answer(1, cx); -// close.await.unwrap(); -// assert!(!window.is_edited(cx)); - -// // Opening the buffer again doesn't impact the window's edited state. -// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) -// .await -// .unwrap(); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// assert!(!window.is_edited(cx)); - -// // Editing the buffer marks the window as edited. -// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); -// assert!(window.is_edited(cx)); - -// // Ensure closing the window via the mouse gets preempted due to the -// // buffer having unsaved changes. -// assert!(!window.simulate_close(cx)); -// executor.run_until_parked(); -// assert_eq!(cx.windows().len(), 1); - -// // The window is successfully closed after the user dismisses the prompt. -// window.simulate_prompt_answer(1, cx); -// executor.run_until_parked(); -// assert_eq!(cx.windows().len(), 0); -// } - -// #[gpui::test] -// async fn test_new_empty_workspace(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// cx.update(|cx| { -// open_new(&app_state, cx, |workspace, cx| { -// Editor::new_file(workspace, &Default::default(), cx) -// }) -// }) -// .await; - -// let window = cx -// .windows() -// .first() -// .unwrap() -// .downcast::() -// .unwrap(); -// let workspace = window.root(cx); - -// let editor = workspace.update(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// editor.update(cx, |editor, cx| { -// assert!(editor.text(cx).is_empty()); -// assert!(!editor.is_dirty(cx)); -// }); - -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); -// save_task.await.unwrap(); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "the-new-name"); -// }); -// } - -// #[gpui::test] -// async fn test_open_entry(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1", -// "file2": "contents 2", -// "file3": "contents 3", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let file1 = entries[0].clone(); -// let file2 = entries[1].clone(); -// let file3 = entries[2].clone(); - -// // Open the first entry -// let entry_1 = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap(); -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file1.clone()) -// ); -// assert_eq!(pane.items_len(), 1); -// }); - -// // Open the second entry -// workspace -// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) -// .await -// .unwrap(); -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file2.clone()) -// ); -// assert_eq!(pane.items_len(), 2); -// }); - -// // Open the first entry again. The existing pane item is activated. -// let entry_1b = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap(); -// assert_eq!(entry_1.id(), entry_1b.id()); - -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file1.clone()) -// ); -// assert_eq!(pane.items_len(), 2); -// }); - -// // Split the pane with the first entry, then open the second entry again. -// workspace -// .update(cx, |w, cx| { -// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx); -// w.open_path(file2.clone(), None, true, cx) -// }) -// .await -// .unwrap(); - -// workspace.read_with(cx, |w, cx| { -// assert_eq!( -// w.active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .project_path(cx), -// Some(file2.clone()) -// ); -// }); - -// // Open the third entry twice concurrently. Only one pane item is added. -// let (t1, t2) = workspace.update(cx, |w, cx| { -// ( -// w.open_path(file3.clone(), None, true, cx), -// w.open_path(file3.clone(), None, true, cx), -// ) -// }); -// t1.await.unwrap(); -// t2.await.unwrap(); -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// assert_eq!( -// pane.active_item().unwrap().project_path(cx), -// Some(file3.clone()) -// ); -// let pane_entries = pane -// .items() -// .map(|i| i.project_path(cx).unwrap()) -// .collect::>(); -// assert_eq!(pane_entries, &[file1, file2, file3]); -// }); -// } - -// #[gpui::test] -// async fn test_open_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/", -// json!({ -// "dir1": { -// "a.txt": "" -// }, -// "dir2": { -// "b.txt": "" -// }, -// "dir3": { -// "c.txt": "" -// }, -// "d.txt": "" -// }), -// ) -// .await; - -// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) -// .await -// .unwrap(); -// assert_eq!(cx.windows().len(), 1); -// let workspace = cx.windows()[0].downcast::().unwrap().root(cx); - -// #[track_caller] -// fn assert_project_panel_selection( -// workspace: &Workspace, -// expected_worktree_path: &Path, -// expected_entry_path: &Path, -// cx: &AppContext, -// ) { -// let project_panel = [ -// workspace.left_dock().read(cx).panel::(), -// workspace.right_dock().read(cx).panel::(), -// workspace.bottom_dock().read(cx).panel::(), -// ] -// .into_iter() -// .find_map(std::convert::identity) -// .expect("found no project panels") -// .read(cx); -// let (selected_worktree, selected_entry) = project_panel -// .selected_entry(cx) -// .expect("project panel should have a selected entry"); -// assert_eq!( -// selected_worktree.abs_path().as_ref(), -// expected_worktree_path, -// "Unexpected project panel selected worktree path" -// ); -// assert_eq!( -// selected_entry.path.as_ref(), -// expected_entry_path, -// "Unexpected project panel selected entry path" -// ); -// } - -// // Open a file within an existing worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/dir1/a.txt".into()], true, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "a.txt" -// ); -// }); - -// // Open a file outside of any existing worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/dir2/b.txt".into()], true, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); -// let worktree_roots = workspace -// .worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "b.txt" -// ); -// }); - -// // Ensure opening a directory and one of its children only adds one worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); -// let worktree_roots = workspace -// .worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt", "/dir3"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "c.txt" -// ); -// }); - -// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec!["/d.txt".into()], false, cx) -// }) -// .await; -// cx.read(|cx| { -// let workspace = workspace.read(cx); -// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); -// let worktree_roots = workspace -// .worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// worktree_roots, -// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); - -// let visible_worktree_roots = workspace -// .visible_worktrees(cx) -// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) -// .collect::>(); -// assert_eq!( -// visible_worktree_roots, -// vec!["/dir1", "/dir2/b.txt", "/dir3"] -// .into_iter() -// .map(Path::new) -// .collect(), -// ); - -// assert_eq!( -// workspace -// .active_pane() -// .read(cx) -// .active_item() -// .unwrap() -// .as_any() -// .downcast_ref::() -// .unwrap() -// .read(cx) -// .title(cx), -// "d.txt" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_opening_excluded_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, |project_settings| { -// project_settings.file_scan_exclusions = -// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); -// }); -// }); -// }); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// ".gitignore": "ignored_dir\n", -// ".git": { -// "HEAD": "ref: refs/heads/main", -// }, -// "regular_dir": { -// "file": "regular file contents", -// }, -// "ignored_dir": { -// "ignored_subdir": { -// "file": "ignored subfile contents", -// }, -// "file": "ignored file contents", -// }, -// "excluded_dir": { -// "file": "excluded file contents", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let paths_to_open = [ -// Path::new("/root/excluded_dir/file").to_path_buf(), -// Path::new("/root/.git/HEAD").to_path_buf(), -// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), -// ]; -// let (opened_workspace, new_items) = cx -// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) -// .await -// .unwrap(); - -// assert_eq!( -// opened_workspace.id(), -// workspace.id(), -// "Excluded files in subfolders of a workspace root should be opened in the workspace" -// ); -// let mut opened_paths = cx.read(|cx| { -// assert_eq!( -// new_items.len(), -// paths_to_open.len(), -// "Expect to get the same number of opened items as submitted paths to open" -// ); -// new_items -// .iter() -// .zip(paths_to_open.iter()) -// .map(|(i, path)| { -// match i { -// Some(Ok(i)) => { -// Some(i.project_path(cx).map(|p| p.path.display().to_string())) -// } -// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), -// None => None, -// } -// .flatten() -// }) -// .collect::>() -// }); -// opened_paths.sort(); -// assert_eq!( -// opened_paths, -// vec![ -// None, -// Some(".git/HEAD".to_string()), -// Some("excluded_dir/file".to_string()), -// ], -// "Excluded files should get opened, excluded dir should not get opened" -// ); - -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// assert_eq!( -// initial_entries, entries, -// "Workspace entries should not change after opening excluded files and directories paths" -// ); - -// cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// let mut opened_buffer_paths = pane -// .items() -// .map(|i| { -// i.project_path(cx) -// .expect("all excluded files that got open should have a path") -// .path -// .display() -// .to_string() -// }) -// .collect::>(); -// opened_buffer_paths.sort(); -// assert_eq!( -// opened_buffer_paths, -// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], -// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_save_conflicting_item(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "a.txt": "" })) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // Open a file within an existing worktree. -// workspace -// .update(cx, |view, cx| { -// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx) -// }) -// .await; -// let editor = cx.read(|cx| { -// let pane = workspace.read(cx).active_pane().read(cx); -// let item = pane.active_item().unwrap(); -// item.downcast::().unwrap() -// }); - -// editor.update(cx, |editor, cx| editor.handle_input("x", cx)); -// app_state -// .fs -// .as_fake() -// .insert_file("/root/a.txt", "changed".to_string()) -// .await; -// editor -// .condition(cx, |editor, cx| editor.has_conflict(cx)) -// .await; -// cx.read(|cx| assert!(editor.is_dirty(cx))); - -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// cx.foreground().run_until_parked(); -// window.simulate_prompt_answer(0, cx); -// save_task.await.unwrap(); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert!(!editor.has_conflict(cx)); -// }); -// } - -// #[gpui::test] -// async fn test_open_and_save_new_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(rust_lang())); -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); - -// // Create a new untitled buffer -// cx.dispatch_action(window.into(), NewFile); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// editor.update(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "untitled"); -// assert!(Arc::ptr_eq( -// &editor.language_at(0, cx).unwrap(), -// &languages::PLAIN_TEXT -// )); -// editor.handle_input("hi", cx); -// assert!(editor.is_dirty(cx)); -// }); - -// // Save the buffer. This prompts for a filename. -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|parent_dir| { -// assert_eq!(parent_dir, Path::new("/root")); -// Some(parent_dir.join("the-new-name.rs")) -// }); -// cx.read(|cx| { -// assert!(editor.is_dirty(cx)); -// assert_eq!(editor.read(cx).title(cx), "untitled"); -// }); - -// // When the save completes, the buffer's title is updated and the language is assigned based -// // on the path. -// save_task.await.unwrap(); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "the-new-name.rs"); -// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust"); -// }); - -// // Edit the file and save it again. This time, there is no filename prompt. -// editor.update(cx, |editor, cx| { -// editor.handle_input(" there", cx); -// assert!(editor.is_dirty(cx)); -// }); -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// save_task.await.unwrap(); -// assert!(!cx.did_prompt_for_new_path()); -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.title(cx), "the-new-name.rs") -// }); - -// // Open the same newly-created file in another pane item. The new editor should reuse -// // the same buffer. -// cx.dispatch_action(window.into(), NewFile); -// workspace -// .update(cx, |workspace, cx| { -// workspace.split_and_clone( -// workspace.active_pane().clone(), -// SplitDirection::Right, -// cx, -// ); -// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx) -// }) -// .await -// .unwrap(); -// let editor2 = workspace.update(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); -// cx.read(|cx| { -// assert_eq!( -// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), -// editor.read(cx).buffer().read(cx).as_singleton().unwrap() -// ); -// }) -// } - -// #[gpui::test] -// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - -// let project = Project::test(app_state.fs.clone(), [], cx).await; -// project.update(cx, |project, _| project.languages().add(rust_lang())); -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // Create a new untitled buffer -// cx.dispatch_action(window.into(), NewFile); -// let editor = workspace.read_with(cx, |workspace, cx| { -// workspace -// .active_item(cx) -// .unwrap() -// .downcast::() -// .unwrap() -// }); - -// editor.update(cx, |editor, cx| { -// assert!(Arc::ptr_eq( -// &editor.language_at(0, cx).unwrap(), -// &languages::PLAIN_TEXT -// )); -// editor.handle_input("hi", cx); -// assert!(editor.is_dirty(cx)); -// }); - -// // Save the buffer. This prompts for a filename. -// let save_task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(SaveIntent::Save, cx) -// }); -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); -// save_task.await.unwrap(); -// // The buffer is not dirty anymore and the language is assigned based on the path. -// editor.read_with(cx, |editor, cx| { -// assert!(!editor.is_dirty(cx)); -// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust") -// }); -// } - -// #[gpui::test] -// async fn test_pane_actions(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1", -// "file2": "contents 2", -// "file3": "contents 3", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let file1 = entries[0].clone(); - -// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); - -// workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap(); - -// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| { -// let editor = pane_1.active_item().unwrap().downcast::().unwrap(); -// assert_eq!(editor.project_path(cx), Some(file1.clone())); -// let buffer = editor.update(cx, |editor, cx| { -// editor.insert("dirt", cx); -// editor.buffer().downgrade() -// }); -// (editor.downgrade(), buffer) -// }); - -// cx.dispatch_action(window.into(), pane::SplitRight); -// let editor_2 = cx.update(|cx| { -// let pane_2 = workspace.read(cx).active_pane().clone(); -// assert_ne!(pane_1, pane_2); - -// let pane2_item = pane_2.read(cx).active_item().unwrap(); -// assert_eq!(pane2_item.project_path(cx), Some(file1.clone())); - -// pane2_item.downcast::().unwrap().downgrade() -// }); -// cx.dispatch_action( -// window.into(), -// workspace::CloseActiveItem { save_intent: None }, -// ); - -// cx.foreground().run_until_parked(); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.panes().len(), 1); -// assert_eq!(workspace.active_pane(), &pane_1); -// }); - -// cx.dispatch_action( -// window.into(), -// workspace::CloseActiveItem { save_intent: None }, -// ); -// cx.foreground().run_until_parked(); -// window.simulate_prompt_answer(1, cx); -// cx.foreground().run_until_parked(); - -// workspace.read_with(cx, |workspace, cx| { -// assert_eq!(workspace.panes().len(), 1); -// assert!(workspace.active_item(cx).is_none()); -// }); - -// cx.assert_dropped(editor_1); -// cx.assert_dropped(editor_2); -// cx.assert_dropped(buffer); -// } - -// #[gpui::test] -// async fn test_navigation(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "contents 1\n".repeat(20), -// "file2": "contents 2\n".repeat(20), -// "file3": "contents 3\n".repeat(20), -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let file1 = entries[0].clone(); -// let file2 = entries[1].clone(); -// let file3 = entries[2].clone(); - -// let editor1 = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// editor1.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { -// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)]) -// }); -// }); -// let editor2 = workspace -// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let editor3 = workspace -// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// editor3 -// .update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { -// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)]) -// }); -// editor.newline(&Default::default(), cx); -// editor.newline(&Default::default(), cx); -// editor.move_down(&Default::default(), cx); -// editor.move_down(&Default::default(), cx); -// editor.save(project.clone(), cx) -// }) -// .await -// .unwrap(); -// editor3.update(cx, |editor, cx| { -// editor.set_scroll_position(vec2f(0., 12.5), cx) -// }); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(16, 0), 12.5) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file2.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(10, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Go back one more time and ensure we don't navigate past the first item in the history. -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(10, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file2.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Go forward to an item that has been closed, ensuring it gets re-opened at the same -// // location. -// pane.update(cx, |pane, cx| { -// let editor3_id = editor3.id(); -// drop(editor3); -// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(16, 0), 12.5) -// ); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. -// pane.update(cx, |pane, cx| { -// let editor2_id = editor2.id(); -// drop(editor2); -// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// app_state -// .fs -// .remove_file(Path::new("/root/a/file2"), Default::default()) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); - -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(10, 0), 0.) -// ); -// workspace -// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file3.clone(), DisplayPoint::new(0, 0), 0.) -// ); - -// // Modify file to collapse multiple nav history entries into the same location. -// // Ensure we don't visit the same location twice when navigating. -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]) -// }) -// }); - -// for _ in 0..5 { -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) -// }); -// }); -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)]) -// }) -// }); -// } - -// editor1.update(cx, |editor, cx| { -// editor.transact(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)]) -// }); -// editor.insert("", cx); -// }) -// }); - -// editor1.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) -// }) -// }); -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(2, 0), 0.) -// ); -// workspace -// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) -// .await -// .unwrap(); -// assert_eq!( -// active_location(&workspace, cx), -// (file1.clone(), DisplayPoint::new(3, 0), 0.) -// ); - -// fn active_location( -// workspace: &ViewHandle, -// cx: &mut TestAppContext, -// ) -> (ProjectPath, DisplayPoint, f32) { -// workspace.update(cx, |workspace, cx| { -// let item = workspace.active_item(cx).unwrap(); -// let editor = item.downcast::().unwrap(); -// let (selections, scroll_position) = editor.update(cx, |editor, cx| { -// ( -// editor.selections.display_ranges(cx), -// editor.scroll_position(cx), -// ) -// }); -// ( -// item.project_path(cx).unwrap(), -// selections[0].start, -// scroll_position.y(), -// ) -// }) -// } -// } - -// #[gpui::test] -// async fn test_reopening_closed_items(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "file1": "", -// "file2": "", -// "file3": "", -// "file4": "", -// }, -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - -// let entries = cx.read(|cx| workspace.file_project_paths(cx)); -// let file1 = entries[0].clone(); -// let file2 = entries[1].clone(); -// let file3 = entries[2].clone(); -// let file4 = entries[3].clone(); - -// let file1_item_id = workspace -// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// let file2_item_id = workspace -// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// let file3_item_id = workspace -// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// let file4_item_id = workspace -// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx)) -// .await -// .unwrap() -// .id(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// // Close all the pane items in some arbitrary order. -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// pane.update(cx, |pane, cx| { -// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), None); - -// // Reopen all the closed items, ensuring they are reopened in the same order -// // in which they were closed. -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// // Reopening past the last closed item is a no-op. -// workspace -// .update(cx, Workspace::reopen_closed_item) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// // Reopening closed items doesn't interfere with navigation history. -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.go_back(workspace.active_pane().downgrade(), cx) -// }) -// .await -// .unwrap(); -// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - -// fn active_path( -// workspace: &ViewHandle, -// cx: &TestAppContext, -// ) -> Option { -// workspace.read_with(cx, |workspace, cx| { -// let item = workspace.active_item(cx)?; -// item.project_path(cx) -// }) -// } -// } - -// #[gpui::test] -// async fn test_base_keymap(cx: &mut gpui::TestAppContext) { -// struct TestView; - -// impl Entity for TestView { -// type Event = (); -// } - -// impl View for TestView { -// fn ui_name() -> &'static str { -// "TestView" -// } - -// fn render(&mut self, _: &mut ViewContext) -> AnyElement { -// Empty::new().into_any() -// } -// } - -// let executor = cx.background(); -// let fs = FakeFs::new(executor.clone()); - -// actions!(test, [A, B]); -// // From the Atom keymap -// actions!(workspace, [ActivatePreviousPane]); -// // From the JetBrains keymap -// actions!(pane, [ActivatePrevItem]); - -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "Atom" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": "test::A" -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(Assets, cx); -// welcome::init(cx); - -// cx.add_global_action(|_: &A, _cx| {}); -// cx.add_global_action(|_: &B, _cx| {}); -// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); -// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); - -// let settings_rx = watch_config_file( -// executor.clone(), -// fs.clone(), -// PathBuf::from("/settings.json"), -// ); -// let keymap_rx = -// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); - -// handle_keymap_file_changes(keymap_rx, cx); -// handle_settings_file_changes(settings_rx, cx); -// }); - -// cx.foreground().run_until_parked(); - -// let window = cx.add_window(|_| TestView); - -// // Test loading the keymap base at all -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &A), ("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test modifying the users keymap, while retaining the base keymap -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": "test::B" -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &B), ("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test modifying the base, while retaining the users keymap -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "JetBrains" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &B), ("[", &ActivatePrevItem)], -// line!(), -// ); - -// #[track_caller] -// fn assert_key_bindings_for<'a>( -// window: AnyWindowHandle, -// cx: &TestAppContext, -// actions: Vec<(&'static str, &'a dyn Action)>, -// line: u32, -// ) { -// for (key, action) in actions { -// // assert that... -// assert!( -// cx.available_actions(window, 0) -// .into_iter() -// .any(|(_, bound_action, b)| { -// // action names match... -// bound_action.name() == action.name() -// && bound_action.namespace() == action.namespace() -// // and key strokes contain the given key -// && b.iter() -// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) -// }), -// "On {} Failed to find {} with key binding {}", -// line, -// action.name(), -// key -// ); -// } -// } -// } - -// #[gpui::test] -// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { -// struct TestView; - -// impl Entity for TestView { -// type Event = (); -// } - -// impl View for TestView { -// fn ui_name() -> &'static str { -// "TestView" -// } - -// fn render(&mut self, _: &mut ViewContext) -> AnyElement { -// Empty::new().into_any() -// } -// } - -// let executor = cx.background(); -// let fs = FakeFs::new(executor.clone()); - -// actions!(test, [A, B]); -// // From the Atom keymap -// actions!(workspace, [ActivatePreviousPane]); -// // From the JetBrains keymap -// actions!(pane, [ActivatePrevItem]); - -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "Atom" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": "test::A" -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(Assets, cx); -// welcome::init(cx); - -// cx.add_global_action(|_: &A, _cx| {}); -// cx.add_global_action(|_: &B, _cx| {}); -// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); -// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); - -// let settings_rx = watch_config_file( -// executor.clone(), -// fs.clone(), -// PathBuf::from("/settings.json"), -// ); -// let keymap_rx = -// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); - -// handle_keymap_file_changes(keymap_rx, cx); -// handle_settings_file_changes(settings_rx, cx); -// }); - -// cx.foreground().run_until_parked(); - -// let window = cx.add_window(|_| TestView); - -// // Test loading the keymap base at all -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("backspace", &A), ("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test disabling the key binding for the base keymap -// fs.save( -// "/keymap.json".as_ref(), -// &r#" -// [ -// { -// "bindings": { -// "backspace": null -// } -// } -// ] -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for( -// window.into(), -// cx, -// vec![("k", &ActivatePreviousPane)], -// line!(), -// ); - -// // Test modifying the base, while retaining the users keymap -// fs.save( -// "/settings.json".as_ref(), -// &r#" -// { -// "base_keymap": "JetBrains" -// } -// "# -// .into(), -// Default::default(), -// ) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); - -// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!()); - -// #[track_caller] -// fn assert_key_bindings_for<'a>( -// window: AnyWindowHandle, -// cx: &TestAppContext, -// actions: Vec<(&'static str, &'a dyn Action)>, -// line: u32, -// ) { -// for (key, action) in actions { -// // assert that... -// assert!( -// cx.available_actions(window, 0) -// .into_iter() -// .any(|(_, bound_action, b)| { -// // action names match... -// bound_action.name() == action.name() -// && bound_action.namespace() == action.namespace() -// // and key strokes contain the given key -// && b.iter() -// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) -// }), -// "On {} Failed to find {} with key binding {}", -// line, -// action.name(), -// key -// ); -// } -// } -// } - -// #[gpui::test] -// fn test_bundled_settings_and_themes(cx: &mut AppContext) { -// cx.platform() -// .fonts() -// .add_fonts(&[ -// Assets -// .load("fonts/zed-sans/zed-sans-extended.ttf") -// .unwrap() -// .to_vec() -// .into(), -// Assets -// .load("fonts/zed-mono/zed-mono-extended.ttf") -// .unwrap() -// .to_vec() -// .into(), -// Assets -// .load("fonts/plex/IBMPlexSans-Regular.ttf") -// .unwrap() -// .to_vec() -// .into(), -// ]) -// .unwrap(); -// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); -// let mut settings = SettingsStore::default(); -// settings -// .set_default_settings(&settings::default_settings(), cx) -// .unwrap(); -// cx.set_global(settings); -// theme::init(Assets, cx); - -// let mut has_default_theme = false; -// for theme_name in themes.list(false).map(|meta| meta.name) { -// let theme = themes.get(&theme_name).unwrap(); -// assert_eq!(theme.meta.name, theme_name); -// if theme.meta.name == settings::get::(cx).theme.meta.name { -// has_default_theme = true; -// } -// } -// assert!(has_default_theme); -// } - -// #[gpui::test] -// fn test_bundled_languages(cx: &mut AppContext) { -// cx.set_global(SettingsStore::test(cx)); -// let mut languages = LanguageRegistry::test(); -// languages.set_executor(cx.background().clone()); -// let languages = Arc::new(languages); -// let node_runtime = node_runtime::FakeNodeRuntime::new(); -// languages::init(languages.clone(), node_runtime, cx); -// for name in languages.language_names() { -// languages.language_for_name(&name); -// } -// cx.foreground().run_until_parked(); -// } - -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// let mut app_state = AppState::test(cx); -// let state = Arc::get_mut(&mut app_state).unwrap(); -// state.initialize_workspace = initialize_workspace; -// state.build_window_options = build_window_options; -// theme::init((), cx); -// audio::init((), cx); -// channel::init(&app_state.client, app_state.user_store.clone(), cx); -// call::init(app_state.client.clone(), app_state.user_store.clone(), cx); -// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); -// workspace::init(app_state.clone(), cx); -// Project::init_settings(cx); -// language::init(cx); -// editor::init(cx); -// project_panel::init_settings(cx); -// collab_ui::init(&app_state, cx); -// pane::init(cx); -// project_panel::init((), cx); -// terminal_view::init(cx); -// assistant::init(cx); -// app_state -// }) -// } - -// fn rust_lang() -> Arc { -// Arc::new(language::Language::new( -// language::LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// )) -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use assets::Assets; + use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor, EditorEvent}; + use gpui::{ + actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext, + VisualTestContext, WindowHandle, + }; + use language::LanguageRegistry; + use project::{project_settings::ProjectSettings, Project, ProjectPath}; + use serde_json::json; + use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + }; + use theme::{ThemeRegistry, ThemeSettings}; + use workspace::{ + item::{Item, ItemHandle}, + open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection, + WorkspaceHandle, + }; + + #[gpui::test] + async fn test_open_paths_action(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "aa": null, + "ab": null, + }, + "b": { + "ba": null, + "bb": null, + }, + "c": { + "ca": null, + "cb": null, + }, + "d": { + "da": null, + "db": null, + }, + }), + ) + .await; + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + let workspace_1 = cx + .read(|cx| cx.windows()[0].downcast::()) + .unwrap(); + workspace_1 + .update(cx, |workspace, cx| { + assert_eq!(workspace.worktrees(cx).count(), 2); + assert!(workspace.left_dock().read(cx).is_open()); + assert!(workspace + .active_pane() + .read(cx) + .focus_handle(cx) + .is_focused(cx)); + }) + .unwrap(); + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 2); + + // Replace existing windows + let window = cx + .update(|cx| cx.windows()[0].downcast::()) + .unwrap(); + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], + &app_state, + Some(window), + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 2); + let workspace_1 = cx + .update(|cx| cx.windows()[0].downcast::()) + .unwrap(); + workspace_1 + .update(cx, |workspace, cx| { + assert_eq!( + workspace + .worktrees(cx) + .map(|w| w.read(cx).abs_path()) + .collect::>(), + &[Path::new("/root/c").into(), Path::new("/root/d").into()] + ); + assert!(workspace.left_dock().read(cx).is_open()); + assert!(workspace.active_pane().focus_handle(cx).is_focused(cx)); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_window_edit_state(cx: &mut TestAppContext) { + let executor = cx.executor(); + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({"a": "hey"})) + .await; + + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + // When opening the workspace, the window is not in a edited state. + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.test_window(window.into()).edited() + }; + let pane = window + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + + assert!(!window_is_edited(window, cx)); + + // Editing a buffer marks the window as edited. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + + assert!(window_is_edited(window, cx)); + + // Undoing the edit restores the window's edited state. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); + }) + .unwrap(); + assert!(!window_is_edited(window, cx)); + + // Redoing the edit marks the window as edited again. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); + }) + .unwrap(); + assert!(window_is_edited(window, cx)); + + // Closing the item restores the window's edited state. + let close = window + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + drop(editor); + pane.close_active_item(&Default::default(), cx).unwrap() + }) + }) + .unwrap(); + executor.run_until_parked(); + + cx.simulate_prompt_answer(1); + close.await.unwrap(); + assert!(!window_is_edited(window, cx)); + + // Opening the buffer again doesn't impact the window's edited state. + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + assert!(!window_is_edited(window, cx)); + + // Editing the buffer marks the window as edited. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + assert!(window_is_edited(window, cx)); + + // Ensure closing the window via the mouse gets preempted due to the + // buffer having unsaved changes. + assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); + executor.run_until_parked(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + // The window is successfully closed after the user dismisses the prompt. + cx.simulate_prompt_answer(1); + executor.run_until_parked(); + assert_eq!(cx.update(|cx| cx.windows().len()), 0); + } + + #[gpui::test] + async fn test_new_empty_workspace(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + }) + .await; + + let workspace = cx + .update(|cx| cx.windows().first().unwrap().downcast::()) + .unwrap(); + + let editor = workspace + .update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + assert!(editor.text(cx).is_empty()); + assert!(!editor.is_dirty(cx)); + }); + + editor + }) + .unwrap(); + + let save_task = workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); + save_task.await.unwrap(); + workspace + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name"); + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_open_entry(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + + // Open the first entry + let entry_1 = window + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file1.clone()) + ); + assert_eq!(pane.items_len(), 1); + }); + + // Open the second entry + window + .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file2.clone()) + ); + assert_eq!(pane.items_len(), 2); + }); + + // Open the first entry again. The existing pane item is activated. + let entry_1b = window + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + assert_eq!(entry_1.item_id(), entry_1b.item_id()); + + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file1.clone()) + ); + assert_eq!(pane.items_len(), 2); + }); + + // Split the pane with the first entry, then open the second entry again. + window + .update(cx, |w, cx| { + w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx); + w.open_path(file2.clone(), None, true, cx) + }) + .unwrap() + .await + .unwrap(); + + window + .read_with(cx, |w, cx| { + assert_eq!( + w.active_pane() + .read(cx) + .active_item() + .unwrap() + .project_path(cx), + Some(file2.clone()) + ); + }) + .unwrap(); + + // Open the third entry twice concurrently. Only one pane item is added. + let (t1, t2) = window + .update(cx, |w, cx| { + ( + w.open_path(file3.clone(), None, true, cx), + w.open_path(file3.clone(), None, true, cx), + ) + }) + .unwrap(); + t1.await.unwrap(); + t2.await.unwrap(); + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + assert_eq!( + pane.active_item().unwrap().project_path(cx), + Some(file3.clone()) + ); + let pane_entries = pane + .items() + .map(|i| i.project_path(cx).unwrap()) + .collect::>(); + assert_eq!(pane_entries, &[file1, file2, file3]); + }); + } + + #[gpui::test] + async fn test_open_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/", + json!({ + "dir1": { + "a.txt": "" + }, + "dir2": { + "b.txt": "" + }, + "dir3": { + "c.txt": "" + }, + "d.txt": "" + }), + ) + .await; + + cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + let workspace = window.root(cx).unwrap(); + + #[track_caller] + fn assert_project_panel_selection( + workspace: &Workspace, + expected_worktree_path: &Path, + expected_entry_path: &Path, + cx: &AppContext, + ) { + let project_panel = [ + workspace.left_dock().read(cx).panel::(), + workspace.right_dock().read(cx).panel::(), + workspace.bottom_dock().read(cx).panel::(), + ] + .into_iter() + .find_map(std::convert::identity) + .expect("found no project panels") + .read(cx); + let (selected_worktree, selected_entry) = project_panel + .selected_entry(cx) + .expect("project panel should have a selected entry"); + assert_eq!( + selected_worktree.abs_path().as_ref(), + expected_worktree_path, + "Unexpected project panel selected worktree path" + ); + assert_eq!( + selected_entry.path.as_ref(), + expected_entry_path, + "Unexpected project panel selected entry path" + ); + } + + // Open a file within an existing worktree. + window + .update(cx, |view, cx| { + view.open_paths(vec!["/dir1/a.txt".into()], OpenVisible::All, None, cx) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "a.txt" + ); + }); + + // Open a file outside of any existing worktree. + window + .update(cx, |view, cx| { + view.open_paths(vec!["/dir2/b.txt".into()], OpenVisible::All, None, cx) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); + let worktree_roots = workspace + .worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt"] + .into_iter() + .map(Path::new) + .collect(), + ); + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "b.txt" + ); + }); + + // Ensure opening a directory and one of its children only adds one worktree. + window + .update(cx, |view, cx| { + view.open_paths( + vec!["/dir3".into(), "/dir3/c.txt".into()], + OpenVisible::All, + None, + cx, + ) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); + let worktree_roots = workspace + .worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt", "/dir3"] + .into_iter() + .map(Path::new) + .collect(), + ); + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "c.txt" + ); + }); + + // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. + window + .update(cx, |view, cx| { + view.open_paths(vec!["/d.txt".into()], OpenVisible::None, None, cx) + }) + .unwrap() + .await; + cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); + let worktree_roots = workspace + .worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + worktree_roots, + vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] + .into_iter() + .map(Path::new) + .collect(), + ); + + let visible_worktree_roots = workspace + .visible_worktrees(cx) + .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) + .collect::>(); + assert_eq!( + visible_worktree_roots, + vec!["/dir1", "/dir2/b.txt", "/dir3"] + .into_iter() + .map(Path::new) + .collect(), + ); + + assert_eq!( + workspace + .active_pane() + .read(cx) + .active_item() + .unwrap() + .act_as::(cx) + .unwrap() + .read(cx) + .title(cx), + "d.txt" + ); + }); + } + + #[gpui::test] + async fn test_opening_excluded_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = + Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); + }); + }); + }); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + ".gitignore": "ignored_dir\n", + ".git": { + "HEAD": "ref: refs/heads/main", + }, + "regular_dir": { + "file": "regular file contents", + }, + "ignored_dir": { + "ignored_subdir": { + "file": "ignored subfile contents", + }, + "file": "ignored file contents", + }, + "excluded_dir": { + "file": "excluded file contents", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); + let paths_to_open = [ + Path::new("/root/excluded_dir/file").to_path_buf(), + Path::new("/root/.git/HEAD").to_path_buf(), + Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), + ]; + let (opened_workspace, new_items) = cx + .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) + .await + .unwrap(); + + assert_eq!( + opened_workspace.root_view(cx).unwrap().entity_id(), + workspace.entity_id(), + "Excluded files in subfolders of a workspace root should be opened in the workspace" + ); + let mut opened_paths = cx.read(|cx| { + assert_eq!( + new_items.len(), + paths_to_open.len(), + "Expect to get the same number of opened items as submitted paths to open" + ); + new_items + .iter() + .zip(paths_to_open.iter()) + .map(|(i, path)| { + match i { + Some(Ok(i)) => { + Some(i.project_path(cx).map(|p| p.path.display().to_string())) + } + Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), + None => None, + } + .flatten() + }) + .collect::>() + }); + opened_paths.sort(); + assert_eq!( + opened_paths, + vec![ + None, + Some(".git/HEAD".to_string()), + Some("excluded_dir/file".to_string()), + ], + "Excluded files should get opened, excluded dir should not get opened" + ); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + assert_eq!( + initial_entries, entries, + "Workspace entries should not change after opening excluded files and directories paths" + ); + + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let mut opened_buffer_paths = pane + .items() + .map(|i| { + i.project_path(cx) + .expect("all excluded files that got open should have a path") + .path + .display() + .to_string() + }) + .collect::>(); + opened_buffer_paths.sort(); + assert_eq!( + opened_buffer_paths, + vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], + "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" + ); + }); + } + + #[gpui::test] + async fn test_save_conflicting_item(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "a.txt": "" })) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + // Open a file within an existing worktree. + window + .update(cx, |view, cx| { + view.open_paths( + vec![PathBuf::from("/root/a.txt")], + OpenVisible::All, + None, + cx, + ) + }) + .unwrap() + .await; + let editor = cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let item = pane.active_item().unwrap(); + item.downcast::().unwrap() + }); + + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.handle_input("x", cx)); + }) + .unwrap(); + + app_state + .fs + .as_fake() + .insert_file("/root/a.txt", "changed".to_string()) + .await; + editor + .condition::(cx, |editor, cx| editor.has_conflict(cx)) + .await; + cx.read(|cx| assert!(editor.is_dirty(cx))); + + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_prompt_answer(0); + save_task.await.unwrap(); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert!(!editor.has_conflict(cx)); + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_open_and_save_new_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(rust_lang())); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap()); + + // Create a new untitled buffer + cx.dispatch_action(window.into(), NewFile); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "untitled"); + assert!(Arc::ptr_eq( + &editor.buffer().read(cx).language_at(0, cx).unwrap(), + &languages::PLAIN_TEXT + )); + editor.handle_input("hi", cx); + assert!(editor.is_dirty(cx)); + }); + }) + .unwrap(); + + // Save the buffer. This prompts for a filename. + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_new_path_selection(|parent_dir| { + assert_eq!(parent_dir, Path::new("/root")); + Some(parent_dir.join("the-new-name.rs")) + }); + cx.read(|cx| { + assert!(editor.is_dirty(cx)); + assert_eq!(editor.read(cx).title(cx), "untitled"); + }); + + // When the save completes, the buffer's title is updated and the language is assigned based + // on the path. + save_task.await.unwrap(); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name.rs"); + assert_eq!( + editor + .buffer() + .read(cx) + .language_at(0, cx) + .unwrap() + .name() + .as_ref(), + "Rust" + ); + }); + }) + .unwrap(); + + // Edit the file and save it again. This time, there is no filename prompt. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + editor.handle_input(" there", cx); + assert!(editor.is_dirty(cx)); + }); + }) + .unwrap(); + + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + save_task.await.unwrap(); + // todo!() po + //assert!(!cx.did_prompt_for_new_path()); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name.rs") + }); + }) + .unwrap(); + + // Open the same newly-created file in another pane item. The new editor should reuse + // the same buffer. + cx.dispatch_action(window.into(), NewFile); + window + .update(cx, |workspace, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + cx, + ); + workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx) + }) + .unwrap() + .await + .unwrap(); + let editor2 = window + .update(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + cx.read(|cx| { + assert_eq!( + editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), + editor.read(cx).buffer().read(cx).as_singleton().unwrap() + ); + }) + } + + #[gpui::test] + async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + + let project = Project::test(app_state.fs.clone(), [], cx).await; + project.update(cx, |project, _| project.languages().add(rust_lang())); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + + // Create a new untitled buffer + cx.dispatch_action(window.into(), NewFile); + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(Arc::ptr_eq( + &editor.buffer().read(cx).language_at(0, cx).unwrap(), + &languages::PLAIN_TEXT + )); + editor.handle_input("hi", cx); + assert!(editor.is_dirty(cx)); + }); + }) + .unwrap(); + + // Save the buffer. This prompts for a filename. + let save_task = window + .update(cx, |workspace, cx| { + workspace.save_active_item(SaveIntent::Save, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); + save_task.await.unwrap(); + // The buffer is not dirty anymore and the language is assigned based on the path. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!( + editor + .buffer() + .read(cx) + .language_at(0, cx) + .unwrap() + .name() + .as_ref(), + "Rust" + ) + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_pane_actions(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1", + "file2": "contents 2", + "file3": "contents 3", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + let file1 = entries[0].clone(); + + let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); + + window + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + + let (editor_1, buffer) = window + .update(cx, |_, cx| { + pane_1.update(cx, |pane_1, cx| { + let editor = pane_1.active_item().unwrap().downcast::().unwrap(); + assert_eq!(editor.project_path(cx), Some(file1.clone())); + let buffer = editor.update(cx, |editor, cx| { + editor.insert("dirt", cx); + editor.buffer().downgrade() + }); + (editor.downgrade(), buffer) + }) + }) + .unwrap(); + + cx.dispatch_action(window.into(), pane::SplitRight); + let editor_2 = cx.update(|cx| { + let pane_2 = workspace.read(cx).active_pane().clone(); + assert_ne!(pane_1, pane_2); + + let pane2_item = pane_2.read(cx).active_item().unwrap(); + assert_eq!(pane2_item.project_path(cx), Some(file1.clone())); + + pane2_item.downcast::().unwrap().downgrade() + }); + cx.dispatch_action( + window.into(), + workspace::CloseActiveItem { save_intent: None }, + ); + + cx.background_executor.run_until_parked(); + window + .read_with(cx, |workspace, _| { + assert_eq!(workspace.panes().len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); + }) + .unwrap(); + + cx.dispatch_action( + window.into(), + workspace::CloseActiveItem { save_intent: None }, + ); + cx.background_executor.run_until_parked(); + cx.simulate_prompt_answer(1); + cx.background_executor.run_until_parked(); + + window + .read_with(cx, |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + assert!(workspace.active_item(cx).is_none()); + }) + .unwrap(); + editor_1.assert_dropped(); + editor_2.assert_dropped(); + buffer.assert_dropped(); + } + + #[gpui::test] + async fn test_navigation(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "contents 1\n".repeat(20), + "file2": "contents 2\n".repeat(20), + "file3": "contents 3\n".repeat(20), + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + + let editor1 = workspace + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_display_ranges( + [DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], + ) + }); + }); + }) + .unwrap(); + + let editor2 = workspace + .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let editor3 = workspace + .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + + workspace + .update(cx, |_, cx| { + editor3.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_display_ranges( + [DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)], + ) + }); + editor.newline(&Default::default(), cx); + editor.newline(&Default::default(), cx); + editor.move_down(&Default::default(), cx); + editor.move_down(&Default::default(), cx); + editor.save(project.clone(), cx) + }) + }) + .unwrap() + .await + .unwrap(); + workspace + .update(cx, |_, cx| { + editor3.update(cx, |editor, cx| { + editor.set_scroll_position(point(0., 12.5), cx) + }); + }) + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(16, 0), 12.5) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file2.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(10, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Go back one more time and ensure we don't navigate past the first item in the history. + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(10, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file2.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Go forward to an item that has been closed, ensuring it gets re-opened at the same + // location. + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + let editor3_id = editor3.entity_id(); + drop(editor3); + pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(16, 0), 12.5) + ); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + let editor2_id = editor2.entity_id(); + drop(editor2); + pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + app_state + .fs + .remove_file(Path::new("/root/a/file2"), Default::default()) + .await + .unwrap(); + cx.background_executor.run_until_parked(); + + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(10, 0), 0.) + ); + workspace + .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file3.clone(), DisplayPoint::new(0, 0), 0.) + ); + + // Modify file to collapse multiple nav history entries into the same location. + // Ensure we don't visit the same location twice when navigating. + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges( + [DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], + ) + }) + }); + }) + .unwrap(); + for _ in 0..5 { + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0) + ]) + }); + }); + }) + .unwrap(); + + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0) + ]) + }) + }); + }) + .unwrap(); + } + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0) + ]) + }); + editor.insert("", cx); + }) + }); + }) + .unwrap(); + + workspace + .update(cx, |_, cx| { + editor1.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }) + }); + }) + .unwrap(); + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(2, 0), 0.) + ); + workspace + .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) + .unwrap() + .await + .unwrap(); + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(3, 0), 0.) + ); + + fn active_location( + workspace: &WindowHandle, + cx: &mut TestAppContext, + ) -> (ProjectPath, DisplayPoint, f32) { + workspace + .update(cx, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + let editor = item.downcast::().unwrap(); + let (selections, scroll_position) = editor.update(cx, |editor, cx| { + ( + editor.selections.display_ranges(cx), + editor.scroll_position(cx), + ) + }); + ( + item.project_path(cx).unwrap(), + selections[0].start, + scroll_position.y, + ) + }) + .unwrap() + } + } + + #[gpui::test] + async fn test_reopening_closed_items(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file1": "", + "file2": "", + "file3": "", + "file4": "", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let file1 = entries[0].clone(); + let file2 = entries[1].clone(); + let file3 = entries[2].clone(); + let file4 = entries[3].clone(); + + let file1_item_id = workspace + .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + let file2_item_id = workspace + .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + let file3_item_id = workspace + .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + let file4_item_id = workspace + .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx)) + .unwrap() + .await + .unwrap() + .item_id(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + // Close all the pane items in some arbitrary order. + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) + }) + }) + .unwrap() + .await + .unwrap(); + + assert_eq!(active_path(&workspace, cx), None); + + // Reopen all the closed items, ensuring they are reopened in the same order + // in which they were closed. + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + // Reopening past the last closed item is a no-op. + workspace + .update(cx, Workspace::reopen_closed_item) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + // Reopening closed items doesn't interfere with navigation history. + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + workspace + .update(cx, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + + fn active_path( + workspace: &WindowHandle, + cx: &TestAppContext, + ) -> Option { + workspace + .read_with(cx, |workspace, cx| { + let item = workspace.active_item(cx)?; + item.project_path(cx) + }) + .unwrap() + } + } + fn init_keymap_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let app_state = AppState::test(cx); + + theme::init(theme::LoadThemes::JustBase, cx); + client::init(&app_state.client, cx); + language::init(cx); + workspace::init(app_state.clone(), cx); + welcome::init(cx); + Project::init_settings(cx); + app_state + }) + } + #[gpui::test] + async fn test_base_keymap(cx: &mut gpui::TestAppContext) { + let executor = cx.executor(); + let app_state = init_keymap_test(cx); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + actions!(test1, [A, B]); + // From the Atom keymap + use workspace::ActivatePreviousPane; + // From the JetBrains keymap + use workspace::ActivatePrevItem; + + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test1::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + executor.run_until_parked(); + cx.update(|cx| { + let settings_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/keymap.json"), + ); + handle_settings_file_changes(settings_rx, cx); + handle_keymap_file_changes(keymap_rx, cx); + }); + workspace + .update(cx, |workspace, _| { + workspace.register_action(|_, _: &A, _cx| {}); + workspace.register_action(|_, _: &B, _cx| {}); + workspace.register_action(|_, _: &ActivatePreviousPane, _cx| {}); + workspace.register_action(|_, _: &ActivatePrevItem, _cx| {}); + }) + .unwrap(); + executor.run_until_parked(); + // Test loading the keymap base at all + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the users keymap, while retaining the base keymap + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test1::B" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &B), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the base, while retaining the users keymap + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &B), ("[", &ActivatePrevItem)], + line!(), + ); + } + + #[gpui::test] + async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { + let executor = cx.executor(); + let app_state = init_keymap_test(cx); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + + actions!(test2, [A, B]); + // From the Atom keymap + use workspace::ActivatePreviousPane; + // From the JetBrains keymap + use pane::ActivatePrevItem; + workspace + .update(cx, |workspace, _| { + workspace + .register_action(|_, _: &A, _| {}) + .register_action(|_, _: &B, _| {}); + }) + .unwrap(); + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "Atom" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": "test2::A" + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.update(|cx| { + let settings_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/settings.json"), + ); + let keymap_rx = watch_config_file( + &executor, + app_state.fs.clone(), + PathBuf::from("/keymap.json"), + ); + + handle_settings_file_changes(settings_rx, cx); + handle_keymap_file_changes(keymap_rx, cx); + }); + + cx.background_executor.run_until_parked(); + + cx.background_executor.run_until_parked(); + // Test loading the keymap base at all + assert_key_bindings_for( + workspace.into(), + cx, + vec![("backspace", &A), ("k", &ActivatePreviousPane)], + line!(), + ); + + // Test disabling the key binding for the base keymap + app_state + .fs + .save( + "/keymap.json".as_ref(), + &r#" + [ + { + "bindings": { + "backspace": null + } + } + ] + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("k", &ActivatePreviousPane)], + line!(), + ); + + // Test modifying the base, while retaining the users keymap + app_state + .fs + .save( + "/settings.json".as_ref(), + &r#" + { + "base_keymap": "JetBrains" + } + "# + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.background_executor.run_until_parked(); + + assert_key_bindings_for( + workspace.into(), + cx, + vec![("[", &ActivatePrevItem)], + line!(), + ); + } + + #[gpui::test] + fn test_bundled_settings_and_themes(cx: &mut AppContext) { + cx.text_system() + .add_fonts(&[ + Assets + .load("fonts/zed-sans/zed-sans-extended.ttf") + .unwrap() + .to_vec() + .into(), + Assets + .load("fonts/zed-mono/zed-mono-extended.ttf") + .unwrap() + .to_vec() + .into(), + Assets + .load("fonts/plex/IBMPlexSans-Regular.ttf") + .unwrap() + .to_vec() + .into(), + ]) + .unwrap(); + let themes = ThemeRegistry::default(); + let mut settings = SettingsStore::default(); + settings + .set_default_settings(&settings::default_settings(), cx) + .unwrap(); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + + let mut has_default_theme = false; + for theme_name in themes.list(false).map(|meta| meta.name) { + let theme = themes.get(&theme_name).unwrap(); + assert_eq!(theme.name, theme_name); + if theme.name == ThemeSettings::get(None, cx).active_theme.name { + has_default_theme = true; + } + } + assert!(has_default_theme); + } + + #[gpui::test] + fn test_bundled_languages(cx: &mut AppContext) { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.background_executor().clone()); + let languages = Arc::new(languages); + let node_runtime = node_runtime::FakeNodeRuntime::new(); + languages::init(languages.clone(), node_runtime, cx); + for name in languages.language_names() { + languages.language_for_name(&name); + } + cx.background_executor().run_until_parked(); + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let mut app_state = AppState::test(cx); + + let state = Arc::get_mut(&mut app_state).unwrap(); + + state.build_window_options = build_window_options; + theme::init(theme::LoadThemes::JustBase, cx); + audio::init((), cx); + channel::init(&app_state.client, app_state.user_store.clone(), cx); + call::init(app_state.client.clone(), app_state.user_store.clone(), cx); + notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + language::init(cx); + editor::init(cx); + project_panel::init_settings(cx); + collab_ui::init(&app_state, cx); + project_panel::init((), cx); + terminal_view::init(cx); + assistant::init(cx); + initialize_workspace(app_state.clone(), cx); + app_state + }) + } + + fn rust_lang() -> Arc { + Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )) + } + #[track_caller] + fn assert_key_bindings_for<'a>( + window: AnyWindowHandle, + cx: &TestAppContext, + actions: Vec<(&'static str, &'a dyn Action)>, + line: u32, + ) { + let available_actions = cx + .update(|cx| window.update(cx, |_, cx| cx.available_actions())) + .unwrap(); + for (key, action) in actions { + let bindings = cx + .update(|cx| window.update(cx, |_, cx| cx.bindings_for_action(action))) + .unwrap(); + // assert that... + assert!( + available_actions.iter().any(|bound_action| { + // actions match... + bound_action.partial_eq(action) + }), + "On {} Failed to find {}", + line, + action.name(), + ); + assert!( + // and key strokes contain the given key + bindings + .into_iter() + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), + "On {} Failed to find {} with key binding {}", + line, + action.name(), + key + ); + } + } +} From 97aed8a4d7583df946743783a4fce42389ff3bf4 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:48:17 +0100 Subject: [PATCH 35/43] Restore ability to reset pane split size by double clicking drag handle. (#3937) Release notes - Fixed double clicking on pane drag handle not resetting pane's split size. - Fixed pane group sizes not being serialized. --- crates/workspace/src/pane_group.rs | 59 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index a7368f61360ce6639dffa26ffd31f2114cd2216b..4428e42830be725fb79979d6acf3e65a777d9386 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -487,6 +487,7 @@ impl PaneAxis { basis, self.flexes.clone(), self.bounding_boxes.clone(), + cx.view().downgrade(), ) .children(self.members.iter().enumerate().map(|(ix, member)| { if member.contains(active_pane) { @@ -575,21 +576,25 @@ mod element { use gpui::{ px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, - Size, Style, WindowContext, + Size, Style, WeakView, WindowContext, }; use parking_lot::Mutex; use smallvec::SmallVec; use ui::prelude::*; + use util::ResultExt; + + use crate::Workspace; use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE}; const DIVIDER_SIZE: f32 = 1.0; - pub fn pane_axis( + pub(super) fn pane_axis( axis: Axis, basis: usize, flexes: Arc>>, bounding_boxes: Arc>>>>, + workspace: WeakView, ) -> PaneAxisElement { PaneAxisElement { axis, @@ -598,6 +603,7 @@ mod element { bounding_boxes, children: SmallVec::new(), active_pane_ix: None, + workspace, } } @@ -608,6 +614,7 @@ mod element { bounding_boxes: Arc>>>>, children: SmallVec<[AnyElement; 2]>, active_pane_ix: Option, + workspace: WeakView, } impl PaneAxisElement { @@ -623,6 +630,7 @@ mod element { axis: Axis, child_start: Point, container_size: Size, + workspace: WeakView, cx: &mut WindowContext, ) { let min_size = match axis { @@ -697,7 +705,9 @@ mod element { } // todo!(schedule serialize) - // workspace.schedule_serialize(cx); + workspace + .update(cx, |this, cx| this.schedule_serialize(cx)) + .log_err(); cx.notify(); } @@ -708,6 +718,7 @@ mod element { ix: usize, pane_bounds: Bounds, axis_bounds: Bounds, + workspace: WeakView, cx: &mut WindowContext, ) { let handle_bounds = Bounds { @@ -742,24 +753,39 @@ mod element { cx.on_mouse_event({ let dragged_handle = dragged_handle.clone(); - move |e: &MouseDownEvent, phase, _cx| { + let flexes = flexes.clone(); + let workspace = workspace.clone(); + move |e: &MouseDownEvent, phase, cx| { if phase.bubble() && handle_bounds.contains(&e.position) { dragged_handle.replace(Some(ix)); + if e.click_count >= 2 { + let mut borrow = flexes.lock(); + *borrow = vec![1.; borrow.len()]; + workspace + .update(cx, |this, cx| this.schedule_serialize(cx)) + .log_err(); + cx.notify(); + } } } }); - cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| { - let dragged_handle = dragged_handle.borrow(); - if phase.bubble() && *dragged_handle == Some(ix) { - Self::compute_resize( - &flexes, - e, - ix, - axis, - pane_bounds.origin, - axis_bounds.size, - cx, - ) + cx.on_mouse_event({ + let workspace = workspace.clone(); + move |e: &MouseMoveEvent, phase, cx| { + let dragged_handle = dragged_handle.borrow(); + + if phase.bubble() && *dragged_handle == Some(ix) { + Self::compute_resize( + &flexes, + e, + ix, + axis, + pane_bounds.origin, + axis_bounds.size, + workspace.clone(), + cx, + ) + } } }); }); @@ -840,6 +866,7 @@ mod element { ix, child_bounds, bounds, + self.workspace.clone(), cx, ); } From 58664206cf34fc9a46c9935e28a63b534635f6a1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Jan 2024 10:51:20 -0500 Subject: [PATCH 36/43] Use info color for assistant role indicator --- crates/assistant/src/assistant_panel.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 6e2ab02cad603e297d1392e89af65dc4fe516970..0a5b30002ed271f0b50b35b65d26745c75d0da61 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2300,9 +2300,7 @@ impl ConversationEditor { let sender = ButtonLike::new("role") .child(match message.role { Role::User => Label::new("You").color(Color::Default), - Role::Assistant => { - Label::new("Assistant").color(Color::Modified) - } + Role::Assistant => Label::new("Assistant").color(Color::Info), Role::System => Label::new("System").color(Color::Warning), }) .tooltip(|cx| { From 42bd9ffa7e1026abce9a6c2009c4ade0b10b603e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Jan 2024 10:52:12 -0500 Subject: [PATCH 37/43] Use filled button style for role indicator in assistant panel This fixes the left side of the button getting clipped on hover. --- crates/assistant/src/assistant_panel.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 0a5b30002ed271f0b50b35b65d26745c75d0da61..f53343531af09083999e04f1d3dce92eafe34850 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2298,6 +2298,7 @@ impl ConversationEditor { move |_cx| { let message_id = message.id; let sender = ButtonLike::new("role") + .style(ButtonStyle::Filled) .child(match message.role { Role::User => Label::new("You").color(Color::Default), Role::Assistant => Label::new("Assistant").color(Color::Info), @@ -2329,10 +2330,7 @@ impl ConversationEditor { .h_11() .relative() .gap_1() - // Sender is a button with a padding of 1, but only has a background on hover, - // so we shift it left by the same amount to align the text with the content - // in the un-hovered state. - .child(div().child(sender).relative().neg_left_1()) + .child(sender) // TODO: Only show this if the message if the message has been sent .child( Label::new( From c40e45e4d74175d444ff18fe131c7ed359d6f107 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 8 Jan 2024 11:34:00 -0500 Subject: [PATCH 38/43] Use default instead of muted color --- crates/diagnostics/src/items.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index da1f77b9afb0f1c4308362f42f479dc30890e77b..a250713e65eb4e13c12a8981b731d6b9233b427a 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -25,11 +25,7 @@ impl Render for DiagnosticIndicator { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { (0, 0) => h_stack().map(|this| { if !self.in_progress_checks.is_empty() { - this.child( - IconElement::new(Icon::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted), - ) + this.child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small)) } else { this.child( IconElement::new(Icon::Check) @@ -74,7 +70,6 @@ impl Render for DiagnosticIndicator { Some( Label::new("Checking…") .size(LabelSize::Small) - .color(Color::Muted) .into_any_element(), ) } else if let Some(diagnostic) = &self.current_diagnostic { From 1ede003de20822d7a1419e125db8e7bc974e8621 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 8 Jan 2024 11:55:51 -0500 Subject: [PATCH 39/43] Always show checking with icon if checks are still running --- assets/icons/arrow_circle.svg | 7 ++++++- crates/diagnostics/src/items.rs | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index 750e349e2b8c73ef0c78b9974ea100f70ae37abe..90e352bdea7a208356139bed8af5bb3c1301b5ce 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index a250713e65eb4e13c12a8981b731d6b9233b427a..0c2d673d8e68b5bd43681788bde078442ed43d9d 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -24,15 +24,11 @@ impl Render for DiagnosticIndicator { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { (0, 0) => h_stack().map(|this| { - if !self.in_progress_checks.is_empty() { - this.child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small)) - } else { - this.child( - IconElement::new(Icon::Check) - .size(IconSize::Small) - .color(Color::Default), - ) - } + this.child( + IconElement::new(Icon::Check) + .size(IconSize::Small) + .color(Color::Default), + ) }), (0, warning_count) => h_stack() .gap_1() @@ -68,8 +64,14 @@ impl Render for DiagnosticIndicator { let status = if !self.in_progress_checks.is_empty() { Some( - Label::new("Checking…") - .size(LabelSize::Small) + h_stack() + .gap_2() + .child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small)) + .child( + Label::new("Checking…") + .size(LabelSize::Small) + .into_any_element(), + ) .into_any_element(), ) } else if let Some(diagnostic) = &self.current_diagnostic { From 04f01ab40821da8bdb1642aa0fc7c44ec3600cdd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Jan 2024 12:01:13 -0500 Subject: [PATCH 40/43] Overdraw the tree branch to avoid gaps --- crates/collab_ui/src/collab_panel.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ee43b32f106e8228806a1ca6108b05b00038ca22..ff87bb8b66ca4b04516026c496a726765c9b8d80 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -896,7 +896,7 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(is_last, cx)) + .child(render_tree_branch(is_last, false, cx)) .child(IconButton::new(0, Icon::Folder)), ) .child(Label::new(project_name.clone())) @@ -917,7 +917,7 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(is_last, cx)) + .child(render_tree_branch(is_last, false, cx)) .child(IconButton::new(0, Icon::Screen)), ) .child(Label::new("Screen")) @@ -958,7 +958,7 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(false, cx)) + .child(render_tree_branch(false, true, cx)) .child(IconButton::new(0, Icon::File)), ) .child(div().h_7().w_full().child(Label::new("notes"))) @@ -979,7 +979,7 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(false, cx)) + .child(render_tree_branch(false, false, cx)) .child(IconButton::new(0, Icon::MessageBubbles)), ) .child(Label::new("chat")) @@ -1007,7 +1007,7 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(!has_visible_participants, cx)) + .child(render_tree_branch(!has_visible_participants, false, cx)) .child(""), ) .child(Label::new(if count == 1 { @@ -2404,7 +2404,7 @@ impl CollabPanel { } } -fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement { +fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement { let rem_size = cx.rem_size(); let line_height = cx.text_style().line_height_in_pixels(rem_size); let width = rem_size * 1.5; @@ -2422,7 +2422,11 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement point(start_x, top), point( start_x + thickness, - if is_last { start_y } else { bounds.bottom() }, + if is_last { + start_y + } else { + bounds.bottom() + if overdraw { px(1.) } else { px(0.) } + }, ), ), color, From d3c9626169f222cf7e2ea650c92c955a48ef473f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:29:14 +0100 Subject: [PATCH 41/43] Comment out test_open_paths_action pending investigation (#3939) Commenting this one out temporarily to not break CI for folks while I look into it. Release Notes: - N/A --- crates/zed/src/zed.rs | 208 +++++++++++++++++++++--------------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 702c815d34600b8502e55f83f60b17c572dc869e..61b8d6eaf84332474ff992e5a72a85cbde5b4776 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -791,110 +791,110 @@ mod tests { WorkspaceHandle, }; - #[gpui::test] - async fn test_open_paths_action(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": { - "aa": null, - "ab": null, - }, - "b": { - "ba": null, - "bb": null, - }, - "c": { - "ca": null, - "cb": null, - }, - "d": { - "da": null, - "db": null, - }, - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], - &app_state, - None, - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let workspace_1 = cx - .read(|cx| cx.windows()[0].downcast::()) - .unwrap(); - workspace_1 - .update(cx, |workspace, cx| { - assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_dock().read(cx).is_open()); - assert!(workspace - .active_pane() - .read(cx) - .focus_handle(cx) - .is_focused(cx)); - }) - .unwrap(); - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], - &app_state, - None, - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 2); - - // Replace existing windows - let window = cx - .update(|cx| cx.windows()[0].downcast::()) - .unwrap(); - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], - &app_state, - Some(window), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 2); - let workspace_1 = cx - .update(|cx| cx.windows()[0].downcast::()) - .unwrap(); - workspace_1 - .update(cx, |workspace, cx| { - assert_eq!( - workspace - .worktrees(cx) - .map(|w| w.read(cx).abs_path()) - .collect::>(), - &[Path::new("/root/c").into(), Path::new("/root/d").into()] - ); - assert!(workspace.left_dock().read(cx).is_open()); - assert!(workspace.active_pane().focus_handle(cx).is_focused(cx)); - }) - .unwrap(); - } + // #[gpui::test] + // async fn test_open_paths_action(cx: &mut TestAppContext) { + // let app_state = init_test(cx); + // app_state + // .fs + // .as_fake() + // .insert_tree( + // "/root", + // json!({ + // "a": { + // "aa": null, + // "ab": null, + // }, + // "b": { + // "ba": null, + // "bb": null, + // }, + // "c": { + // "ca": null, + // "cb": null, + // }, + // "d": { + // "da": null, + // "db": null, + // }, + // }), + // ) + // .await; + + // cx.update(|cx| { + // open_paths( + // &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], + // &app_state, + // None, + // cx, + // ) + // }) + // .await + // .unwrap(); + // assert_eq!(cx.read(|cx| cx.windows().len()), 1); + + // cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + // .await + // .unwrap(); + // assert_eq!(cx.read(|cx| cx.windows().len()), 1); + // let workspace_1 = cx + // .read(|cx| cx.windows()[0].downcast::()) + // .unwrap(); + // workspace_1 + // .update(cx, |workspace, cx| { + // assert_eq!(workspace.worktrees(cx).count(), 2); + // assert!(workspace.left_dock().read(cx).is_open()); + // assert!(workspace + // .active_pane() + // .read(cx) + // .focus_handle(cx) + // .is_focused(cx)); + // }) + // .unwrap(); + + // cx.update(|cx| { + // open_paths( + // &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], + // &app_state, + // None, + // cx, + // ) + // }) + // .await + // .unwrap(); + // assert_eq!(cx.read(|cx| cx.windows().len()), 2); + + // // Replace existing windows + // let window = cx + // .update(|cx| cx.windows()[0].downcast::()) + // .unwrap(); + // cx.update(|cx| { + // open_paths( + // &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], + // &app_state, + // Some(window), + // cx, + // ) + // }) + // .await + // .unwrap(); + // assert_eq!(cx.read(|cx| cx.windows().len()), 2); + // let workspace_1 = cx + // .update(|cx| cx.windows()[0].downcast::()) + // .unwrap(); + // workspace_1 + // .update(cx, |workspace, cx| { + // assert_eq!( + // workspace + // .worktrees(cx) + // .map(|w| w.read(cx).abs_path()) + // .collect::>(), + // &[Path::new("/root/c").into(), Path::new("/root/d").into()] + // ); + // assert!(workspace.left_dock().read(cx).is_open()); + // assert!(workspace.active_pane().focus_handle(cx).is_focused(cx)); + // }) + // .unwrap(); + // } #[gpui::test] async fn test_window_edit_state(cx: &mut TestAppContext) { From fd2abb7ba132031be9c32a2415e225edd8b7fc68 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Jan 2024 12:31:53 -0500 Subject: [PATCH 42/43] Adjust thickness of tree branches --- crates/collab_ui/src/collab_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ff87bb8b66ca4b04516026c496a726765c9b8d80..ac0925e7b0b6230ab2688d421af77ae5333f3d56 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2408,7 +2408,7 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> let rem_size = cx.rem_size(); let line_height = cx.text_style().line_height_in_pixels(rem_size); let width = rem_size * 1.5; - let thickness = px(2.); + let thickness = px(1.); let color = cx.theme().colors().text; canvas(move |bounds, cx| { From 46a99feb972a51f942147a84517cc663871915b7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 8 Jan 2024 12:58:04 -0500 Subject: [PATCH 43/43] Use correct color for folded diff indicator (#3942) This PR updates the indicator for changes within a fold to use the correct color from the theme: Screenshot 2024-01-08 at 12 52 56 PM Release Notes: - Updated the color of the indicator for a fold containing modified lines. --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e96cb5df0eab53d4bf691c4d13a3e20928856ba5..53a376c2842937a6029cfe6c848b9998153a8d77 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -795,7 +795,7 @@ impl EditorElement { cx.paint_quad(quad( highlight_bounds, Corners::all(1. * line_height), - gpui::yellow(), // todo!("use the right color") + cx.theme().status().modified, Edges::default(), transparent_black(), ));