diff --git a/Cargo.lock b/Cargo.lock index 4443526775..cd36221de0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,7 +1082,6 @@ dependencies = [ "anyhow", "async-broadcast", "audio", - "channel", "client", "collections", "fs", @@ -1467,7 +1466,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.23.1" +version = "0.23.3" dependencies = [ "anyhow", "async-trait", @@ -2079,9 +2078,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.66+curl-8.3.0" +version = "0.4.67+curl-8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" +checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34" dependencies = [ "cc", "libc", @@ -2832,7 +2831,6 @@ dependencies = [ "parking_lot 0.11.2", "regex", "rope", - "rpc", "serde", "serde_derive", "serde_json", @@ -9665,6 +9663,7 @@ dependencies = [ "theme", "theme_selector", "util", + "vim", "workspace", ] @@ -9971,7 +9970,6 @@ dependencies = [ "async-recursion 1.0.5", "bincode", "call", - "channel", "client", "collections", "context_menu", diff --git a/Dockerfile b/Dockerfile index 208700f7fb..f3d0b601b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.72-bullseye as builder +FROM rust:1.73-bullseye as builder WORKDIR app COPY . . diff --git a/README.md b/README.md index b3d4987526..eed8dd4d91 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,7 @@ foreman start If you want to run Zed pointed at the local servers, you can run: ``` -script/zed-with-local-servers -# or... -script/zed-with-local-servers --release +script/zed-local ``` ### Dump element JSON diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index b4e94fe56c..eb448d8d8d 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,7 +20,6 @@ test-support = [ [dependencies] audio = { path = "../audio" } -channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index d86ed1be37..0846341325 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,7 +5,6 @@ pub mod room; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use channel::ChannelId; use client::{ proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, @@ -79,7 +78,7 @@ impl ActiveCall { } } - pub fn channel_id(&self, cx: &AppContext) -> Option { + pub fn channel_id(&self, cx: &AppContext) -> Option { self.room()?.read(cx).channel_id() } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 32b8232b4f..4e52f57f60 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -606,35 +606,39 @@ impl Room { /// Returns the most 'active' projects, defined as most people in the project pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { - let mut projects = HashMap::default(); - let mut hosts = HashMap::default(); - + let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); for participant in self.remote_participants.values() { match participant.location { ParticipantLocation::SharedProject { project_id } => { - *projects.entry(project_id).or_insert(0) += 1; + project_hosts_and_guest_counts + .entry(project_id) + .or_default() + .1 += 1; } ParticipantLocation::External | ParticipantLocation::UnsharedProject => {} } for project in &participant.projects { - *projects.entry(project.id).or_insert(0) += 1; - hosts.insert(project.id, participant.user.id); + project_hosts_and_guest_counts + .entry(project.id) + .or_default() + .0 = Some(participant.user.id); } } if let Some(user) = self.user_store.read(cx).current_user() { for project in &self.local_participant.projects { - *projects.entry(project.id).or_insert(0) += 1; - hosts.insert(project.id, user.id); + project_hosts_and_guest_counts + .entry(project.id) + .or_default() + .0 = Some(user.id); } } - let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); - pairs.sort_by_key(|(_, count)| *count as i32); - - pairs - .iter() - .find_map(|(project_id, _)| hosts.get(project_id).map(|host| (*project_id, *host))) + project_hosts_and_guest_counts + .into_iter() + .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count))) + .max_by_key(|(_, _, guest_count)| *guest_count) + .map(|(id, host, _)| (id, host)) } async fn handle_room_updated( @@ -700,6 +704,7 @@ impl Room { let Some(peer_id) = participant.peer_id else { continue; }; + let participant_index = ParticipantIndex(participant.participant_index); this.participant_user_ids.insert(participant.user_id); let old_projects = this @@ -750,8 +755,9 @@ impl Room { if let Some(remote_participant) = this.remote_participants.get_mut(&participant.user_id) { - remote_participant.projects = participant.projects; remote_participant.peer_id = peer_id; + remote_participant.projects = participant.projects; + remote_participant.participant_index = participant_index; if location != remote_participant.location { remote_participant.location = location; cx.emit(Event::ParticipantLocationChanged { @@ -763,9 +769,7 @@ impl Room { participant.user_id, RemoteParticipant { user: user.clone(), - participant_index: ParticipantIndex( - participant.participant_index, - ), + participant_index, peer_id, projects: participant.projects, location, diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 160b8441ff..d31d4b3c8c 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -2,19 +2,21 @@ mod channel_buffer; mod channel_chat; mod channel_store; +use client::{Client, UserStore}; +use gpui::{AppContext, ModelHandle}; +use std::sync::Arc; + pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; pub use channel_store::{ Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, }; -use client::Client; -use std::sync::Arc; - #[cfg(test)] mod channel_store_tests; -pub fn init(client: &Arc) { +pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { + channel_store::init(client, user_store, cx); channel_buffer::init(client); channel_chat::init(client); } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index cd2153ad5c..2a2fa454f2 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -2,6 +2,7 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; use anyhow::{anyhow, Result}; +use channel_index::ChannelIndex; use client::{Client, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use db::RELEASE_CHANNEL; @@ -15,7 +16,11 @@ use serde_derive::{Deserialize, Serialize}; use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration}; use util::ResultExt; -use self::channel_index::ChannelIndex; +pub fn init(client: &Arc, user_store: ModelHandle, cx: &mut AppContext) { + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + cx.set_global(channel_store); +} pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -92,6 +97,10 @@ enum OpenedModelHandle { } impl ChannelStore { + pub fn global(cx: &AppContext) -> ModelHandle { + cx.global::>().clone() + } + pub fn new( client: Arc, user_store: ModelHandle, diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 41acafa3a3..9303a52092 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle { cx.foreground().forbid_parking(); cx.set_global(SettingsStore::test(cx)); - crate::init(&client); client::init(&client, cx); + crate::init(&client, user_store, cx); - cx.add_model(|cx| ChannelStore::new(client, user_store, cx)) + ChannelStore::global(cx) } fn update_channels( diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5767ac54b7..9f63d0e2be 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); -actions!(client, [SignIn, SignOut]); +actions!(client, [SignIn, SignOut, Reconnect]); pub fn init_settings(cx: &mut AppContext) { settings::register::(cx); @@ -102,6 +102,17 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } } }); + cx.add_global_action({ + let client = client.clone(); + move |_: &Reconnect, cx| { + if let Some(client) = client.upgrade() { + cx.spawn(|cx| async move { + client.reconnect(&cx); + }) + .detach(); + } + } + }); } pub struct Client { @@ -1212,6 +1223,11 @@ impl Client { self.set_status(Status::SignedOut, cx); } + pub fn reconnect(self: &Arc, cx: &AsyncAppContext) { + self.peer.teardown(); + self.set_status(Status::ConnectionLost, cx); + } + fn connection_id(&self) -> Result { if let Status::Connected { connection_id, .. } = *self.status().borrow() { Ok(connection_id) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 0f753679e1..70878bf2e4 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -8,7 +8,6 @@ use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; -use uuid::Uuid; pub struct Telemetry { http_client: Arc, @@ -20,7 +19,7 @@ pub struct Telemetry { struct TelemetryState { metrics_id: Option>, // Per logged-in user installation_id: Option>, // Per app installation (different for dev, preview, and stable) - session_id: String, // Per app launch + session_id: Option>, // Per app launch app_version: Option>, release_channel: Option<&'static str>, os_name: &'static str, @@ -43,7 +42,7 @@ lazy_static! { struct ClickhouseEventRequestBody { token: &'static str, installation_id: Option>, - session_id: String, + session_id: Option>, is_staff: Option, app_version: Option>, os_name: &'static str, @@ -134,7 +133,7 @@ impl Telemetry { release_channel, installation_id: None, metrics_id: None, - session_id: Uuid::new_v4().to_string(), + session_id: None, clickhouse_events_queue: Default::default(), flush_clickhouse_events_task: Default::default(), log_file: None, @@ -149,9 +148,15 @@ impl Telemetry { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } - pub fn start(self: &Arc, installation_id: Option, cx: &mut AppContext) { + pub fn start( + self: &Arc, + installation_id: Option, + session_id: String, + cx: &mut AppContext, + ) { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); + state.session_id = Some(session_id.into()); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); @@ -283,23 +288,21 @@ impl Telemetry { { let state = this.state.lock(); - json_bytes.clear(); - serde_json::to_writer( - &mut json_bytes, - &ClickhouseEventRequestBody { - token: ZED_SECRET_CLIENT_TOKEN, - installation_id: state.installation_id.clone(), - session_id: state.session_id.clone(), - is_staff: state.is_staff.clone(), - app_version: state.app_version.clone(), - os_name: state.os_name, - os_version: state.os_version.clone(), - architecture: state.architecture, + let request_body = ClickhouseEventRequestBody { + token: ZED_SECRET_CLIENT_TOKEN, + installation_id: state.installation_id.clone(), + session_id: state.session_id.clone(), + is_staff: state.is_staff.clone(), + app_version: state.app_version.clone(), + os_name: state.os_name, + os_version: state.os_version.clone(), + architecture: state.architecture, - release_channel: state.release_channel, - events, - }, - )?; + release_channel: state.release_channel, + events, + }; + json_bytes.clear(); + serde_json::to_writer(&mut json_bytes, &request_body)?; } this.http_client diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 0ede6483bc..6177c23620 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.23.1" +version = "0.23.3" publish = false [[bin]] diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 83b5382cf5..a48d425d90 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -89,7 +89,7 @@ impl Database { let mut rows = channel_message::Entity::find() .filter(condition) - .order_by_asc(channel_message::Column::Id) + .order_by_desc(channel_message::Column::Id) .limit(count as u64) .stream(&*tx) .await?; @@ -110,6 +110,7 @@ impl Database { }); } drop(rows); + messages.reverse(); Ok(messages) }) .await diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 4966ef1bda..464aaba207 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -1,10 +1,75 @@ use crate::{ - db::{Database, NewUserParams}, + db::{Database, MessageId, NewUserParams}, test_both_dbs, }; use std::sync::Arc; use time::OffsetDateTime; +test_both_dbs!( + test_channel_message_retrieval, + test_channel_message_retrieval_postgres, + test_channel_message_retrieval_sqlite +); + +async fn test_channel_message_retrieval(db: &Arc) { + let user = db + .create_user( + "user@example.com", + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let channel = db + .create_channel("channel", None, "room", user) + .await + .unwrap(); + + let owner_id = db.create_server("test").await.unwrap().0 as u32; + db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user) + .await + .unwrap(); + + let mut all_messages = Vec::new(); + for i in 0..10 { + all_messages.push( + db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i) + .await + .unwrap() + .0 + .to_proto(), + ); + } + + let messages = db + .get_channel_messages(channel, user, 3, None) + .await + .unwrap() + .into_iter() + .map(|message| message.id) + .collect::>(); + assert_eq!(messages, &all_messages[7..10]); + + let messages = db + .get_channel_messages( + channel, + user, + 4, + Some(MessageId::from_proto(all_messages[6])), + ) + .await + .unwrap() + .into_iter() + .map(|message| message.id) + .collect::>(); + assert_eq!(messages, &all_messages[2..6]); +} + test_both_dbs!( test_channel_message_nonces, test_channel_message_nonces_postgres, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 70052468d4..268228077f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1917,13 +1917,10 @@ async fn follow( .check_room_participants(room_id, leader_id, session.connection_id) .await?; - let mut response_payload = session + let response_payload = session .peer .forward_request(session.connection_id, leader_id, request) .await?; - response_payload - .views - .retain(|view| view.leader_id != Some(follower_id.into())); response.send(response_payload)?; if let Some(project_id) = project_id { @@ -1984,14 +1981,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> .await? }; - let leader_id = request.variant.as_ref().and_then(|variant| match variant { - proto::update_followers::Variant::CreateView(payload) => payload.leader_id, + // For now, don't send view update messages back to that view's current leader. + let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant { proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, - proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id, + _ => None, }); + for follower_peer_id in request.follower_ids.iter().copied() { let follower_connection_id = follower_peer_id.into(); - if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) { + if Some(follower_peer_id) != connection_id_to_omit + && connection_ids.contains(&follower_connection_id) + { session.peer.forward_send( session.connection_id, follower_connection_id, diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 6d374b7920..f3857e3db3 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; +use rpc::proto::PeerId; use serde_json::json; use std::{borrow::Cow, sync::Arc}; use workspace::{ @@ -183,20 +184,12 @@ async fn test_basic_following( // All clients see that clients B and C are following client A. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); + 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. @@ -206,46 +199,39 @@ async fn test_basic_following( // All clients see that clients B is following client A. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); + 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); - }); + 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.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); + 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 C. + // 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() @@ -255,20 +241,15 @@ async fn test_basic_following( // All clients see that D is following C cx_d.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[peer_id_d], - "checking followers for C as {name}" - ); - }); + 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. @@ -277,32 +258,12 @@ async fn test_basic_following( // Clients A and B see that client B is following A, and client C is not present in the followers. cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); - } - - // All clients see that no-one is following C - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[], - "checking followers for C as {name}" - ); - }); + 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. @@ -724,10 +685,9 @@ async fn test_peers_following_each_other( .await .unwrap(); - // Client A opens some editors. + // Client A opens a file. let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let _editor_a1 = workspace_a + workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), None, true, cx) }) @@ -736,10 +696,9 @@ async fn test_peers_following_each_other( .downcast::() .unwrap(); - // Client B opens an editor. + // Client B opens a different file. let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let _editor_b1 = workspace_b + workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), None, true, cx) }) @@ -754,9 +713,7 @@ async fn test_peers_following_each_other( }); workspace_a .update(cx_a, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_a1); - let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(leader_id, cx).unwrap() + workspace.follow(client_b.peer_id().unwrap(), cx).unwrap() }) .await .unwrap(); @@ -765,85 +722,443 @@ async fn test_peers_following_each_other( }); workspace_b .update(cx_b, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_b1); - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(leader_id, cx).unwrap() + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() }) .await .unwrap(); - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_next_pane(cx); - }); - // Wait for focus effects to be fully flushed - workspace_a.update(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); + // 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)); + deterministic.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.activate_next_pane(cx); - }); workspace_b .update(cx_b, |workspace, cx| { - assert_eq!(*workspace.active_pane(), pane_b1); workspace.open_path((worktree_id, "4.txt"), None, true, cx) }) .await .unwrap(); - cx_a.foreground().run_until_parked(); + deterministic.run_until_parked(); - // Ensure leader updates don't change the active pane of followers - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_b1); - }); - - // Ensure peers following each other doesn't cause an infinite loop. + // Both client's see the other client open the new file, but keep their + // focus on their own active pane. assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .project_path(cx)), - Some((worktree_id, "3.txt").into()) + 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()) + ] + }, + ] ); - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - workspace.activate_next_pane(cx); + 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 A focuses their right pane, in which they're following client B. + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.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()) + ] + }, + ] + ); + + // 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)); + deterministic.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 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); + }); }); + deterministic.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 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(); + }); + }); + + deterministic.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 again. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // 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); + }); + }); + deterministic.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())] + }, + ] + ); workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); }); + deterministic.run_until_parked(); - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - workspace.activate_next_pane(cx); - }); + 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_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); }); + deterministic.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![ + (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()), + ] + }, + ] + ); } #[gpui::test(iterations = 10)] @@ -1074,24 +1389,6 @@ async fn test_peers_simultaneously_following_each_other( }); } -fn visible_push_notifications( - cx: &mut TestAppContext, -) -> Vec> { - let mut ret = Vec::new(); - for window in cx.windows() { - window.read_with(cx, |window| { - if let Some(handle) = window - .root_view() - .clone() - .downcast::() - { - ret.push(handle) - } - }); - } - ret -} - #[gpui::test(iterations = 10)] async fn test_following_across_workspaces( deterministic: Arc, @@ -1304,3 +1601,83 @@ async fn test_following_across_workspaces( assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); }); } + +fn visible_push_notifications( + cx: &mut TestAppContext, +) -> Vec> { + let mut ret = Vec::new(); + for window in cx.windows() { + window.read_with(cx, |window| { + if let Some(handle) = window + .root_view() + .clone() + .downcast::() + { + ret.push(handle) + } + }); + } + ret +} + +#[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| { + 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: &ViewHandle, cx: &mut TestAppContext) -> Vec { + workspace.read_with(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/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e10ded7d95..2e13874125 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -44,6 +44,7 @@ pub struct TestServer { pub struct TestClient { pub username: String, pub app_state: Arc, + channel_store: ModelHandle, state: RefCell, } @@ -206,15 +207,12 @@ impl TestServer { let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let mut language_registry = LanguageRegistry::test(); language_registry.set_executor(cx.background()); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), workspace_store, - channel_store: channel_store.clone(), languages: Arc::new(language_registry), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), @@ -231,7 +229,7 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); - channel::init(&client); + channel::init(&client, user_store, cx); }); client @@ -242,6 +240,7 @@ impl TestServer { let client = TestClient { app_state, username: name.to_string(), + channel_store: cx.read(ChannelStore::global).clone(), state: Default::default(), }; client.wait_for_current_user(cx).await; @@ -310,10 +309,9 @@ impl TestServer { admin: (&TestClient, &mut TestAppContext), members: &mut [(&TestClient, &mut TestAppContext)], ) -> u64 { - let (admin_client, admin_cx) = admin; - let channel_id = admin_client - .app_state - .channel_store + let (_, admin_cx) = admin; + let channel_id = admin_cx + .read(ChannelStore::global) .update(admin_cx, |channel_store, cx| { channel_store.create_channel(channel, parent, cx) }) @@ -321,9 +319,8 @@ impl TestServer { .unwrap(); for (member_client, member_cx) in members { - admin_client - .app_state - .channel_store + admin_cx + .read(ChannelStore::global) .update(admin_cx, |channel_store, cx| { channel_store.invite_member( channel_id, @@ -337,9 +334,8 @@ impl TestServer { admin_cx.foreground().run_until_parked(); - member_client - .app_state - .channel_store + member_cx + .read(ChannelStore::global) .update(*member_cx, |channels, _| { channels.respond_to_channel_invite(channel_id, true) }) @@ -447,7 +443,7 @@ impl TestClient { } pub fn channel_store(&self) -> &ModelHandle { - &self.app_state.channel_store + &self.channel_store } pub fn user_store(&self) -> &ModelHandle { @@ -614,8 +610,8 @@ impl TestClient { ) { let (other_client, other_cx) = user; - self.app_state - .channel_store + cx_self + .read(ChannelStore::global) .update(cx_self, |channel_store, cx| { channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx) }) @@ -624,11 +620,10 @@ impl TestClient { cx_self.foreground().run_until_parked(); - other_client - .app_state - .channel_store - .update(other_cx, |channels, _| { - channels.respond_to_channel_invite(channel, true) + other_cx + .read(ChannelStore::global) + .update(other_cx, |channel_store, _| { + channel_store.respond_to_channel_invite(channel, true) }) .await .unwrap(); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a955768050..b2e65eb2fa 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -73,7 +73,7 @@ impl ChannelView { ) -> Task>> { let workspace = workspace.read(cx); let project = workspace.project().to_owned(); - let channel_store = workspace.app_state().channel_store.clone(); + let channel_store = ChannelStore::global(cx); let markdown = workspace .app_state() .languages diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index b446521c5a..1a17b48f19 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -81,7 +81,7 @@ impl ChatPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); - let channel_store = workspace.app_state().channel_store.clone(); + let channel_store = ChannelStore::global(cx); let languages = workspace.app_state().languages.clone(); let input_editor = cx.add_view(|cx| { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8a8fde88ee..30505b0876 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -655,7 +655,7 @@ impl CollabPanel { channel_editing_state: None, selection: None, user_store: workspace.user_store().clone(), - channel_store: workspace.app_state().channel_store.clone(), + channel_store: ChannelStore::global(cx), project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6e587d8c98..222a9c650a 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -107,13 +107,23 @@ fn matching_history_item_paths( ) -> HashMap, PathMatch> { let history_items_by_worktrees = history_items .iter() - .map(|found_path| { - let path = &found_path.project.path; + .filter_map(|found_path| { let candidate = PathMatchCandidate { - path, - char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()), + path: &found_path.project.path, + // Only match history items names, otherwise their paths may match too many queries, producing false positives. + // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item, + // it would be shown first always, despite the latter being a better match. + char_bag: CharBag::from_iter( + found_path + .project + .path + .file_name()? + .to_string_lossy() + .to_lowercase() + .chars(), + ), }; - (found_path.project.worktree_id, candidate) + Some((found_path.project.worktree_id, candidate)) }) .fold( HashMap::default(), @@ -1803,6 +1813,113 @@ mod tests { }); } + #[gpui::test] + async fn test_history_items_vs_very_good_external_match( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "collab_ui": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "collab_ui.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + // generate some history to select from + open_close_queried_buffer( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "thi", + 1, + "third.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + open_close_queried_buffer( + "sec", + 1, + "second.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "collab_ui"; + let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder + .update(cx, |finder, cx| { + finder.delegate_mut().update_matches(query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + let delegate = finder.delegate(); + assert!( + delegate.matches.history.is_empty(), + "History items should not math query {query}, they should be matched by name only" + ); + + let search_entries = delegate + .matches + .search + .iter() + .map(|e| e.path.to_path_buf()) + .collect::>(); + assert_eq!( + search_entries.len(), + 4, + "All history and the new file should be found after query {query} as search results" + ); + assert_eq!( + search_entries, + vec![ + PathBuf::from("collab_ui/collab_ui.rs"), + PathBuf::from("collab_ui/third.rs"), + PathBuf::from("collab_ui/first.rs"), + PathBuf::from("collab_ui/second.rs"), + ], + "Despite all search results having the same directory name, the most matching one should be on top" + ); + }); + } + async fn open_close_queried_buffer( input: &str, expected_matches: usize, diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 441ce6f9c7..11a34bcecb 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -13,7 +13,6 @@ rope = { path = "../rope" } text = { path = "../text" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } -rpc = { path = "../rpc" } anyhow.workspace = true async-trait.workspace = true diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 2b2aebe679..4637a7f754 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -2,7 +2,6 @@ use anyhow::Result; use collections::HashMap; use git2::{BranchType, StatusShow}; use parking_lot::Mutex; -use rpc::proto; use serde_derive::{Deserialize, Serialize}; use std::{ cmp::Ordering, @@ -23,6 +22,7 @@ pub struct Branch { /// Timestamp of most recent commit, normalized to Unix Epoch format. pub unix_timestamp: Option, } + #[async_trait::async_trait] pub trait GitRepository: Send { fn reload_index(&self); @@ -358,24 +358,6 @@ impl GitFileStatus { } } } - - pub fn from_proto(git_status: Option) -> Option { - git_status.and_then(|status| { - proto::GitStatus::from_i32(status).map(|status| match status { - proto::GitStatus::Added => GitFileStatus::Added, - proto::GitStatus::Modified => GitFileStatus::Modified, - proto::GitStatus::Conflict => GitFileStatus::Conflict, - }) - }) - } - - pub fn to_proto(self) -> i32 { - match self { - GitFileStatus::Added => proto::GitStatus::Added as i32, - GitFileStatus::Modified => proto::GitStatus::Modified as i32, - GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, - } - } } #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)] diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index dafafe40a0..e808a4886f 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -441,7 +441,7 @@ mod tests { score, worktree_id: 0, positions: Vec::new(), - path: candidate.path.clone(), + path: Arc::from(candidate.path), path_prefix: "".into(), distance_to_relative_ancestor: usize::MAX, }, diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index 4eb31936a8..d8fae471e1 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -14,7 +14,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct PathMatchCandidate<'a> { - pub path: &'a Arc, + pub path: &'a Path, pub char_bag: CharBag, } @@ -120,7 +120,7 @@ pub fn match_fixed_path_set( score, worktree_id, positions: Vec::new(), - path: candidate.path.clone(), + path: Arc::from(candidate.path), path_prefix: Arc::from(""), distance_to_relative_ancestor: usize::MAX, }, @@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>( score, worktree_id, positions: Vec::new(), - path: candidate.path.clone(), + path: Arc::from(candidate.path), path_prefix: candidate_set.prefix(), distance_to_relative_ancestor: relative_to.as_ref().map_or( usize::MAX, diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 4eca6f3a30..7ba1e85100 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -71,7 +71,7 @@ pub struct Window { pub(crate) hovered_region_ids: Vec, pub(crate) clicked_region_ids: Vec, pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>, - text_layout_cache: TextLayoutCache, + text_layout_cache: Arc, refreshing: bool, } @@ -107,7 +107,7 @@ impl Window { cursor_regions: Default::default(), mouse_regions: Default::default(), event_handlers: Default::default(), - text_layout_cache: TextLayoutCache::new(cx.font_system.clone()), + text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())), last_mouse_moved_event: None, last_mouse_position: Vector2F::zero(), pressed_buttons: Default::default(), @@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> { self.window.refreshing } - pub fn text_layout_cache(&self) -> &TextLayoutCache { + pub fn text_layout_cache(&self) -> &Arc { &self.window.text_layout_cache } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 6f89375df0..323b3d9f89 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::Result; use gpui::{ geometry::{vector::Vector2F, Size}, - text_layout::LineLayout, + text_layout::Line, LayoutId, }; use parking_lot::Mutex; @@ -32,7 +32,7 @@ impl Element for Text { _view: &mut V, cx: &mut ViewContext, ) -> Result<(LayoutId, Self::PaintState)> { - let fonts = cx.platform().fonts(); + let layout_cache = cx.text_layout_cache().clone(); let text_style = cx.text_style(); let line_height = cx.font_cache().line_height(text_style.font_size); let text = self.text.clone(); @@ -41,14 +41,14 @@ impl Element for Text { let layout_id = cx.add_measured_layout_node(Default::default(), { let paint_state = paint_state.clone(); move |_params| { - let line_layout = fonts.layout_line( + let line_layout = layout_cache.layout_str( text.as_ref(), text_style.font_size, &[(text.len(), text_style.to_run())], ); let size = Size { - width: line_layout.width, + width: line_layout.width(), height: line_height, }; @@ -85,13 +85,9 @@ impl Element for Text { line_height = paint_state.line_height; } - let text_style = cx.text_style(); - let line = - gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]); - // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder. let visible_bounds = bounds; - line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx); + line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx); } } @@ -104,6 +100,6 @@ impl IntoElement for Text { } pub struct TextLayout { - line_layout: Arc, + line_layout: Arc, line_height: f32, } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4771fc7083..cf468020ce 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -22,7 +22,6 @@ test-support = [ ] [dependencies] -client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 3bedf5b7a8..b7e9001c6a 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1427,7 +1427,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex // Insert the block at column zero. The entire block is indented // so that the first line matches the previous line's indentation. buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { original_indent_columns: original_indent_columns.clone(), }), diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2de3671033..a38e43cd87 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry { is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, is_external: entry.is_external, - git_status: entry.git_status.map(|status| status.to_proto()), + git_status: entry.git_status.map(git_status_to_proto), } } } @@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, is_external: entry.is_external, - git_status: GitFileStatus::from_proto(entry.git_status), + git_status: git_status_from_proto(entry.git_status), }) } else { Err(anyhow!( @@ -4366,3 +4366,21 @@ fn combine_git_statuses( unstaged } } + +fn git_status_from_proto(git_status: Option) -> Option { + git_status.and_then(|status| { + proto::GitStatus::from_i32(status).map(|status| match status { + proto::GitStatus::Added => GitFileStatus::Added, + proto::GitStatus::Modified => GitFileStatus::Modified, + proto::GitStatus::Conflict => GitFileStatus::Conflict, + }) + }) +} + +fn git_status_to_proto(status: GitFileStatus) -> i32 { + match status { + GitFileStatus::Added => proto::GitStatus::Added as i32, + GitFileStatus::Modified => proto::GitStatus::Modified as i32, + GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, + } +} diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs index 345fcfa309..85d5ce088f 100644 --- a/crates/storybook/src/stories/components.rs +++ b/crates/storybook/src/stories/components.rs @@ -6,13 +6,17 @@ pub mod collab_panel; pub mod context_menu; pub mod facepile; pub mod keybinding; +pub mod language_selector; +pub mod multi_buffer; pub mod palette; pub mod panel; pub mod project_panel; +pub mod recent_projects; pub mod status_bar; pub mod tab; pub mod tab_bar; pub mod terminal; +pub mod theme_selector; pub mod title_bar; pub mod toolbar; pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/language_selector.rs b/crates/storybook/src/stories/components/language_selector.rs new file mode 100644 index 0000000000..c6dbd13d3f --- /dev/null +++ b/crates/storybook/src/stories/components/language_selector.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::LanguageSelector; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct LanguageSelectorStory {} + +impl LanguageSelectorStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, LanguageSelector>(cx)) + .child(Story::label(cx, "Default")) + .child(LanguageSelector::new()) + } +} diff --git a/crates/storybook/src/stories/components/multi_buffer.rs b/crates/storybook/src/stories/components/multi_buffer.rs new file mode 100644 index 0000000000..cd760c54dc --- /dev/null +++ b/crates/storybook/src/stories/components/multi_buffer.rs @@ -0,0 +1,24 @@ +use ui::prelude::*; +use ui::{hello_world_rust_buffer_example, MultiBuffer}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct MultiBufferStory {} + +impl MultiBufferStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + Story::container(cx) + .child(Story::title_for::<_, MultiBuffer>(cx)) + .child(Story::label(cx, "Default")) + .child(MultiBuffer::new(vec![ + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + ])) + } +} diff --git a/crates/storybook/src/stories/components/recent_projects.rs b/crates/storybook/src/stories/components/recent_projects.rs new file mode 100644 index 0000000000..f924654695 --- /dev/null +++ b/crates/storybook/src/stories/components/recent_projects.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::RecentProjects; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct RecentProjectsStory {} + +impl RecentProjectsStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, RecentProjects>(cx)) + .child(Story::label(cx, "Default")) + .child(RecentProjects::new()) + } +} diff --git a/crates/storybook/src/stories/components/theme_selector.rs b/crates/storybook/src/stories/components/theme_selector.rs new file mode 100644 index 0000000000..43e2a704e7 --- /dev/null +++ b/crates/storybook/src/stories/components/theme_selector.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::ThemeSelector; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ThemeSelectorStory {} + +impl ThemeSelectorStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, ThemeSelector>(cx)) + .child(Story::label(cx, "Default")) + .child(ThemeSelector::new()) + } +} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index acc965aa0a..6b0a9ac78d 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -42,13 +42,17 @@ pub enum ComponentStory { CollabPanel, Facepile, Keybinding, + LanguageSelector, + MultiBuffer, Palette, Panel, ProjectPanel, + RecentProjects, StatusBar, Tab, TabBar, Terminal, + ThemeSelector, TitleBar, Toolbar, TrafficLights, @@ -69,15 +73,25 @@ impl ComponentStory { Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(), Self::Facepile => components::facepile::FacepileStory::default().into_any(), Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(), + Self::LanguageSelector => { + components::language_selector::LanguageSelectorStory::default().into_any() + } + Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(), Self::Palette => components::palette::PaletteStory::default().into_any(), Self::Panel => components::panel::PanelStory::default().into_any(), Self::ProjectPanel => { components::project_panel::ProjectPanelStory::default().into_any() } + Self::RecentProjects => { + components::recent_projects::RecentProjectsStory::default().into_any() + } Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(), Self::Tab => components::tab::TabStory::default().into_any(), Self::TabBar => components::tab_bar::TabBarStory::default().into_any(), Self::Terminal => components::terminal::TerminalStory::default().into_any(), + Self::ThemeSelector => { + components::theme_selector::ThemeSelectorStory::default().into_any() + } Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(), Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(), Self::TrafficLights => { diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 0af13040f7..65b0218565 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -9,17 +9,22 @@ mod editor_pane; mod facepile; mod icon_button; mod keybinding; +mod language_selector; mod list; +mod multi_buffer; mod palette; mod panel; mod panes; mod player_stack; mod project_panel; +mod recent_projects; mod status_bar; mod tab; mod tab_bar; mod terminal; +mod theme_selector; mod title_bar; +mod toast; mod toolbar; mod traffic_lights; mod workspace; @@ -35,17 +40,22 @@ pub use editor_pane::*; pub use facepile::*; pub use icon_button::*; pub use keybinding::*; +pub use language_selector::*; pub use list::*; +pub use multi_buffer::*; pub use palette::*; pub use panel::*; pub use panes::*; pub use player_stack::*; pub use project_panel::*; +pub use recent_projects::*; pub use status_bar::*; pub use tab::*; pub use tab_bar::*; pub use terminal::*; +pub use theme_selector::*; pub use title_bar::*; +pub use toast::*; pub use toolbar::*; pub use traffic_lights::*; pub use workspace::*; diff --git a/crates/ui/src/components/language_selector.rs b/crates/ui/src/components/language_selector.rs new file mode 100644 index 0000000000..124d7f13ee --- /dev/null +++ b/crates/ui/src/components/language_selector.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct LanguageSelector { + scroll_state: ScrollState, +} + +impl LanguageSelector { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("C"), + PaletteItem::new("C++"), + PaletteItem::new("CSS"), + PaletteItem::new("Elixir"), + PaletteItem::new("Elm"), + PaletteItem::new("ERB"), + PaletteItem::new("Rust (current)"), + PaletteItem::new("Scheme"), + PaletteItem::new("TOML"), + PaletteItem::new("TypeScript"), + ]) + .placeholder("Select a language...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs index 8389c8c2c7..b7dff6b2c5 100644 --- a/crates/ui/src/components/list.rs +++ b/crates/ui/src/components/list.rs @@ -135,7 +135,7 @@ impl ListHeader { .size(IconSize::Small) })) .child( - Label::new(self.label.clone()) + Label::new(self.label) .color(LabelColor::Muted) .size(LabelSize::Small), ), @@ -191,7 +191,7 @@ impl ListSubHeader { .size(IconSize::Small) })) .child( - Label::new(self.label.clone()) + Label::new(self.label) .color(LabelColor::Muted) .size(LabelSize::Small), ), diff --git a/crates/ui/src/components/multi_buffer.rs b/crates/ui/src/components/multi_buffer.rs new file mode 100644 index 0000000000..d38603457a --- /dev/null +++ b/crates/ui/src/components/multi_buffer.rs @@ -0,0 +1,42 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize}; + +#[derive(Element)] +pub struct MultiBuffer { + view_type: PhantomData, + buffers: Vec, +} + +impl MultiBuffer { + pub fn new(buffers: Vec) -> Self { + Self { + view_type: PhantomData, + buffers, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + v_stack() + .w_full() + .h_full() + .flex_1() + .children(self.buffers.clone().into_iter().map(|buffer| { + v_stack() + .child( + div() + .flex() + .items_center() + .justify_between() + .p_4() + .fill(theme.lowest.base.default.background) + .child(Label::new("main.rs").size(LabelSize::Small)) + .child(IconButton::new(Icon::ArrowUpRight)), + ) + .child(buffer) + })) + } +} diff --git a/crates/ui/src/components/palette.rs b/crates/ui/src/components/palette.rs index 430ab8be63..16001e50c1 100644 --- a/crates/ui/src/components/palette.rs +++ b/crates/ui/src/components/palette.rs @@ -93,19 +93,17 @@ impl Palette { .fill(theme.lowest.base.hovered.background) .active() .fill(theme.lowest.base.pressed.background) - .child( - PaletteItem::new(item.label) - .keybinding(item.keybinding.clone()), - ) + .child(item.clone()) })), ), ) } } -#[derive(Element)] +#[derive(Element, Clone)] pub struct PaletteItem { pub label: &'static str, + pub sublabel: Option<&'static str>, pub keybinding: Option, } @@ -113,6 +111,7 @@ impl PaletteItem { pub fn new(label: &'static str) -> Self { Self { label, + sublabel: None, keybinding: None, } } @@ -122,6 +121,11 @@ impl PaletteItem { self } + pub fn sublabel>>(mut self, sublabel: L) -> Self { + self.sublabel = sublabel.into(); + self + } + pub fn keybinding(mut self, keybinding: K) -> Self where K: Into>, @@ -138,7 +142,11 @@ impl PaletteItem { .flex_row() .grow() .justify_between() - .child(Label::new(self.label)) + .child( + v_stack() + .child(Label::new(self.label)) + .children(self.sublabel.map(|sublabel| Label::new(sublabel))), + ) .children(self.keybinding.clone()) } } diff --git a/crates/ui/src/components/recent_projects.rs b/crates/ui/src/components/recent_projects.rs new file mode 100644 index 0000000000..6aca6631b9 --- /dev/null +++ b/crates/ui/src/components/recent_projects.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct RecentProjects { + scroll_state: ScrollState, +} + +impl RecentProjects { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("zed").sublabel("~/projects/zed"), + PaletteItem::new("saga").sublabel("~/projects/saga"), + PaletteItem::new("journal").sublabel("~/journal"), + PaletteItem::new("dotfiles").sublabel("~/dotfiles"), + PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"), + PaletteItem::new("laminar").sublabel("~/projects/laminar"), + ]) + .placeholder("Recent Projects...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/theme_selector.rs b/crates/ui/src/components/theme_selector.rs new file mode 100644 index 0000000000..e6f5237afe --- /dev/null +++ b/crates/ui/src/components/theme_selector.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct ThemeSelector { + scroll_state: ScrollState, +} + +impl ThemeSelector { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("One Dark"), + PaletteItem::new("Rosé Pine"), + PaletteItem::new("Rosé Pine Moon"), + PaletteItem::new("Sandcastle"), + PaletteItem::new("Solarized Dark"), + PaletteItem::new("Summercamp"), + PaletteItem::new("Atelier Cave Light"), + PaletteItem::new("Atelier Dune Light"), + PaletteItem::new("Atelier Estuary Light"), + PaletteItem::new("Atelier Forest Light"), + PaletteItem::new("Atelier Heath Light"), + ]) + .placeholder("Select Theme...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/toast.rs b/crates/ui/src/components/toast.rs new file mode 100644 index 0000000000..c299cdd6bc --- /dev/null +++ b/crates/ui/src/components/toast.rs @@ -0,0 +1,66 @@ +use crate::prelude::*; + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToastOrigin { + #[default] + Bottom, + BottomRight, +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToastVariant { + #[default] + Toast, + Status, +} + +/// A toast is a small, temporary window that appears to show a message to the user +/// or indicate a required action. +/// +/// Toasts should not persist on the screen for more than a few seconds unless +/// they are actively showing the a process in progress. +/// +/// Only one toast may be visible at a time. +#[derive(Element)] +pub struct Toast { + origin: ToastOrigin, + children: HackyChildren, + payload: HackyChildrenPayload, +} + +impl Toast { + pub fn new( + origin: ToastOrigin, + children: HackyChildren, + payload: HackyChildrenPayload, + ) -> Self { + Self { + origin, + children, + payload, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let color = ThemeColor::new(cx); + + let mut div = div(); + + if self.origin == ToastOrigin::Bottom { + div = div.right_1_2(); + } else { + div = div.right_4(); + } + + div.absolute() + .bottom_4() + .flex() + .py_2() + .px_1p5() + .min_w_40() + .rounded_md() + .fill(color.elevated_surface) + .max_w_64() + .children_any((self.children)(cx, self.payload.as_ref())) + } +} diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs index b609546f7f..b3d375bd64 100644 --- a/crates/ui/src/components/workspace.rs +++ b/crates/ui/src/components/workspace.rs @@ -82,6 +82,7 @@ impl WorkspaceElement { ); div() + .relative() .size_full() .flex() .flex_col() @@ -169,5 +170,17 @@ impl WorkspaceElement { ), ) .child(StatusBar::new()) + // An example of a toast is below + // Currently because of stacking order this gets obscured by other elements + + // .child(Toast::new( + // ToastOrigin::Bottom, + // |_, payload| { + // let theme = payload.downcast_ref::>().unwrap(); + + // vec![Label::new("label").into_any()] + // }, + // Box::new(theme.clone()), + // )) } } diff --git a/crates/ui/src/elements/details.rs b/crates/ui/src/elements/details.rs index d36b674291..9c829bcd41 100644 --- a/crates/ui/src/elements/details.rs +++ b/crates/ui/src/elements/details.rs @@ -27,7 +27,7 @@ impl Details { .gap_0p5() .text_xs() .text_color(theme.lowest.base.default.foreground) - .child(self.text.clone()) + .child(self.text) .children(self.meta.map(|m| m)) } } diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index 6d4053a4ae..26bf7dab22 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -60,6 +60,7 @@ pub enum Icon { ChevronUp, Close, ExclamationTriangle, + ExternalLink, File, FileGeneric, FileDoc, @@ -109,6 +110,7 @@ impl Icon { Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", Icon::ExclamationTriangle => "icons/warning.svg", + Icon::ExternalLink => "icons/external_link.svg", Icon::File => "icons/file.svg", Icon::FileGeneric => "icons/file_icons/file.svg", Icon::FileDoc => "icons/file_icons/book.svg", diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index b19b2becd1..e0da3579e2 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -29,6 +29,26 @@ impl SystemColor { } } +#[derive(Clone, Copy)] +pub struct ThemeColor { + pub border: Hsla, + pub border_variant: Hsla, + /// The background color of an elevated surface, like a modal, tooltip or toast. + pub elevated_surface: Hsla, +} + +impl ThemeColor { + pub fn new(cx: &WindowContext) -> Self { + let theme = theme(cx); + + Self { + border: theme.lowest.base.default.border, + border_variant: theme.lowest.variant.default.border, + elevated_surface: theme.middle.base.default.background, + } + } +} + #[derive(Default, PartialEq, EnumIter, Clone, Copy)] pub enum HighlightColor { #[default] diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index e7e6e0ac72..4578ce0bc9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -139,6 +139,12 @@ impl

PathLikeWithPosition

{ column: None, }) } else { + let maybe_col_str = + if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) { + &maybe_col_str[..maybe_col_str.len() - 1] + } else { + maybe_col_str + }; match maybe_col_str.parse::() { Ok(col) => Ok(Self { path_like: parse_path_like_str(path_like_str)?, @@ -241,7 +247,6 @@ mod tests { "test_file.rs:1::", "test_file.rs::1:2", "test_file.rs:1::2", - "test_file.rs:1:2:", "test_file.rs:1:2:3", ] { let actual = parse_str(input); @@ -277,6 +282,14 @@ mod tests { column: None, }, ), + ( + "crates/file_finder/src/file_finder.rs:1902:13:", + PathLikeWithPosition { + path_like: "crates/file_finder/src/file_finder.rs".to_string(), + row: Some(1902), + column: Some(13), + }, + ), ]; for (input, expected) in input_and_expected { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index d27be2c54b..aad97c558e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -33,7 +33,7 @@ use workspace::{self, Workspace}; use crate::state::ReplayableAction; -struct VimModeSetting(bool); +pub struct VimModeSetting(pub bool); #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index ea01f822a7..c7ad62f155 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" } util = { path = "../util" } picker = { path = "../picker" } workspace = { path = "../workspace" } +vim = { path = "../vim" } anyhow.workspace = true log.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 4d8df53a1b..a5d95429bd 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -10,6 +10,7 @@ use gpui::{ }; use settings::{update_settings_file, SettingsStore}; use std::{borrow::Cow, sync::Arc}; +use vim::VimModeSetting; use workspace::{ dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace, WorkspaceId, @@ -65,6 +66,7 @@ impl View for WelcomePage { let width = theme.welcome.page_width; let telemetry_settings = *settings::get::(cx); + let vim_mode_setting = settings::get::(cx).0; enum Metrics {} enum Diagnostics {} @@ -144,6 +146,27 @@ impl View for WelcomePage { ) .with_child( Flex::column() + .with_child( + theme::ui::checkbox::( + "Enable vim mode", + &theme.welcome.checkbox, + vim_mode_setting, + 0, + cx, + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| *setting = Some(checked), + ) + } + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container), + ) .with_child( theme::ui::checkbox_with_label::( Flex::column() @@ -186,7 +209,7 @@ impl View for WelcomePage { "Send crash reports", &theme.welcome.checkbox, telemetry_settings.diagnostics, - 0, + 1, cx, |this, checked, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 41c86e538d..d1240a45ce 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -22,7 +22,6 @@ test-support = [ db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } -channel = { path = "../channel" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c12cb261c8..aef03dcda0 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,10 +1,7 @@ -use std::{cell::RefCell, rc::Rc, sync::Arc}; - -use crate::{ - pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace, -}; +use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace}; use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; +use collections::HashMap; use gpui::{ elements::*, geometry::{rect::RectF, vector::Vector2F}, @@ -13,6 +10,7 @@ use gpui::{ }; use project::Project; use serde::Deserialize; +use std::{cell::RefCell, rc::Rc, sync::Arc}; use theme::Theme; const HANDLE_HITBOX_SIZE: f32 = 4.0; @@ -95,7 +93,7 @@ impl PaneGroup { &self, project: &ModelHandle, theme: &Theme, - follower_states: &FollowerStatesByLeader, + follower_states: &HashMap, FollowerState>, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, @@ -162,7 +160,7 @@ impl Member { project: &ModelHandle, basis: usize, theme: &Theme, - follower_states: &FollowerStatesByLeader, + follower_states: &HashMap, FollowerState>, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, @@ -179,19 +177,10 @@ impl Member { ChildView::new(pane, cx).into_any() }; - let leader = follower_states - .iter() - .find_map(|(leader_id, follower_states)| { - if follower_states.contains_key(pane) { - Some(leader_id) - } else { - None - } - }) - .and_then(|leader_id| { - let room = active_call?.read(cx).room()?.read(cx); - room.remote_participant_for_peer_id(*leader_id) - }); + let leader = follower_states.get(pane).and_then(|state| { + let room = active_call?.read(cx).room()?.read(cx); + room.remote_participant_for_peer_id(state.leader_id) + }); let mut leader_border = Border::default(); let mut leader_status_box = None; @@ -486,7 +475,7 @@ impl PaneAxis { project: &ModelHandle, basis: usize, theme: &Theme, - follower_state: &FollowerStatesByLeader, + follower_states: &HashMap, FollowerState>, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, zoomed: Option<&AnyViewHandle>, @@ -515,7 +504,7 @@ impl PaneAxis { project, (basis + ix) * 10, theme, - follower_state, + follower_states, active_call, active_pane, zoomed, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c021384d91..c3c6f9a4b6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,7 +12,6 @@ mod workspace_settings; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; -use channel::ChannelStore; use client::{ proto::{self, PeerId}, Client, Status, TypedEnvelope, UserStore, @@ -79,7 +78,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{async_iife, ResultExt}; +use util::ResultExt; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { @@ -450,7 +449,6 @@ pub struct AppState { pub languages: Arc, pub client: Arc, pub user_store: ModelHandle, - pub channel_store: ModelHandle, pub workspace_store: ModelHandle, pub fs: Arc, pub build_window_options: @@ -487,8 +485,6 @@ impl AppState { let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); theme::init((), cx); @@ -500,7 +496,7 @@ impl AppState { fs, languages, user_store, - channel_store, + // channel_store, workspace_store, initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), @@ -573,11 +569,12 @@ pub struct Workspace { panes_by_item: HashMap>, active_pane: ViewHandle, last_active_center_pane: Option>, + last_active_view_id: Option, status_bar: ViewHandle, titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, - follower_states_by_leader: FollowerStatesByLeader, + follower_states: HashMap, FollowerState>, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, active_call: Option<(ModelHandle, Vec)>, @@ -602,10 +599,9 @@ pub struct ViewId { pub id: u64, } -type FollowerStatesByLeader = HashMap, FollowerState>>; - #[derive(Default)] struct FollowerState { + leader_id: PeerId, active_view_id: Option, items_by_leader_view_id: HashMap>, } @@ -786,6 +782,7 @@ impl Workspace { panes_by_item: Default::default(), active_pane: center_pane.clone(), last_active_center_pane: Some(center_pane.downgrade()), + last_active_view_id: None, status_bar, titlebar_item: None, notifications: Default::default(), @@ -793,7 +790,7 @@ impl Workspace { bottom_dock, right_dock, project: project.clone(), - follower_states_by_leader: Default::default(), + follower_states: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, active_call, @@ -934,7 +931,8 @@ impl Workspace { app_state, cx, ) - .await; + .await + .unwrap_or_default(); (workspace, opened_items) }) @@ -2510,13 +2508,16 @@ impl Workspace { } fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { - for state in states_by_pane.into_values() { - for item in state.items_by_leader_view_id.into_values() { + self.follower_states.retain(|_, state| { + if state.leader_id == peer_id { + for item in state.items_by_leader_view_id.values() { item.set_leader_peer_id(None, cx); } + false + } else { + true } - } + }); cx.notify(); } @@ -2529,10 +2530,15 @@ impl Workspace { self.last_leaders_by_pane .insert(pane.downgrade(), leader_id); - self.follower_states_by_leader - .entry(leader_id) - .or_default() - .insert(pane.clone(), Default::default()); + self.unfollow(&pane, cx); + self.follower_states.insert( + pane.clone(), + FollowerState { + leader_id, + active_view_id: None, + items_by_leader_view_id: Default::default(), + }, + ); cx.notify(); let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); @@ -2547,9 +2553,8 @@ impl Workspace { let response = request.await?; this.update(&mut cx, |this, _| { let state = this - .follower_states_by_leader - .get_mut(&leader_id) - .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) + .follower_states + .get_mut(&pane) .ok_or_else(|| anyhow!("following interrupted"))?; state.active_view_id = if let Some(active_view_id) = response.active_view_id { Some(ViewId::from_proto(active_view_id)?) @@ -2644,12 +2649,10 @@ impl Workspace { } // if you're already following, find the right pane and focus it. - for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader { - if leader_id == *existing_leader_id { - for (pane, _) in states_by_pane { - cx.focus(pane); - return None; - } + for (pane, state) in &self.follower_states { + if leader_id == state.leader_id { + cx.focus(pane); + return None; } } @@ -2662,36 +2665,37 @@ impl Workspace { pane: &ViewHandle, cx: &mut ViewContext, ) -> Option { - for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { - let leader_id = *leader_id; - if let Some(state) = states_by_pane.remove(pane) { - for (_, item) in state.items_by_leader_view_id { - item.set_leader_peer_id(None, cx); - } - - if states_by_pane.is_empty() { - self.follower_states_by_leader.remove(&leader_id); - let project_id = self.project.read(cx).remote_id(); - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - self.app_state - .client - .send(proto::Unfollow { - room_id, - project_id, - leader_id: Some(leader_id), - }) - .log_err(); - } - - cx.notify(); - return Some(leader_id); - } + let state = self.follower_states.remove(pane)?; + let leader_id = state.leader_id; + for (_, item) in state.items_by_leader_view_id { + item.set_leader_peer_id(None, cx); } - None + + if self + .follower_states + .values() + .all(|state| state.leader_id != state.leader_id) + { + let project_id = self.project.read(cx).remote_id(); + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + self.app_state + .client + .send(proto::Unfollow { + room_id, + project_id, + leader_id: Some(leader_id), + }) + .log_err(); + } + + cx.notify(); + Some(leader_id) } pub fn is_being_followed(&self, peer_id: PeerId) -> bool { - self.follower_states_by_leader.contains_key(&peer_id) + self.follower_states + .values() + .any(|state| state.leader_id == peer_id) } fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { @@ -2862,6 +2866,7 @@ impl Workspace { cx.notify(); + self.last_active_view_id = active_view_id.clone(); proto::FollowResponse { active_view_id, views: self @@ -2913,8 +2918,8 @@ impl Workspace { match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(cx, |this, _| { - if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { - for state in state.values_mut() { + for (_, state) in &mut this.follower_states { + if state.leader_id == leader_id { state.active_view_id = if let Some(active_view_id) = update_active_view.id.clone() { Some(ViewId::from_proto(active_view_id)?) @@ -2936,8 +2941,8 @@ impl Workspace { let mut tasks = Vec::new(); this.update(cx, |this, cx| { let project = this.project.clone(); - if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) { - for state in state.values_mut() { + for (_, state) in &mut this.follower_states { + if state.leader_id == leader_id { let view_id = ViewId::from_proto(id.clone())?; if let Some(item) = state.items_by_leader_view_id.get(&view_id) { tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); @@ -2950,10 +2955,9 @@ impl Workspace { } proto::update_followers::Variant::CreateView(view) => { let panes = this.read_with(cx, |this, _| { - this.follower_states_by_leader - .get(&leader_id) - .into_iter() - .flat_map(|states_by_pane| states_by_pane.keys()) + this.follower_states + .iter() + .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) .cloned() .collect() })?; @@ -3012,11 +3016,7 @@ impl Workspace { for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { let items = futures::future::try_join_all(item_tasks).await?; this.update(cx, |this, cx| { - let state = this - .follower_states_by_leader - .get_mut(&leader_id)? - .get_mut(&pane)?; - + let state = this.follower_states.get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { item.set_leader_peer_id(Some(leader_id), cx); state.items_by_leader_view_id.insert(id, item); @@ -3028,7 +3028,7 @@ impl Workspace { Ok(()) } - fn update_active_view_for_followers(&self, cx: &AppContext) { + fn update_active_view_for_followers(&mut self, cx: &AppContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); if self.active_pane.read(cx).has_focus() { @@ -3046,11 +3046,14 @@ impl Workspace { } } - self.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateActiveView(update), - cx, - ); + if update.id != self.last_active_view_id { + self.last_active_view_id = update.id.clone(); + self.update_followers( + is_project_item, + proto::update_followers::Variant::UpdateActiveView(update), + cx, + ); + } } fn update_followers( @@ -3070,15 +3073,7 @@ impl Workspace { } pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { - self.follower_states_by_leader - .iter() - .find_map(|(leader_id, state)| { - if state.contains_key(pane) { - Some(*leader_id) - } else { - None - } - }) + self.follower_states.get(pane).map(|state| state.leader_id) } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { @@ -3106,17 +3101,23 @@ impl Workspace { } }; - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - if leader_in_this_app { - let item = state - .active_view_id - .and_then(|id| state.items_by_leader_view_id.get(&id)); - if let Some(item) = item { + for (pane, state) in &self.follower_states { + if state.leader_id != leader_id { + continue; + } + if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { if leader_in_this_project || !item.is_project_item(cx) { items_to_activate.push((pane.clone(), item.boxed_clone())); } - continue; + } else { + log::warn!( + "unknown view id {:?} for leader {:?}", + active_view_id, + leader_id + ); } + continue; } if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { items_to_activate.push((pane.clone(), Box::new(shared_screen))); @@ -3394,140 +3395,124 @@ impl Workspace { serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, cx: &mut AppContext, - ) -> Task, anyhow::Error>>>> { + ) -> Task>>>> { cx.spawn(|mut cx| async move { - let result = async_iife! {{ - let (project, old_center_pane) = - workspace.read_with(&cx, |workspace, _| { - ( - workspace.project().clone(), - workspace.last_active_center_pane.clone(), - ) - })?; + let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { + ( + workspace.project().clone(), + workspace.last_active_center_pane.clone(), + ) + })?; - let mut center_items = None; - let mut center_group = None; - // Traverse the splits tree and add to things - if let Some((group, active_pane, items)) = serialized_workspace - .center_group - .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) - .await { - center_items = Some(items); - center_group = Some((group, active_pane)) + let mut center_group = None; + let mut center_items = None; + // Traverse the splits tree and add to things + if let Some((group, active_pane, items)) = serialized_workspace + .center_group + .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) + .await + { + center_items = Some(items); + center_group = Some((group, active_pane)) + } + + let mut items_by_project_path = cx.read(|cx| { + center_items + .unwrap_or_default() + .into_iter() + .filter_map(|item| { + let item = item?; + let project_path = item.project_path(cx)?; + Some((project_path, item)) + }) + .collect::>() + }); + + let opened_items = paths_to_open + .into_iter() + .map(|path_to_open| { + path_to_open + .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) + }) + .collect::>(); + + // Remove old panes from workspace panes list + workspace.update(&mut cx, |workspace, cx| { + if let Some((center_group, active_pane)) = center_group { + workspace.remove_panes(workspace.center.root.clone(), cx); + + // Swap workspace center group + workspace.center = PaneGroup::with_root(center_group); + + // Change the focus to the workspace first so that we retrigger focus in on the pane. + cx.focus_self(); + + if let Some(active_pane) = active_pane { + cx.focus(&active_pane); + } else { + cx.focus(workspace.panes.last().unwrap()); + } + } else { + let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); + if let Some(old_center_handle) = old_center_handle { + cx.focus(&old_center_handle) + } else { + cx.focus_self() + } } - let resulting_list = cx.read(|cx| { - let mut opened_items = center_items - .unwrap_or_default() - .into_iter() - .filter_map(|item| { - let item = item?; - let project_path = item.project_path(cx)?; - Some((project_path, item)) - }) - .collect::>(); - - paths_to_open - .into_iter() - .map(|path_to_open| { - path_to_open.map(|path_to_open| { - Ok(opened_items.remove(&path_to_open)) - }) - .transpose() - .map(|item| item.flatten()) - .transpose() - }) - .collect::>() - }); - - // Remove old panes from workspace panes list - workspace.update(&mut cx, |workspace, cx| { - if let Some((center_group, active_pane)) = center_group { - workspace.remove_panes(workspace.center.root.clone(), cx); - - // Swap workspace center group - workspace.center = PaneGroup::with_root(center_group); - - // Change the focus to the workspace first so that we retrigger focus in on the pane. - cx.focus_self(); - - if let Some(active_pane) = active_pane { - cx.focus(&active_pane); - } else { - cx.focus(workspace.panes.last().unwrap()); + let docks = serialized_workspace.docks; + workspace.left_dock.update(cx, |dock, cx| { + dock.set_open(docks.left.visible, cx); + if let Some(active_panel) = docks.left.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); } - } else { - let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); - if let Some(old_center_handle) = old_center_handle { - cx.focus(&old_center_handle) - } else { - cx.focus_self() + } + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.left.zoom, cx)); + if docks.left.visible && docks.left.zoom { + cx.focus_self() + } + }); + // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something + workspace.right_dock.update(cx, |dock, cx| { + dock.set_open(docks.right.visible, cx); + if let Some(active_panel) = docks.right.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.right.zoom, cx)); + + if docks.right.visible && docks.right.zoom { + cx.focus_self() + } + }); + workspace.bottom_dock.update(cx, |dock, cx| { + dock.set_open(docks.bottom.visible, cx); + if let Some(active_panel) = docks.bottom.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); } } - let docks = serialized_workspace.docks; - workspace.left_dock.update(cx, |dock, cx| { - dock.set_open(docks.left.visible, cx); - if let Some(active_panel) = docks.left.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - dock.active_panel() - .map(|panel| { - panel.set_zoomed(docks.left.zoom, cx) - }); - if docks.left.visible && docks.left.zoom { - cx.focus_self() - } - }); - // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something - workspace.right_dock.update(cx, |dock, cx| { - dock.set_open(docks.right.visible, cx); - if let Some(active_panel) = docks.right.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); - } - } - dock.active_panel() - .map(|panel| { - panel.set_zoomed(docks.right.zoom, cx) - }); + if docks.bottom.visible && docks.bottom.zoom { + cx.focus_self() + } + }); - if docks.right.visible && docks.right.zoom { - cx.focus_self() - } - }); - workspace.bottom_dock.update(cx, |dock, cx| { - dock.set_open(docks.bottom.visible, cx); - if let Some(active_panel) = docks.bottom.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } + cx.notify(); + })?; - dock.active_panel() - .map(|panel| { - panel.set_zoomed(docks.bottom.zoom, cx) - }); + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated + workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; - if docks.bottom.visible && docks.bottom.zoom { - cx.focus_self() - } - }); - - - cx.notify(); - })?; - - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; - - Ok::<_, anyhow::Error>(resulting_list) - }}; - - result.await.unwrap_or_default() + Ok(opened_items) }) } @@ -3536,15 +3521,12 @@ impl Workspace { let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), workspace_store, client, user_store, - channel_store, fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), @@ -3601,7 +3583,7 @@ async fn open_items( mut project_paths_to_open: Vec<(PathBuf, Option)>, app_state: Arc, mut cx: AsyncAppContext, -) -> Vec>>> { +) -> Result>>>> { let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); if let Some(serialized_workspace) = serialized_workspace { @@ -3619,16 +3601,19 @@ async fn open_items( cx, ) }) - .await; + .await?; let restored_project_paths = cx.read(|cx| { restored_items .iter() - .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx)) + .filter_map(|item| item.as_ref()?.project_path(cx)) .collect::>() }); - opened_items = restored_items; + for restored_item in restored_items { + opened_items.push(restored_item.map(Ok)); + } + project_paths_to_open .iter_mut() .for_each(|(_, project_path)| { @@ -3681,7 +3666,7 @@ async fn open_items( } } - opened_items + Ok(opened_items) } fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { @@ -3817,7 +3802,7 @@ impl View for Workspace { self.center.render( &project, &theme, - &self.follower_states_by_leader, + &self.follower_states, self.active_call(), self.active_pane(), self.zoomed diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 62bdddab5b..dc2697ab19 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -165,17 +165,25 @@ impl LspAdapter for RustLspAdapter { lazy_static! { static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap(); } - let detail = completion.detail.as_ref().unwrap(); - if detail.starts_with("fn(") { - let text = REGEX.replace(&completion.label, &detail[2..]).to_string(); - let source = Rope::from(format!("fn {} {{}}", text).as_str()); - let runs = language.highlight_text(&source, 3..3 + text.len()); - return Some(CodeLabel { - filter_range: 0..completion.label.find('(').unwrap_or(text.len()), - text, - runs, - }); + const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"]; + let prefix = FUNCTION_PREFIXES + .iter() + .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix))); + // fn keyword should be followed by opening parenthesis. + if let Some((prefix, suffix)) = prefix { + if suffix.starts_with('(') { + let text = REGEX.replace(&completion.label, suffix).to_string(); + let source = Rope::from(format!("{prefix} {} {{}}", text).as_str()); + let run_start = prefix.len() + 1; + let runs = + language.highlight_text(&source, run_start..run_start + text.len()); + return Some(CodeLabel { + filter_range: 0..completion.label.find('(').unwrap_or(text.len()), + text, + runs, + }); + } } } Some(kind) => { @@ -377,7 +385,28 @@ mod tests { ], }) ); - + assert_eq!( + language + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("async fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + }) + ); assert_eq!( language .label_for_completion(&lsp::CompletionItem { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a75caa54f6..f89a880c71 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,7 +3,6 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; -use channel::ChannelStore; use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, @@ -78,7 +77,8 @@ fn main() { let mut app = gpui::App::new(Assets).unwrap(); let installation_id = app.background().block(installation_id()).ok(); - init_panic_hook(&app, installation_id.clone()); + let session_id = Uuid::new_v4().to_string(); + init_panic_hook(&app, installation_id.clone(), session_id.clone()); load_embedded_fonts(&app); @@ -132,8 +132,6 @@ fn main() { languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let channel_store = - cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); cx.set_global(client.clone()); @@ -150,7 +148,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); - channel::init(&client); + channel::init(&client, user_store.clone(), cx); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); @@ -172,13 +170,12 @@ fn main() { }) .detach(); - client.telemetry().start(installation_id, cx); + client.telemetry().start(installation_id, session_id, cx); let app_state = Arc::new(AppState { languages, client: client.clone(), user_store, - channel_store, fs, build_window_options, initialize_workspace, @@ -387,6 +384,7 @@ struct Panic { panicked_on: u128, #[serde(skip_serializing_if = "Option::is_none")] installation_id: Option, + session_id: String, } #[derive(Serialize)] @@ -397,7 +395,7 @@ struct PanicRequest { static PANIC_COUNT: AtomicU32 = AtomicU32::new(0); -fn init_panic_hook(app: &App, installation_id: Option) { +fn init_panic_hook(app: &App, installation_id: Option, session_id: String) { let is_pty = stdout_is_a_pty(); let platform = app.platform(); @@ -462,7 +460,7 @@ fn init_panic_hook(app: &App, installation_id: Option) { line: location.line(), }), app_version: app_version.clone(), - release_channel: RELEASE_CHANNEL.dev_name().into(), + release_channel: RELEASE_CHANNEL.display_name().into(), os_name: platform.os_name().into(), os_version: platform .os_version() @@ -475,13 +473,14 @@ fn init_panic_hook(app: &App, installation_id: Option) { .as_millis(), backtrace, installation_id: installation_id.clone(), + session_id: session_id.clone(), }; - if is_pty { - if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { - eprintln!("{}", panic_data_json); - } - } else { + if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { + log::error!("{}", panic_data_json); + } + + if !is_pty { if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index e3c08ff2c8..9b416e14be 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -3,8 +3,6 @@ use cli::{ipc::IpcSender, CliRequest, CliResponse}; use futures::channel::mpsc; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use std::ffi::OsStr; -use std::fs::OpenOptions; -use std::io::Write; use std::os::unix::prelude::OsStrExt; use std::sync::atomic::Ordering; use std::{path::PathBuf, sync::atomic::AtomicBool}; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ce9b7e32a3..4e9a34c269 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2424,6 +2424,7 @@ mod tests { 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); workspace::init(app_state.clone(), cx); Project::init_settings(cx); diff --git a/docs/building-zed.md b/docs/building-zed.md index 6981913285..ec4538cf85 100644 --- a/docs/building-zed.md +++ b/docs/building-zed.md @@ -75,8 +75,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil - If you are just using the latest version, but not working on zed: - `cargo run --release` - If you need to run the collaboration server locally: - - `script/zed-with-local-servers` - - If you need to test collaboration with mutl + - `script/zed-local` ## Troubleshooting diff --git a/docs/local-collaboration.md b/docs/local-collaboration.md index 7d8054af67..4c059c0878 100644 --- a/docs/local-collaboration.md +++ b/docs/local-collaboration.md @@ -17,6 +17,6 @@ ## Testing collab locally 1. Run `foreman start` from the root of the repo. -1. In another terminal run `script/start-local-collaboration`. +1. In another terminal run `script/zed-local -2`. 1. Two copies of Zed will open. Add yourself as a contact in the one that is not you. 1. Start a collaboration session as normal with any open project. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9720c52eaa..38d54d7aed 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.72.1" +channel = "1.73" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] diff --git a/script/crate-dep-graph b/script/crate-dep-graph new file mode 100755 index 0000000000..25285cc097 --- /dev/null +++ b/script/crate-dep-graph @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +if [[ -x cargo-depgraph ]]; then + cargo install cargo-depgraph +fi + +graph_file=target/crate-graph.html + +cargo depgraph \ + --workspace-only \ + --offline \ + --root=zed,cli,collab \ + --dedup-transitive-deps \ + | dot -Tsvg > $graph_file + +echo "open $graph_file" +open $graph_file diff --git a/script/start-local-collaboration b/script/start-local-collaboration deleted file mode 100755 index 0c4e60f9c3..0000000000 --- a/script/start-local-collaboration +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -e - -if [[ -z "$GITHUB_TOKEN" ]]; then - cat <<-MESSAGE -Missing \`GITHUB_TOKEN\` environment variable. This token is needed -for fetching your GitHub identity from the command-line. - -Create an access token here: https://github.com/settings/tokens -Then edit your \`~/.zshrc\` (or other shell initialization script), -adding a line like this: - - export GITHUB_TOKEN="(the token)" - -MESSAGE - exit 1 -fi - -# Install jq if it's not installed -if ! command -v jq &> /dev/null; then - echo "Installing jq..." - brew install jq -fi - -# Start one Zed instance as the current user and a second instance with a different user. -username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login) -username_2=nathansobo -if [[ $username_1 == $username_2 ]]; then - username_2=as-cii -fi - -# Make each Zed instance take up half of the screen. -output=$(system_profiler SPDisplaysDataType -json) -main_display=$(echo "$output" | jq '.SPDisplaysDataType[].spdisplays_ndrvs[] | select(.spdisplays_main == "spdisplays_yes")') -resolution=$(echo "$main_display" | jq -r '._spdisplays_resolution') -width=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[0].string') -half_width=$(($width / 2)) -height=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[1].string') -y=0 - -position_1=0,${y} -position_2=${half_width},${y} - -# Authenticate using the collab server's admin secret. -export ZED_STATELESS=1 -export ZED_ALWAYS_ACTIVE=1 -export ZED_ADMIN_API_TOKEN=secret -export ZED_SERVER_URL=http://localhost:8080 -export ZED_WINDOW_SIZE=${half_width},${height} - -cargo build -sleep 0.5 - -# Start the two Zed child processes. Open the given paths with the first instance. -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT -ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & -SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & -wait diff --git a/script/zed-local b/script/zed-local new file mode 100755 index 0000000000..683e31ef14 --- /dev/null +++ b/script/zed-local @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +const {spawn, execFileSync} = require('child_process') + +const RESOLUTION_REGEX = /(\d+) x (\d+)/ +const DIGIT_FLAG_REGEX = /^--?(\d+)$/ + +const args = process.argv.slice(2) + +// Parse the number of Zed instances to spawn. +let instanceCount = 1 +const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX) +if (digitMatch) { + instanceCount = parseInt(digitMatch[1]) + args.shift() +} +if (instanceCount > 4) { + throw new Error('Cannot spawn more than 4 instances') +} + +// Parse the resolution of the main screen +const displayInfo = JSON.parse( + execFileSync( + 'system_profiler', + ['SPDisplaysDataType', '-json'], + {encoding: 'utf8'} + ) +) +const mainDisplayResolution = displayInfo + ?.SPDisplaysDataType[0] + ?.spdisplays_ndrvs + ?.find(entry => entry.spdisplays_main === "spdisplays_yes") + ?._spdisplays_resolution + ?.match(RESOLUTION_REGEX) +if (!mainDisplayResolution) { + throw new Error('Could not parse screen resolution') +} +const screenWidth = parseInt(mainDisplayResolution[1]) +const screenHeight = parseInt(mainDisplayResolution[2]) + +// Determine the window size for each instance +let instanceWidth = screenWidth +let instanceHeight = screenHeight +if (instanceCount > 1) { + instanceWidth = Math.floor(screenWidth / 2) + if (instanceCount > 2) { + instanceHeight = Math.floor(screenHeight / 2) + } +} + +let users = [ + 'nathansobo', + 'as-cii', + 'maxbrunsfeld', + 'iamnbutler' +] + +// If a user is specified, make sure it's first in the list +const user = process.env.ZED_IMPERSONATE +if (user) { + users = [user].concat(users.filter(u => u !== user)) +} + +const positions = [ + '0,0', + `${instanceWidth},0`, + `0,${instanceHeight}`, + `${instanceWidth},${instanceHeight}` +] + +execFileSync('cargo', ['build'], {stdio: 'inherit'}) + +setTimeout(() => { + for (let i = 0; i < instanceCount; i++) { + spawn('target/debug/Zed', i == 0 ? args : [], { + stdio: 'inherit', + env: { + ZED_IMPERSONATE: users[i], + ZED_WINDOW_POSITION: positions[i], + ZED_STATELESS: '1', + ZED_ALWAYS_ACTIVE: '1', + ZED_SERVER_URL: 'http://localhost:8080', + ZED_ADMIN_API_TOKEN: 'secret', + ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}` + } + }) + } +}, 0.1) diff --git a/script/zed-with-local-servers b/script/zed-with-local-servers deleted file mode 100755 index e1b224de60..0000000000 --- a/script/zed-with-local-servers +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -: "${ZED_IMPERSONATE:=as-cii}" -export ZED_IMPERSONATE - -ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@