From 8bdc59703ac9b9ec80e17afedf697ea8490697a0 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 4 Oct 2023 15:00:34 -0400 Subject: [PATCH 01/31] v0.107.x preview --- crates/zed/RELEASE_CHANNEL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 90012116c0..4de2f126df 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -dev \ No newline at end of file +preview \ No newline at end of file From 11f7a2cb0e13e841c09c9c90977c893695f9289e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Oct 2023 15:39:24 -0600 Subject: [PATCH 02/31] Fix panic in increment (#3084) Release Notes: - Fixes a panic in vim when incrementing a non-number. --- crates/vim/src/normal/increment.rs | 14 ++++++++++---- crates/vim/test_data/test_increment_radix.json | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index d7f3d1f904..9d62f8ab7b 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -78,10 +78,14 @@ fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) { 2 => format!("{:b}", result), _ => unreachable!(), }; - if selection.is_empty() { - new_anchors.push((false, snapshot.anchor_after(range.end))) - } - edits.push((range, replace)); + edits.push((range.clone(), replace)); + } + if selection.is_empty() { + new_anchors.push((false, snapshot.anchor_after(range.end))) + } + } else { + if selection.is_empty() { + new_anchors.push((true, snapshot.anchor_after(start))) } } } @@ -226,6 +230,8 @@ mod test { cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)") .await; cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await; + cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana") + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_increment_radix.json b/crates/vim/test_data/test_increment_radix.json index f9379a7195..0f41c01599 100644 --- a/crates/vim/test_data/test_increment_radix.json +++ b/crates/vim/test_data/test_increment_radix.json @@ -13,3 +13,6 @@ {"Put":{"state":"ˇ-1"}} {"Key":"ctrl-a"} {"Get":{"state":"ˇ0","mode":"Normal"}} +{"Put":{"state":"banˇana"}} +{"Key":"ctrl-a"} +{"Get":{"state":"banˇana","mode":"Normal"}} From b90c04009f519d7e9e6201655e1c1b39f3980125 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Oct 2023 15:10:49 -0700 Subject: [PATCH 03/31] Ensure chat messages are retrieved in order of id (#3086) Also, remove logic for implicitly marking chat messages as observed when they are fetched. I think this is unnecessary, because the client always explicitly acknowledges messages when they are shown. Release Notes: - Fixed a bug where chat messages were shown out of order (preview only) --- crates/collab/src/db/queries.rs | 10 ----- crates/collab/src/db/queries/messages.rs | 49 +----------------------- 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 59face1f33..80bd8704b2 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -9,13 +9,3 @@ pub mod projects; pub mod rooms; pub mod servers; pub mod users; - -fn max_assign(max: &mut Option, val: T) { - if let Some(max_val) = max { - if val > *max_val { - *max = Some(val); - } - } else { - *max = Some(val); - } -} diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index db1252230e..83b5382cf5 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -89,17 +89,14 @@ impl Database { let mut rows = channel_message::Entity::find() .filter(condition) + .order_by_asc(channel_message::Column::Id) .limit(count as u64) .stream(&*tx) .await?; - let mut max_id = None; let mut messages = Vec::new(); while let Some(row) = rows.next().await { let row = row?; - - max_assign(&mut max_id, row.id); - let nonce = row.nonce.as_u64_pair(); messages.push(proto::ChannelMessage { id: row.id.to_proto(), @@ -113,50 +110,6 @@ impl Database { }); } drop(rows); - - if let Some(max_id) = max_id { - let has_older_message = observed_channel_messages::Entity::find() - .filter( - observed_channel_messages::Column::UserId - .eq(user_id) - .and(observed_channel_messages::Column::ChannelId.eq(channel_id)) - .and(observed_channel_messages::Column::ChannelMessageId.lt(max_id)), - ) - .one(&*tx) - .await? - .is_some(); - - if has_older_message { - observed_channel_messages::Entity::update( - observed_channel_messages::ActiveModel { - user_id: ActiveValue::Unchanged(user_id), - channel_id: ActiveValue::Unchanged(channel_id), - channel_message_id: ActiveValue::Set(max_id), - }, - ) - .exec(&*tx) - .await?; - } else { - observed_channel_messages::Entity::insert( - observed_channel_messages::ActiveModel { - user_id: ActiveValue::Set(user_id), - channel_id: ActiveValue::Set(channel_id), - channel_message_id: ActiveValue::Set(max_id), - }, - ) - .on_conflict( - OnConflict::columns([ - observed_channel_messages::Column::UserId, - observed_channel_messages::Column::ChannelId, - ]) - .update_columns([observed_channel_messages::Column::ChannelMessageId]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - } - Ok(messages) }) .await From 643f3db2b2572d164451736b955eb532fda6cd1d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 4 Oct 2023 15:14:39 -0700 Subject: [PATCH 04/31] 107 channel touch ups (#3087) Release Notes: - Add user avatars to channel chat messages - Group messages by sender - Fix visual bugs in new chat and note buttons --- crates/collab_ui/src/chat_panel.rs | 151 +++++++++++++++++------- crates/collab_ui/src/collab_panel.rs | 35 ++++-- crates/theme/src/theme.rs | 2 + styles/src/style_tree/chat_panel.ts | 14 ++- styles/src/style_tree/component_test.ts | 1 + 5 files changed, 154 insertions(+), 49 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index f0874f544e..41bc5fbd08 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -130,6 +130,7 @@ impl ChatPanel { fs, client, channel_store, + active_chat: Default::default(), pending_serialization: Task::ready(None), message_list, @@ -328,12 +329,26 @@ impl ChatPanel { } fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix); + let (message, is_continuation, is_last) = { + let active_chat = self.active_chat.as_ref().unwrap().0.read(cx); + let last_message = active_chat.message(ix.saturating_sub(1)); + let this_message = active_chat.message(ix); + let is_continuation = last_message.id != this_message.id + && this_message.sender.id == last_message.sender.id; + + ( + active_chat.message(ix), + is_continuation, + active_chat.message_count() == ix + 1, + ) + }; let now = OffsetDateTime::now_utc(); let theme = theme::current(cx); let style = if message.is_pending() { &theme.chat_panel.pending_message + } else if is_continuation { + &theme.chat_panel.continuation_message } else { &theme.chat_panel.message }; @@ -349,49 +364,103 @@ impl ChatPanel { enum DeleteMessage {} let body = message.body.clone(); - Flex::column() - .with_child( - Flex::row() - .with_child( - Label::new( - message.sender.github_login.clone(), - style.sender.text.clone(), + if is_continuation { + Flex::row() + .with_child(Text::new(body, style.body.clone())) + .with_children(message_id_to_remove.map(|id| { + MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { + let button_style = theme.chat_panel.icon_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .into_any() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_message(id, cx); + }) + .flex_float() + })) + .contained() + .with_style(style.container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. + }) + .into_any() + } else { + Flex::column() + .with_child( + Flex::row() + .with_child( + message + .sender + .avatar + .clone() + .map(|avatar| { + Image::from_data(avatar) + .with_style(theme.collab_panel.channel_avatar) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width( + theme.collab_panel.channel_avatar.width.unwrap_or(12.), + ) + .into_any() + }) + .contained() + .with_margin_right(4.), ) - .contained() - .with_style(style.sender.container), - ) - .with_child( - Label::new( - format_timestamp(message.timestamp, now, self.local_timezone), - style.timestamp.text.clone(), + .with_child( + Label::new( + message.sender.github_login.clone(), + style.sender.text.clone(), + ) + .contained() + .with_style(style.sender.container), ) - .contained() - .with_style(style.timestamp.container), - ) - .with_children(message_id_to_remove.map(|id| { - MouseEventHandler::new::( - id as usize, - cx, - |mouse_state, _| { - let button_style = - theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() - }, + .with_child( + Label::new( + format_timestamp(message.timestamp, now, self.local_timezone), + style.timestamp.text.clone(), + ) + .contained() + .with_style(style.timestamp.container), ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - })), - ) - .with_child(Text::new(body, style.body.clone())) - .contained() - .with_style(style.container) - .into_any() + .with_children(message_id_to_remove.map(|id| { + MouseEventHandler::new::( + id as usize, + cx, + |mouse_state, _| { + let button_style = + theme.chat_panel.icon_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .into_any() + }, + ) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_message(id, cx); + }) + .flex_float() + })) + .align_children_center(), + ) + .with_child(Text::new(body, style.body.clone())) + .contained() + .with_style(style.container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. + }) + .into_any() + } } fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 39543c8def..7bcaa5be66 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1937,6 +1937,8 @@ impl CollabPanel { is_dragged_over = true; } + let has_messages_notification = channel.unseen_message_id.is_some(); + MouseEventHandler::new::(ix, cx, |state, cx| { let row_hovered = state.hovered(); @@ -2022,24 +2024,33 @@ impl CollabPanel { .flex(1., true) }) .with_child( - MouseEventHandler::new::(ix, cx, move |_, _| { + MouseEventHandler::new::(ix, cx, move |mouse_state, _| { + let container_style = collab_theme + .disclosure + .button + .style_for(mouse_state) + .container; + if channel.unseen_message_id.is_some() { Svg::new("icons/conversations.svg") .with_color(collab_theme.channel_note_active_color) .constrained() .with_width(collab_theme.channel_hash.width) + .contained() + .with_style(container_style) + .with_uniform_padding(4.) .into_any() } else if row_hovered { Svg::new("icons/conversations.svg") .with_color(collab_theme.channel_hash.color) .constrained() .with_width(collab_theme.channel_hash.width) + .contained() + .with_style(container_style) + .with_uniform_padding(4.) .into_any() } else { - Empty::new() - .constrained() - .with_width(collab_theme.channel_hash.width) - .into_any() + Empty::new().into_any() } }) .on_click(MouseButton::Left, move |_, this, cx| { @@ -2056,7 +2067,12 @@ impl CollabPanel { .with_margin_right(4.), ) .with_child( - MouseEventHandler::new::(ix, cx, move |_, cx| { + MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { + let container_style = collab_theme + .disclosure + .button + .style_for(mouse_state) + .container; if row_hovered || channel.unseen_note_version.is_some() { Svg::new("icons/file.svg") .with_color(if channel.unseen_note_version.is_some() { @@ -2067,6 +2083,8 @@ impl CollabPanel { .constrained() .with_width(collab_theme.channel_hash.width) .contained() + .with_style(container_style) + .with_uniform_padding(4.) .with_margin_right(collab_theme.channel_hash.container.margin.left) .with_tooltip::( ix as usize, @@ -2076,13 +2094,16 @@ impl CollabPanel { cx, ) .into_any() - } else { + } else if has_messages_notification { Empty::new() .constrained() .with_width(collab_theme.channel_hash.width) .contained() + .with_uniform_padding(4.) .with_margin_right(collab_theme.channel_hash.container.margin.left) .into_any() + } else { + Empty::new().into_any() } }) .on_click(MouseButton::Left, move |_, this, cx| { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9b7c0309c6..63241668c4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -635,6 +635,8 @@ pub struct ChatPanel { pub channel_select: ChannelSelect, pub input_editor: FieldEditor, pub message: ChatMessage, + pub continuation_message: ChatMessage, + pub last_message_bottom_spacing: f32, pub pending_message: ChatMessage, pub sign_in_prompt: Interactive, pub icon_button: Interactive, diff --git a/styles/src/style_tree/chat_panel.ts b/styles/src/style_tree/chat_panel.ts index 9efa084456..466d25f43d 100644 --- a/styles/src/style_tree/chat_panel.ts +++ b/styles/src/style_tree/chat_panel.ts @@ -87,7 +87,19 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", { weight: "bold" }), }, timestamp: text(layer, "sans", "base", "disabled"), - margin: { bottom: SPACING } + margin: { top: SPACING } + }, + last_message_bottom_spacing: SPACING, + continuation_message: { + body: text(layer, "sans", "base"), + sender: { + margin: { + right: 8, + }, + ...text(layer, "sans", "base", { weight: "bold" }), + }, + timestamp: text(layer, "sans", "base", "disabled"), + }, pending_message: { body: text(layer, "sans", "base"), diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index 71057c67ea..8dc22eec31 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -21,6 +21,7 @@ export default function contacts_panel(): any { ...text(theme.lowest, "sans", "base"), button: icon_button({ variant: "ghost" }), spacing: 4, + padding: 4, }, } } From fc3d754aeae69f71dd59deacba95987f31064586 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 4 Oct 2023 15:15:25 -0700 Subject: [PATCH 05/31] Remove old code from notes icon click handler (#3085) Release Notes: - Fix clicking the notes icon when people are in the channel (preview only) --- crates/collab_ui/src/collab_panel.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 7bcaa5be66..66913c2da7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2107,13 +2107,7 @@ impl CollabPanel { } }) .on_click(MouseButton::Left, move |_, this, cx| { - let participants = - this.channel_store.read(cx).channel_participants(channel_id); - if is_active || participants.is_empty() { - this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - } else { - this.join_channel(channel_id, cx); - }; + this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); }), ) .align_children_center() From cc4fb1c1b5c72765f4c13f951c9cfd2cd8983157 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 15:44:17 -0700 Subject: [PATCH 06/31] zed 0.107.1 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35f3285ebd..41a2d88d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10063,7 +10063,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.0" +version = "0.107.1" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bd6a85e3aa..5e00ce8c41 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.0" +version = "0.107.1" publish = false [lib] From 4edd0365a10ecc4bf0d22708c242a560a16e3cd4 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 15:45:55 -0700 Subject: [PATCH 07/31] collab 0.22.2 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41a2d88d95..0e3d694229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,7 +1467,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.22.1" +version = "0.22.2" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index ecc4b57b12..6b98c74c03 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.22.1" +version = "0.22.2" publish = false [[bin]] From c41a3ec01b4381a5d72d164f05c968fe9c407502 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 5 Oct 2023 14:57:08 -0400 Subject: [PATCH 08/31] Add session id (#3090) Release Notes: - N/A --- crates/client/src/telemetry.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 38a4115ddd..0f753679e1 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -8,6 +8,7 @@ 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, @@ -18,7 +19,8 @@ pub struct Telemetry { #[derive(Default)] struct TelemetryState { metrics_id: Option>, // Per logged-in user - installation_id: Option>, // Per app installation + installation_id: Option>, // Per app installation (different for dev, preview, and stable) + session_id: String, // Per app launch app_version: Option>, release_channel: Option<&'static str>, os_name: &'static str, @@ -41,6 +43,7 @@ lazy_static! { struct ClickhouseEventRequestBody { token: &'static str, installation_id: Option>, + session_id: String, is_staff: Option, app_version: Option>, os_name: &'static str, @@ -131,6 +134,7 @@ impl Telemetry { release_channel, installation_id: None, metrics_id: None, + session_id: Uuid::new_v4().to_string(), clickhouse_events_queue: Default::default(), flush_clickhouse_events_task: Default::default(), log_file: None, @@ -285,6 +289,7 @@ impl Telemetry { &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, From 4ca2645a5407208804934cd7d0e7ecf62f757e6c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 15:16:58 -0700 Subject: [PATCH 09/31] Fix bugs in handling mutual following (#3091) This fixes some bugs in our following logic, due to our attempts to prevent infinite loops when two people follow each other. * Propagate all of leader's views to a new follower, even if those views were originally created by that follower. * Propagate active view changes to followers, even if the active view is following that follower. * Avoid redundant active view updates on the client. Release Notes: - Fixed bugs where it was impossible to follow someone into a view that they previously following you into. --- crates/collab/src/rpc.rs | 16 +- crates/collab/src/tests/following_tests.rs | 549 ++++++++++++++++++--- crates/workspace/src/workspace.rs | 18 +- 3 files changed, 491 insertions(+), 92 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6171803341..5eb434e167 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1906,13 +1906,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 { @@ -1973,14 +1970,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..3a489b9ac3 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::{ @@ -724,10 +725,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 +736,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 +753,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 +762,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 +1429,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 +1641,59 @@ 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 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/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f7bb409229..801c45fa89 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -573,6 +573,7 @@ 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)>, @@ -786,6 +787,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(), @@ -2862,6 +2864,7 @@ impl Workspace { cx.notify(); + self.last_active_view_id = active_view_id.clone(); proto::FollowResponse { active_view_id, views: self @@ -3028,7 +3031,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 +3049,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( From 6f4dee5b1d12ffe84e9b30e7f1e0a3e94387a0ec Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 5 Oct 2023 14:30:12 -0700 Subject: [PATCH 10/31] Add markdown parsing to channel chat (#3088) TODO: - [x] Add markdown rendering to channel chat - [x] Unify (?) rendering logic between hover popover and chat - [x] ~~Determine how to deal with document-oriented markdown like `#`~~ Unimportant until we want to do something special with `#channel` - [x] Tidy up spacing and styles in chat panel Release Notes: - Added markdown rendering to channel chat - Improved channel chat message style - Fixed a bug where long chat messages would not soft wrap --- Cargo.lock | 20 ++ Cargo.toml | 1 + crates/channel/src/channel_chat.rs | 2 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/chat_panel.rs | 287 ++++++++++++++++--------- crates/collab_ui/src/collab_panel.rs | 6 +- crates/editor/Cargo.toml | 1 + crates/editor/src/hover_popover.rs | 305 ++++----------------------- crates/rich_text/Cargo.toml | 30 +++ crates/rich_text/src/rich_text.rs | 287 +++++++++++++++++++++++++ crates/theme/src/theme.rs | 4 +- styles/src/style_tree/chat_panel.ts | 69 +++++- 12 files changed, 636 insertions(+), 377 deletions(-) create mode 100644 crates/rich_text/Cargo.toml create mode 100644 crates/rich_text/src/rich_text.rs diff --git a/Cargo.lock b/Cargo.lock index 0e3d694229..d0f311cd60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,6 +1563,7 @@ dependencies = [ "postage", "project", "recent_projects", + "rich_text", "schemars", "serde", "serde_derive", @@ -2405,6 +2406,7 @@ dependencies = [ "project", "pulldown-cmark", "rand 0.8.5", + "rich_text", "rpc", "schemars", "serde", @@ -6242,6 +6244,24 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rich_text" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui", + "language", + "lazy_static", + "pulldown-cmark", + "smallvec", + "smol", + "sum_tree", + "theme", + "util", +] + [[package]] name = "ring" version = "0.16.20" diff --git a/Cargo.toml b/Cargo.toml index 05a013a4e0..7dae3bd81f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ members = [ "crates/sqlez", "crates/sqlez_macros", "crates/feature_flags", + "crates/rich_text", "crates/storybook", "crates/sum_tree", "crates/terminal", diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 29a260ea7e..734182886b 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -36,7 +36,7 @@ pub struct ChannelMessage { pub nonce: u128, } -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ChannelMessageId { Saved(u64), Pending(usize), diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index b6e45471f1..98790778c9 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } +rich_text = { path = "../rich_text" } picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 41bc5fbd08..b446521c5a 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -3,6 +3,7 @@ use anyhow::Result; use call::ActiveCall; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; use client::Client; +use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; @@ -12,12 +13,13 @@ use gpui::{ platform::{CursorStyle, MouseButton}, serde_json, views::{ItemType, Select, SelectStyle}, - AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::language_settings::SoftWrap; +use language::{language_settings::SoftWrap, LanguageRegistry}; use menu::Confirm; use project::Fs; +use rich_text::RichText; use serde::{Deserialize, Serialize}; use settings::SettingsStore; use std::sync::Arc; @@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel"; pub struct ChatPanel { client: Arc, channel_store: ModelHandle, + languages: Arc, active_chat: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, @@ -47,6 +50,7 @@ pub struct ChatPanel { subscriptions: Vec, workspace: WeakViewHandle, has_focus: bool, + markdown_data: HashMap, } #[derive(Serialize, Deserialize)] @@ -78,6 +82,7 @@ impl ChatPanel { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = workspace.app_state().channel_store.clone(); + let languages = workspace.app_state().languages.clone(); let input_editor = cx.add_view(|cx| { let mut editor = Editor::auto_height( @@ -130,6 +135,7 @@ impl ChatPanel { fs, client, channel_store, + languages, active_chat: Default::default(), pending_serialization: Task::ready(None), @@ -142,6 +148,7 @@ impl ChatPanel { workspace: workspace_handle, active: false, width: None, + markdown_data: Default::default(), }; let mut old_dock_position = this.position(cx); @@ -178,6 +185,25 @@ impl ChatPanel { }) .detach(); + let markdown = this.languages.language_for_name("Markdown"); + cx.spawn(|this, mut cx| async move { + let markdown = markdown.await?; + + this.update(&mut cx, |this, cx| { + this.input_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multi_buffer, cx| { + multi_buffer + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx)) + }) + }) + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + this }) } @@ -328,7 +354,7 @@ impl ChatPanel { messages.flex(1., true).into_any() } - fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { let (message, is_continuation, is_last) = { let active_chat = self.active_chat.as_ref().unwrap().0.read(cx); let last_message = active_chat.message(ix.saturating_sub(1)); @@ -337,15 +363,21 @@ impl ChatPanel { && this_message.sender.id == last_message.sender.id; ( - active_chat.message(ix), + active_chat.message(ix).clone(), is_continuation, active_chat.message_count() == ix + 1, ) }; + let is_pending = message.is_pending(); + let text = self + .markdown_data + .entry(message.id) + .or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None)); + let now = OffsetDateTime::now_utc(); let theme = theme::current(cx); - let style = if message.is_pending() { + let style = if is_pending { &theme.chat_panel.pending_message } else if is_continuation { &theme.chat_panel.continuation_message @@ -361,106 +393,90 @@ impl ChatPanel { None }; - enum DeleteMessage {} - - let body = message.body.clone(); - if is_continuation { - Flex::row() - .with_child(Text::new(body, style.body.clone())) - .with_children(message_id_to_remove.map(|id| { - MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { - let button_style = theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() + enum MessageBackgroundHighlight {} + MouseEventHandler::new::(ix, cx, |state, cx| { + let container = style.container.style_for(state); + if is_continuation { + Flex::row() + .with_child( + text.element( + theme.editor.syntax.clone(), + style.body.clone(), + theme.editor.document_highlight_read_background, + cx, + ) + .flex(1., true), + ) + .with_child(render_remove(message_id_to_remove, cx, &theme)) + .contained() + .with_style(*container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - })) - .contained() - .with_style(style.container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } else { - Flex::column() - .with_child( - Flex::row() - .with_child( - message - .sender - .avatar - .clone() - .map(|avatar| { - Image::from_data(avatar) - .with_style(theme.collab_panel.channel_avatar) - .into_any() - }) - .unwrap_or_else(|| { - Empty::new() - .constrained() - .with_width( - theme.collab_panel.channel_avatar.width.unwrap_or(12.), + .into_any() + } else { + Flex::column() + .with_child( + Flex::row() + .with_child( + Flex::row() + .with_child(render_avatar( + message.sender.avatar.clone(), + &theme, + )) + .with_child( + Label::new( + message.sender.github_login.clone(), + style.sender.text.clone(), ) - .into_any() - }) - .contained() - .with_margin_right(4.), - ) - .with_child( - Label::new( - message.sender.github_login.clone(), - style.sender.text.clone(), + .contained() + .with_style(style.sender.container), + ) + .with_child( + Label::new( + format_timestamp( + message.timestamp, + now, + self.local_timezone, + ), + style.timestamp.text.clone(), + ) + .contained() + .with_style(style.timestamp.container), + ) + .align_children_center() + .flex(1., true), ) - .contained() - .with_style(style.sender.container), - ) - .with_child( - Label::new( - format_timestamp(message.timestamp, now, self.local_timezone), - style.timestamp.text.clone(), + .with_child(render_remove(message_id_to_remove, cx, &theme)) + .align_children_center(), + ) + .with_child( + Flex::row() + .with_child( + text.element( + theme.editor.syntax.clone(), + style.body.clone(), + theme.editor.document_highlight_read_background, + cx, + ) + .flex(1., true), ) - .contained() - .with_style(style.timestamp.container), - ) - .with_children(message_id_to_remove.map(|id| { - MouseEventHandler::new::( - id as usize, - cx, - |mouse_state, _| { - let button_style = - theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() - }, - ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - })) - .align_children_center(), - ) - .with_child(Text::new(body, style.body.clone())) - .contained() - .with_style(style.container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } + // Add a spacer to make everything line up + .with_child(render_remove(None, cx, &theme)), + ) + .contained() + .with_style(*container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. + }) + .into_any() + } + }) + .into_any() } fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { @@ -634,6 +650,7 @@ impl ChatPanel { cx.spawn(|this, mut cx| async move { let chat = open_chat.await?; this.update(&mut cx, |this, cx| { + this.markdown_data = Default::default(); this.set_active_chat(chat, cx); }) }) @@ -658,6 +675,72 @@ impl ChatPanel { } } +fn render_avatar(avatar: Option>, theme: &Arc) -> AnyElement { + let avatar_style = theme.chat_panel.avatar; + + avatar + .map(|avatar| { + Image::from_data(avatar) + .with_style(avatar_style.image) + .aligned() + .contained() + .with_corner_radius(avatar_style.outer_corner_radius) + .constrained() + .with_width(avatar_style.outer_width) + .with_height(avatar_style.outer_width) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width(avatar_style.outer_width) + .into_any() + }) + .contained() + .with_style(theme.chat_panel.avatar_container) + .into_any() +} + +fn render_remove( + message_id_to_remove: Option, + cx: &mut ViewContext<'_, '_, ChatPanel>, + theme: &Arc, +) -> AnyElement { + enum DeleteMessage {} + + message_id_to_remove + .map(|id| { + MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { + let button_style = theme.chat_panel.icon_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .into_any() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_message(id, cx); + }) + .flex_float() + .into_any() + }) + .unwrap_or_else(|| { + let style = theme.chat_panel.icon_button.default; + + Empty::new() + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_uniform_padding(2.) + .flex_float() + .into_any() + }) +} + impl Entity for ChatPanel { type Event = Event; } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 66913c2da7..951c8bf70c 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1976,11 +1976,7 @@ impl CollabPanel { .left() .with_tooltip::( ix, - if is_active { - "Open channel notes" - } else { - "Join channel" - }, + "Join channel", None, theme.tooltip.clone(), cx, diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index b0f8323a76..2c3d6227a9 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -36,6 +36,7 @@ language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } rpc = { path = "../rpc" } +rich_text = { path = "../rich_text" } settings = { path = "../settings" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index f460b18bce..553cb321c3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -8,12 +8,12 @@ use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, - fonts::{HighlightStyle, Underline, Weight}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; +use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -346,158 +346,25 @@ fn show_hover( } fn render_blocks( - theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, language: Option<&Arc>, - style: &EditorStyle, -) -> RenderedInfo { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut region_ranges = Vec::new(); - let mut regions = Vec::new(); +) -> RichText { + let mut data = RichText { + text: Default::default(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }; for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut text, &mut Vec::new()); - text.push_str(&block.text); + new_paragraph(&mut data.text, &mut Vec::new()); + data.text.push_str(&block.text); } HoverBlockKind::Markdown => { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; - - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut link_url = None; - let mut current_language = None; - let mut list_stack = Vec::new(); - - for event in Parser::new_ext(&block.text, Options::all()) { - let prev_len = text.len(); - match event { - Event::Text(t) => { - if let Some(language) = ¤t_language { - render_code( - &mut text, - &mut highlights, - t.as_ref(), - language, - style, - ); - } else { - text.push_str(t.as_ref()); - - let mut style = HighlightStyle::default(); - if bold_depth > 0 { - style.weight = Some(Weight::BOLD); - } - if italic_depth > 0 { - style.italic = Some(true); - } - if let Some(link_url) = link_url.clone() { - region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { - link_url: Some(link_url), - code: false, - }); - style.underline = Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }); - } - - if style != HighlightStyle::default() { - let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == prev_len && last_style == &style { - last_range.end = text.len(); - new_highlight = false; - } - } - if new_highlight { - highlights.push((prev_len..text.len(), style)); - } - } - } - } - Event::Code(t) => { - text.push_str(t.as_ref()); - region_ranges.push(prev_len..text.len()); - if link_url.is_some() { - highlights.push(( - prev_len..text.len(), - HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }, - )); - } - regions.push(RenderedRegion { - code: true, - link_url: link_url.clone(), - }); - } - Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), - Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); - bold_depth += 1; - } - Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); - current_language = if let CodeBlockKind::Fenced(language) = kind { - language_registry - .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) - } else { - language.cloned() - } - } - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Link(_, url, _) => link_url = Some(url.to_string()), - Tag::List(number) => { - list_stack.push((number, false)); - } - Tag::Item => { - let len = list_stack.len(); - if let Some((list_number, has_content)) = list_stack.last_mut() { - *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - for _ in 0..len - 1 { - text.push_str(" "); - } - if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); - *number += 1; - *has_content = false; - } else { - text.push_str("- "); - } - } - } - _ => {} - }, - Event::End(tag) => match tag { - Tag::Heading(_, _, _) => bold_depth -= 1, - Tag::CodeBlock(_) => current_language = None, - Tag::Emphasis => italic_depth -= 1, - Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => link_url = None, - Tag::List(_) => drop(list_stack.pop()), - _ => {} - }, - Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push(' '), - _ => {} - } - } + render_markdown_mut(&block.text, language_registry, language, &mut data) } HoverBlockKind::Code { language } => { if let Some(language) = language_registry @@ -505,62 +372,17 @@ fn render_blocks( .now_or_never() .and_then(Result::ok) { - render_code(&mut text, &mut highlights, &block.text, &language, style); + render_code(&mut data.text, &mut data.highlights, &block.text, &language); } else { - text.push_str(&block.text); + data.text.push_str(&block.text); } } } } - RenderedInfo { - theme_id, - text: text.trim().to_string(), - highlights, - region_ranges, - regions, - } -} + data.text = data.text.trim().to_string(); -fn render_code( - text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, - content: &str, - language: &Arc, - style: &EditorStyle, -) { - let prev_len = text.len(); - text.push_str(content); - for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if let Some(style) = highlight_id.style(&style.syntax) { - highlights.push((prev_len + range.start..prev_len + range.end, style)); - } - } -} - -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { - let mut is_subsequent_paragraph_of_list = false; - if let Some((_, has_content)) = list_stack.last_mut() { - if *has_content { - is_subsequent_paragraph_of_list = true; - } else { - *has_content = true; - return; - } - } - - if !text.is_empty() { - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - } - for _ in 0..list_stack.len().saturating_sub(1) { - text.push_str(" "); - } - if is_subsequent_paragraph_of_list { - text.push_str(" "); - } + data } #[derive(Default)] @@ -623,22 +445,7 @@ pub struct InfoPopover { symbol_range: RangeInEditor, pub blocks: Vec, language: Option>, - rendered_content: Option, -} - -#[derive(Debug, Clone)] -struct RenderedInfo { - theme_id: usize, - text: String, - highlights: Vec<(Range, HighlightStyle)>, - region_ranges: Vec>, - regions: Vec, -} - -#[derive(Debug, Clone)] -struct RenderedRegion { - code: bool, - link_url: Option, + rendered_content: Option, } impl InfoPopover { @@ -647,63 +454,24 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - if let Some(rendered) = &self.rendered_content { - if rendered.theme_id != style.theme_id { - self.rendered_content = None; - } - } - let rendered_content = self.rendered_content.get_or_insert_with(|| { render_blocks( - style.theme_id, &self.blocks, self.project.read(cx).languages(), self.language.as_ref(), - style, ) }); - MouseEventHandler::new::(0, cx, |_, cx| { - let mut region_id = 0; - let view_id = cx.view_id(); - + MouseEventHandler::new::(0, cx, move |_, cx| { let code_span_background_color = style.document_highlight_read_background; - let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) - .with_child( - Text::new(rendered_content.text.clone(), style.text.clone()) - .with_highlights(rendered_content.highlights.clone()) - .with_custom_runs( - rendered_content.region_ranges.clone(), - move |ix, bounds, cx| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - cx.scene().push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - cx.scene().push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::( - MouseButton::Left, - move |_, _, cx| cx.platform().open_url(&url), - ), - ); - } - if region.code { - cx.scene().push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }, - ) - .with_soft_wrap(true), - ) + .with_child(rendered_content.element( + style.syntax.clone(), + style.text.clone(), + code_span_background_color, + cx, + )) .contained() .with_style(style.hover_popover.container) }) @@ -799,11 +567,12 @@ mod tests { InlayId, }; use collections::BTreeSet; - use gpui::fonts::Weight; + use gpui::fonts::{HighlightStyle, Underline, Weight}; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; + use rich_text::Highlight; use smol::stream::StreamExt; use unindent::Unindent; use util::test::marked_text_ranges; @@ -1014,7 +783,7 @@ mod tests { .await; cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, cx| { + cx.editor(|editor, _| { let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; assert_eq!( blocks, @@ -1024,8 +793,7 @@ mod tests { }], ); - let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(&blocks, &Default::default(), None); assert_eq!( rendered.text, code_str.trim(), @@ -1217,7 +985,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(&blocks, &Default::default(), None); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -1228,8 +996,21 @@ mod tests { rendered.text, expected_text, "wrong text for input {blocks:?}" ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let style = match highlight { + Highlight::Id(id) => id.style(&style.syntax)?, + Highlight::Highlight(style) => style.clone(), + }; + Some((range.clone(), style)) + }) + .collect(); + assert_eq!( - rendered.highlights, expected_highlights, + rendered_highlights, expected_highlights, "wrong highlights for input {blocks:?}" ); } diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml new file mode 100644 index 0000000000..3d2c25406d --- /dev/null +++ b/crates/rich_text/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "rich_text" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/rich_text.rs" +doctest = false + +[features] +test-support = [ + "gpui/test-support", + "util/test-support", +] + + +[dependencies] +collections = { path = "../collections" } +gpui = { path = "../gpui" } +sum_tree = { path = "../sum_tree" } +theme = { path = "../theme" } +language = { path = "../language" } +util = { path = "../util" } +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } +smallvec.workspace = true +smol.workspace = true diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs new file mode 100644 index 0000000000..72c7bdf6c1 --- /dev/null +++ b/crates/rich_text/src/rich_text.rs @@ -0,0 +1,287 @@ +use std::{ops::Range, sync::Arc}; + +use futures::FutureExt; +use gpui::{ + color::Color, + elements::Text, + fonts::{HighlightStyle, TextStyle, Underline, Weight}, + platform::{CursorStyle, MouseButton}, + AnyElement, CursorRegion, Element, MouseRegion, ViewContext, +}; +use language::{HighlightId, Language, LanguageRegistry}; +use theme::SyntaxTheme; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Highlight { + Id(HighlightId), + Highlight(HighlightStyle), +} + +#[derive(Debug, Clone)] +pub struct RichText { + pub text: String, + pub highlights: Vec<(Range, Highlight)>, + pub region_ranges: Vec>, + pub regions: Vec, +} + +#[derive(Debug, Clone)] +pub struct RenderedRegion { + code: bool, + link_url: Option, +} + +impl RichText { + pub fn element( + &self, + syntax: Arc, + style: TextStyle, + code_span_background_color: Color, + cx: &mut ViewContext, + ) -> AnyElement { + let mut region_id = 0; + let view_id = cx.view_id(); + + let regions = self.regions.clone(); + + enum Markdown {} + Text::new(self.text.clone(), style.clone()) + .with_highlights( + self.highlights + .iter() + .filter_map(|(range, highlight)| { + let style = match highlight { + Highlight::Id(id) => id.style(&syntax)?, + Highlight::Highlight(style) => style.clone(), + }; + Some((range.clone(), style)) + }) + .collect::>(), + ) + .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + cx.scene().push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + cx.scene().push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + cx.scene().push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) + .into_any() + } +} + +pub fn render_markdown_mut( + block: &str, + language_registry: &Arc, + language: Option<&Arc>, + data: &mut RichText, +) { + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&block, Options::all()) { + let prev_len = data.text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code(&mut data.text, &mut data.highlights, t.as_ref(), language); + } else { + data.text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if let Some(link_url) = link_url.clone() { + data.region_ranges.push(prev_len..data.text.len()); + data.regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = data.highlights.last_mut() { + if last_range.end == prev_len + && last_style == &Highlight::Highlight(style) + { + last_range.end = data.text.len(); + new_highlight = false; + } + } + if new_highlight { + data.highlights + .push((prev_len..data.text.len(), Highlight::Highlight(style))); + } + } + } + } + Event::Code(t) => { + data.text.push_str(t.as_ref()); + data.region_ranges.push(prev_len..data.text.len()); + if link_url.is_some() { + data.highlights.push(( + prev_len..data.text.len(), + Highlight::Highlight(HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }), + )); + } + data.regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); + } + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack), + Tag::Heading(_, _, _) => { + new_paragraph(&mut data.text, &mut list_stack); + bold_depth += 1; + } + Tag::CodeBlock(kind) => { + new_paragraph(&mut data.text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + language.cloned() + } + } + Tag::Emphasis => italic_depth += 1, + Tag::Strong => bold_depth += 1, + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + Tag::List(number) => { + list_stack.push((number, false)); + } + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !data.text.is_empty() && !data.text.ends_with('\n') { + data.text.push('\n'); + } + for _ in 0..len - 1 { + data.text.push_str(" "); + } + if let Some(number) = list_number { + data.text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + data.text.push_str("- "); + } + } + } + _ => {} + }, + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + Event::HardBreak => data.text.push('\n'), + Event::SoftBreak => data.text.push(' '), + _ => {} + } + } +} + +pub fn render_markdown( + block: String, + language_registry: &Arc, + language: Option<&Arc>, +) -> RichText { + let mut data = RichText { + text: Default::default(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }; + + render_markdown_mut(&block, language_registry, language, &mut data); + + data.text = data.text.trim().to_string(); + + data +} + +pub fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, Highlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + highlights.push(( + prev_len + range.start..prev_len + range.end, + Highlight::Id(highlight_id), + )); + } +} + +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 63241668c4..e534ba4260 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -634,6 +634,8 @@ pub struct ChatPanel { pub list: ContainerStyle, pub channel_select: ChannelSelect, pub input_editor: FieldEditor, + pub avatar: AvatarStyle, + pub avatar_container: ContainerStyle, pub message: ChatMessage, pub continuation_message: ChatMessage, pub last_message_bottom_spacing: f32, @@ -645,7 +647,7 @@ pub struct ChatPanel { #[derive(Deserialize, Default, JsonSchema)] pub struct ChatMessage { #[serde(flatten)] - pub container: ContainerStyle, + pub container: Interactive, pub body: TextStyle, pub sender: ContainedText, pub timestamp: ContainedText, diff --git a/styles/src/style_tree/chat_panel.ts b/styles/src/style_tree/chat_panel.ts index 466d25f43d..829540de30 100644 --- a/styles/src/style_tree/chat_panel.ts +++ b/styles/src/style_tree/chat_panel.ts @@ -5,6 +5,7 @@ import { } from "./components" import { icon_button } from "../component/icon_button" import { useTheme } from "../theme" +import { interactive } from "../element" export default function chat_panel(): any { const theme = useTheme() @@ -27,11 +28,23 @@ export default function chat_panel(): any { return { background: background(layer), - list: { - margin: { - left: SPACING, - right: SPACING, + avatar: { + icon_width: 24, + icon_height: 24, + corner_radius: 4, + outer_width: 24, + outer_corner_radius: 16, + }, + avatar_container: { + padding: { + right: 6, + left: 2, + top: 2, + bottom: 2, } + }, + list: { + }, channel_select: { header: { @@ -79,6 +92,22 @@ export default function chat_panel(): any { }, }, message: { + ...interactive({ + base: { + margin: { top: SPACING }, + padding: { + top: 4, + bottom: 4, + left: SPACING / 2, + right: SPACING / 3, + } + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), body: text(layer, "sans", "base"), sender: { margin: { @@ -87,7 +116,6 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", { weight: "bold" }), }, timestamp: text(layer, "sans", "base", "disabled"), - margin: { top: SPACING } }, last_message_bottom_spacing: SPACING, continuation_message: { @@ -99,7 +127,21 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", { weight: "bold" }), }, timestamp: text(layer, "sans", "base", "disabled"), - + ...interactive({ + base: { + padding: { + top: 4, + bottom: 4, + left: SPACING / 2, + right: SPACING / 3, + } + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), }, pending_message: { body: text(layer, "sans", "base"), @@ -110,6 +152,21 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", "disabled"), }, timestamp: text(layer, "sans", "base"), + ...interactive({ + base: { + padding: { + top: 4, + bottom: 4, + left: SPACING / 2, + right: SPACING / 3, + } + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), }, sign_in_prompt: { default: text(layer, "sans", "base"), From 6098f94dc1e01d9ca6970cc2855379b33228eb00 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 16:03:07 -0700 Subject: [PATCH 11/31] zed 0.107.2 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0f311cd60..148c369301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10083,7 +10083,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.1" +version = "0.107.2" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5e00ce8c41..6743d9ea23 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.1" +version = "0.107.2" publish = false [lib] From df63290a32c04a1e75fb16f7597b908b81f1a8d3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 16:28:23 -0700 Subject: [PATCH 12/31] Fix panic when immediately closing a window while opening paths (#3092) Fixes this panic that I've been seeing in Slack: [example](https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1696530575535779) ``` thread 'main' panicked at 'assertion failed: opened_items.len() == project_paths_to_open.len()' crates/workspace/src/workspace.rs:3628 ::create ::new Zed::init_panic_hook::{closure#0} std::panicking::rust_panic_with_hook std::panicking::begin_panic_handler::{{closure}} std::sys_common::backtrace::__rust_end_short_backtrace _rust_begin_unwind core::panicking::panic_fmt core::panicking::panic ::new_local::{closure#0}::{closure#0} ``` I believe it was caused by a window being closed immediately, while it was still loading some paths. There was a mismatch in expectation between the `workspace::open_items` function (which contains this assertion), and the `Workspace::load_workspace` method. That later method can return an empty vector if the workspace handle is dropped while it is executing. Release Notes: - Fixed a crash when closing a Zed window immediately after opening it --- crates/workspace/src/workspace.rs | 246 ++++++++++++++---------------- 1 file changed, 117 insertions(+), 129 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 801c45fa89..6496d81349 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -79,7 +79,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! { @@ -936,7 +936,8 @@ impl Workspace { app_state, cx, ) - .await; + .await + .unwrap_or_default(); (workspace, opened_items) }) @@ -3400,140 +3401,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) }) } @@ -3607,7 +3592,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 { @@ -3625,16 +3610,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)| { @@ -3687,7 +3675,7 @@ async fn open_items( } } - opened_items + Ok(opened_items) } fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { From 62358b9bce65273acbf3f282dad9af57f6e1136b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 6 Oct 2023 13:32:45 -0400 Subject: [PATCH 13/31] Add session id to panic events (#3098) Release Notes: - N/A --- crates/client/src/telemetry.rs | 45 ++++++++++++++++++---------------- crates/zed/src/main.rs | 9 ++++--- 2 files changed, 30 insertions(+), 24 deletions(-) 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/zed/src/main.rs b/crates/zed/src/main.rs index d6f3be2b46..704eef464c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -74,7 +74,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); @@ -177,7 +178,7 @@ fn main() { }) .detach(); - client.telemetry().start(installation_id, cx); + client.telemetry().start(installation_id, session_id, cx); let app_state = Arc::new(AppState { languages, @@ -402,6 +403,7 @@ struct Panic { panicked_on: u128, #[serde(skip_serializing_if = "Option::is_none")] installation_id: Option, + session_id: String, } #[derive(Serialize)] @@ -412,7 +414,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(); @@ -490,6 +492,7 @@ 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 { From f92d44ed70ba74fd85e94d614b3aa13b63c4589f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 6 Oct 2023 14:20:06 -0400 Subject: [PATCH 14/31] Use display name for release channel in panic events (#3101) This was a mistake from long ago - something I've been meaning to fix for a long time. All other events use `display_name()`, but panic events, which leads to mistakes when filtering out `Zed Dev`, which isn't the format that `dev_name()` returns. I'm adding a fix to zed.dev as well: - https://github.com/zed-industries/zed.dev/pull/393 so that the values are adjusted for all clients, not just ones with this fix. I will correct the data in clickhouse, and adjust the queries in metabase. Release Notes: - N/A --- crates/zed/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 704eef464c..12ae0f2ffc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -479,7 +479,7 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin 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() From b865efe3a34cc5b1739e72ca86b1ed9477ea48bc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 13:50:51 -0700 Subject: [PATCH 15/31] Fix bug that allowed following multiple people in one pane (#3108) I've also simplified the representation of a workspace's leaders, so that it encodes in the type that there can only be one leader per pane. Release Notes: - Fixed a bug where you could accidentally follow multiple collaborators in one pane at the same time. --- crates/collab/src/tests/following_tests.rs | 156 +++++++++------------ crates/workspace/src/pane_group.rs | 33 ++--- crates/workspace/src/workspace.rs | 137 +++++++++--------- 3 files changed, 147 insertions(+), 179 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 3a489b9ac3..f3857e3db3 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -184,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. @@ -207,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() @@ -256,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. @@ -278,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. @@ -1667,6 +1627,30 @@ struct PaneSummary { 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(); 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 6496d81349..b2c9863a6c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -578,7 +578,7 @@ pub struct Workspace { 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)>, @@ -603,10 +603,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>, } @@ -795,7 +794,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, @@ -2513,13 +2512,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(); } @@ -2532,10 +2534,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(); @@ -2550,9 +2557,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)?) @@ -2647,12 +2653,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; } } @@ -2665,36 +2669,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 { @@ -2917,8 +2922,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)?) @@ -2940,8 +2945,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)); @@ -2954,10 +2959,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() })?; @@ -3016,11 +3020,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); @@ -3077,15 +3077,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<()> { @@ -3113,7 +3105,10 @@ impl Workspace { } }; - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + for (pane, state) in &self.follower_states { + if state.leader_id != leader_id { + continue; + } if leader_in_this_app { let item = state .active_view_id @@ -3811,7 +3806,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 From 9d76ba445fcd2e9fddbe667a9da86711d6c17295 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Oct 2023 23:16:03 +0200 Subject: [PATCH 16/31] Detect file paths that end with `:` (#3109) New rustc messages look like ``` thread 'tests::test_history_items_vs_very_good_external_match' panicked at crates/file_finder/src/file_finder.rs:1902:13: assertion `left == right` failed: Only one history item contains collab_ui, it should be present and others should be filtered out left: 0 right: 1 ``` now and we fail to parse that `13:` bit properly, fix that. One caveat is that we highlight the entire word including the trailing `:`: image this is unfortunate, but better than nothing (as now). This is due to the fact, that we detect words with regex inside the `terminal.rs` and send events to other place that's able to check paths for existence (and whether that's a path at all), currently there's no way to detect a path and sanitize it in `terminal.rs` Release Notes: - N/A --- crates/util/src/paths.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 { From 78fe18acbc357fcb0e31fe7fbd514e2dbbd57c97 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 15:25:22 -0700 Subject: [PATCH 17/31] More small following-related fixes (#3110) --- crates/call/src/room.rs | 33 +++++++++++++++++-------------- crates/client/src/client.rs | 18 ++++++++++++++++- crates/workspace/src/workspace.rs | 15 ++++++++------ 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 72db174d72..43354fd5a2 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -600,27 +600,30 @@ impl Room { /// Returns the most 'active' projects, defined as most people in the project pub fn most_active_project(&self) -> 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); } } - let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); - pairs.sort_by_key(|(_, count)| *count as i32); - - pairs - .first() - .map(|(project_id, _)| (*project_id, hosts[&project_id])) + 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( @@ -686,6 +689,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 @@ -736,8 +740,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 { @@ -749,9 +754,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/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/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b2c9863a6c..2aa9b008da 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3109,16 +3109,19 @@ impl Workspace { if state.leader_id != leader_id { continue; } - 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 { + 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))); From 91ee6b509f284e2c6f59bff0a71413f51ede1b4c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 15:28:46 -0700 Subject: [PATCH 18/31] zed 0.107.3 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 148c369301..629aec0776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10083,7 +10083,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.2" +version = "0.107.3" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6743d9ea23..bf00cf4483 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.2" +version = "0.107.3" publish = false [lib] From a15b9a55d2e879aff8a8d17564eca39cbc065719 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 10 Oct 2023 00:07:48 -0400 Subject: [PATCH 19/31] Truncate Discord release note text (#3112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hopefully this works the first time 😅 Release Notes: - N/A --- .github/workflows/release_actions.yml | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index f767324e4f..54e5d07787 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -10,19 +10,30 @@ jobs: id: get-appropriate-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then - URL="https://zed.dev/releases/preview/latest" + url="https://zed.dev/releases/preview/latest" else - URL="https://zed.dev/releases/stable/latest" + url="https://zed.dev/releases/stable/latest" fi - echo "::set-output name=URL::$URL" + echo "::set-output name=url::$url" + + - name: Prepare release content + id: prepare-content + run: | + set -eu + + text="📣 Zed ${{ github.event.release.tag_name }} was just released!\n\nRestart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.\n\n${{ github.event.release.body }}" + + maxTextLength=2000 + truncationIndicator="..." + + if (( ${#text} > maxTextLength )); then + text=${text:0:maxTextLength - ${#truncationIndicator}}$truncationIndicator + fi + + echo "::set-output name=content::$text" - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: | - 📣 Zed ${{ github.event.release.tag_name }} was just released! - - Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it. - - ${{ github.event.release.body }} + content: ${{ steps.prepare-content.outputs.content }} From aed317840fbde9ab25e3a9b35acc3a12aad8a34a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 Oct 2023 15:53:11 -0700 Subject: [PATCH 20/31] Fix inclusion of spurious views from other projects in FollowResponse (#3116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A logic error in https://github.com/zed-industries/zed/pull/2993 caused follow responses to sometimes contain extra views for other unshared projects 😱 . These views would generally fail to deserialize on the other end. This would create a broken intermediate state, where the following relationship was registered on the server (and on the leader's client), but the follower didn't have the state necessary for following into certain views. Release Notes: - Fixed a bug where following would sometimes fail if the leader had another unshared project open. --- crates/workspace/src/workspace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2aa9b008da..8d8b9046f6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2882,8 +2882,7 @@ impl Workspace { let cx = &cx; move |item| { let item = item.to_followable_item_handle(cx)?; - if project_id.is_some() - && project_id != follower_project_id + if (project_id.is_none() || project_id != follower_project_id) && item.is_project_item(cx) { return None; From 7ef8bd6377fe49e75240083d80437afee30e118c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2023 09:21:08 -0700 Subject: [PATCH 21/31] Always log panics (#2896) I just panicked and wanted to see the cause, but forgot that panic files get deleted when Zed uploads them. Release Notes: - Panics are now written to `~/Library/Logs/Zed/Zed.log` --- crates/zed/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 12ae0f2ffc..cd862f031f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -495,11 +495,11 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin 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)); From 0f622417d7ffe93953ef4bba4ee3827ba110739e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 10 Oct 2023 15:56:19 -0700 Subject: [PATCH 22/31] zed 0.107.4 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 629aec0776..cd78395335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10083,7 +10083,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.3" +version = "0.107.4" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bf00cf4483..529048b9b8 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.3" +version = "0.107.4" publish = false [lib] From ad0e53aa6f498dfd2edc2acec2254782dea3fba1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 11 Oct 2023 01:54:32 -0400 Subject: [PATCH 23/31] Fix Discord text truncation --- .github/workflows/release_actions.yml | 36 +++++++++++---------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 54e5d07787..c1df24a8e5 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,34 +6,28 @@ jobs: discord_release: runs-on: ubuntu-latest steps: - - name: Get appropriate URL - id: get-appropriate-url + - name: Get release URL + id: get-release-url run: | if [ "${{ github.event.release.prerelease }}" == "true" ]; then - url="https://zed.dev/releases/preview/latest" + URL="https://zed.dev/releases/preview/latest" else - url="https://zed.dev/releases/stable/latest" + URL="https://zed.dev/releases/stable/latest" fi - echo "::set-output name=url::$url" + echo "::set-output name=URL::$URL" + - name: Get content + uses: 2428392/gh-truncate-string-action@v1.2.0 + id: get-content + with: + stringToTruncate: | + 📣 Zed ${{ github.event.release.tag_name }} was just released! - - name: Prepare release content - id: prepare-content - run: | - set -eu - - text="📣 Zed ${{ github.event.release.tag_name }} was just released!\n\nRestart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.\n\n${{ github.event.release.body }}" - - maxTextLength=2000 - truncationIndicator="..." - - if (( ${#text} > maxTextLength )); then - text=${text:0:maxTextLength - ${#truncationIndicator}}$truncationIndicator - fi - - echo "::set-output name=content::$text" + Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it. + ${{ github.event.release.body }} + maxLength: 2000 - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: ${{ steps.prepare-content.outputs.content }} + content: ${{ steps.get-content.outputs.string }} From c44181d4457a1c3d4789ef8c49a31ddacfded761 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 Oct 2023 19:20:54 +0200 Subject: [PATCH 24/31] Revert outline summarization (#3114) This pull request essentially reverts #3067: we noticed that only using the function signatures produces far worse results in codegen, and so that feels like a regression compared to before. We should re-enable this once we have a smarter approach to fetching context during codegen, possibly when #3097 lands. As a drive-by, we also fixed a longstanding bug that caused codegen to include the final line of a selection even if the selection ended at the start of the line. Ideally, I'd like to hot fix this to preview so that it goes to stable during the weekly release. /cc: @KCaverly @nathansobo Release Notes: - N/A --- crates/assistant/src/assistant_panel.rs | 48 ++++++++++++++++--------- crates/assistant/src/codegen.rs | 23 ++---------- crates/assistant/src/prompts.rs | 22 +++++++++--- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b69c12a2a3..b1c6038602 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -17,7 +17,7 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; @@ -278,22 +278,36 @@ impl AssistantPanel { if selection.start.excerpt_id() != selection.end.excerpt_id() { return; } + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + + // Extend the selection to the start and the end of the line. + let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); + if point_selection.end > point_selection.start { + point_selection.start.column = 0; + // If the selection ends at the start of the line, we don't want to include it. + if point_selection.end.column == 0 { + point_selection.end.row -= 1; + } + point_selection.end.column = snapshot.line_len(point_selection.end.row); + } + + let codegen_kind = if point_selection.start == point_selection.end { + CodegenKind::Generate { + position: snapshot.anchor_after(point_selection.start), + } + } else { + CodegenKind::Transform { + range: snapshot.anchor_before(point_selection.start) + ..snapshot.anchor_after(point_selection.end), + } + }; let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let provider = Arc::new(OpenAICompletionProvider::new( api_key, cx.background().clone(), )); - let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - CodegenKind::Generate { - position: selection.start, - } - } else { - CodegenKind::Transform { - range: selection.start..selection.end, - } - }; + let codegen = cx.add_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); @@ -319,7 +333,7 @@ impl AssistantPanel { editor.insert_blocks( [BlockProperties { style: BlockStyle::Flex, - position: selection.head().bias_left(&snapshot), + position: snapshot.anchor_before(point_selection.head()), height: 2, render: Arc::new({ let inline_assistant = inline_assistant.clone(); @@ -578,10 +592,7 @@ impl AssistantPanel { let codegen_kind = codegen.read(cx).kind().clone(); let user_prompt = user_prompt.to_string(); - let prompt = cx.background().spawn(async move { - let language_name = language_name.as_deref(); - generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) - }); + let mut messages = Vec::new(); let mut model = settings::get::(cx) .default_open_ai_model @@ -597,6 +608,11 @@ impl AssistantPanel { model = conversation.model.clone(); } + let prompt = cx.background().spawn(async move { + let language_name = language_name.as_deref(); + generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) + }); + cx.spawn(|_, mut cx| async move { let prompt = prompt.await; diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index e956d72260..b6ef6b5cfa 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -1,9 +1,7 @@ use crate::streaming_diff::{Hunk, StreamingDiff}; use ai::completion::{CompletionProvider, OpenAIRequest}; use anyhow::Result; -use editor::{ - multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; +use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{Entity, ModelContext, ModelHandle, Task}; use language::{Rope, TransactionId}; @@ -40,26 +38,11 @@ impl Entity for Codegen { impl Codegen { pub fn new( buffer: ModelHandle, - mut kind: CodegenKind, + kind: CodegenKind, provider: Arc, cx: &mut ModelContext, ) -> Self { let snapshot = buffer.read(cx).snapshot(cx); - match &mut kind { - CodegenKind::Transform { range } => { - let mut point_range = range.to_point(&snapshot); - point_range.start.column = 0; - if point_range.end.column > 0 || point_range.start.row == point_range.end.row { - point_range.end.column = snapshot.line_len(point_range.end.row); - } - range.start = snapshot.anchor_before(point_range.start); - range.end = snapshot.anchor_after(point_range.end); - } - CodegenKind::Generate { position } => { - *position = position.bias_right(&snapshot); - } - } - Self { provider, buffer: buffer.clone(), @@ -386,7 +369,7 @@ mod tests { let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); - snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4)) + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(TestCompletionProvider::new()); let codegen = cx.add_model(|cx| { diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index bf041dff52..d326a7f445 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -4,6 +4,7 @@ use std::cmp::{self, Reverse}; use std::fmt::Write; use std::ops::Range; +#[allow(dead_code)] fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] struct Match { @@ -121,6 +122,7 @@ pub fn generate_content_prompt( range: Range, kind: CodegenKind, ) -> String { + let range = range.to_offset(buffer); let mut prompt = String::new(); // General Preamble @@ -130,17 +132,29 @@ pub fn generate_content_prompt( writeln!(prompt, "You're an expert engineer.\n").unwrap(); } - let outline = summarize(buffer, range); + let mut content = String::new(); + content.extend(buffer.text_for_range(0..range.start)); + if range.start == range.end { + content.push_str("<|START|>"); + } else { + content.push_str("<|START|"); + } + content.extend(buffer.text_for_range(range.clone())); + if range.start != range.end { + content.push_str("|END|>"); + } + content.extend(buffer.text_for_range(range.end..buffer.len())); + writeln!( prompt, - "The file you are currently working on has the following outline:" + "The file you are currently working on has the following content:" ) .unwrap(); if let Some(language_name) = language_name { let language_name = language_name.to_lowercase(); - writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + writeln!(prompt, "```{language_name}\n{content}\n```").unwrap(); } else { - writeln!(prompt, "```\n{outline}\n```").unwrap(); + writeln!(prompt, "```\n{content}\n```").unwrap(); } match kind { From 933537e912ea876c7e2f6aecf742489f1fae5180 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 Oct 2023 08:27:53 +0200 Subject: [PATCH 25/31] zed 0.107.5 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd78395335..8dfce0808e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10083,7 +10083,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.4" +version = "0.107.5" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 529048b9b8..dca5b10bb4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.4" +version = "0.107.5" publish = false [lib] From 596e2f307bb60bcfb507204ce1a3819c1734f873 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Oct 2023 22:35:11 +0200 Subject: [PATCH 26/31] Ignore history items' paths when matching search queries (#3107) Follow-up of https://github.com/zed-industries/zed/pull/3059 Before: ![image](https://github.com/zed-industries/zed/assets/2690773/4eb2d2d1-1aa3-40b8-b782-bf2bc5f17b43) After: ![image](https://github.com/zed-industries/zed/assets/2690773/5587d46b-9198-45fe-9372-114a95d4b7d6) Release Notes: - N/A --- crates/file_finder/src/file_finder.rs | 127 +++++++++++++++++++++++++- crates/fuzzy/src/matcher.rs | 2 +- crates/fuzzy/src/paths.rs | 6 +- 3 files changed, 126 insertions(+), 9 deletions(-) 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/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, From ac9623780631f527a90644b0778faf06ecc60844 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 10 Oct 2023 11:55:06 +0200 Subject: [PATCH 27/31] Omit history files with path that does not exist on disk anymore (#3113) --- crates/file_finder/src/file_finder.rs | 122 +++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 12 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 222a9c650a..b7a4a387ab 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -222,6 +222,10 @@ fn toggle_or_cycle_file_finder( .as_ref() .and_then(|found_path| found_path.absolute.as_ref()) }) + .filter(|(_, history_abs_path)| match history_abs_path { + Some(abs_path) => history_file_exists(abs_path), + None => true, + }) .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), ) .collect(); @@ -246,6 +250,16 @@ fn toggle_or_cycle_file_finder( } } +#[cfg(not(test))] +fn history_file_exists(abs_path: &PathBuf) -> bool { + abs_path.exists() +} + +#[cfg(test)] +fn history_file_exists(abs_path: &PathBuf) -> bool { + !abs_path.ends_with("nonexistent.rs") +} + pub enum Event { Selected(ProjectPath), Dismissed, @@ -515,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate { project .worktree_for_id(history_item.project.worktree_id, cx) .is_some() - || (project.is_local() - && history_item - .absolute - .as_ref() - .filter(|abs_path| abs_path.exists()) - .is_some()) + || (project.is_local() && history_item.absolute.is_some()) }) .cloned() .map(|p| (p, None)) @@ -1900,13 +1909,8 @@ mod tests { .matches .search .iter() - .map(|e| e.path.to_path_buf()) + .map(|path_match| path_match.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![ @@ -1920,6 +1924,100 @@ mod tests { }); } + #[gpui::test] + async fn test_nonexistent_history_items_not_shown( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "nonexistent.rs": "// Second Rust file", + "third.rs": "// Third 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( + "non", + 1, + "nonexistent.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( + "fir", + 1, + "first.rs", + window.into(), + &workspace, + &deterministic, + cx, + ) + .await; + + cx.dispatch_action(window.into(), Toggle); + let query = "rs"; + 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(); + let history_entries = delegate + .matches + .history + .iter() + .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + .collect::>(); + assert_eq!( + history_entries, + vec![ + PathBuf::from("test/first.rs"), + PathBuf::from("test/third.rs"), + ], + "Should have all opened files in the history, except the ones that do not exist on disk" + ); + }); + } + async fn open_close_queried_buffer( input: &str, expected_matches: usize, From 5a2a92c7fef9b5a856d322d8877568f13125e410 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 11 Oct 2023 12:50:56 +0300 Subject: [PATCH 28/31] zed 0.107.6 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8dfce0808e..8205eba8fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10083,7 +10083,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.5" +version = "0.107.6" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index dca5b10bb4..290234bbcd 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.5" +version = "0.107.6" publish = false [lib] From bfea1f22637547d76d4b9e96843596dba83f0263 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 11 Oct 2023 12:40:23 -0400 Subject: [PATCH 29/31] v0.107.x stable --- crates/zed/RELEASE_CHANNEL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 4de2f126df..870bbe4e50 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -preview \ No newline at end of file +stable \ No newline at end of file From c59fd08c6518430bc5176326dff21c73364756e3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 16 Oct 2023 14:01:52 -0400 Subject: [PATCH 30/31] Fix telemetry-related crash on start up (#3131) Fixes (hopefully) [#2136](https://github.com/zed-industries/community/issues/2136). Release Notes: - N/A --- crates/client/src/telemetry.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 70878bf2e4..fd93aaeec8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -4,7 +4,9 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; -use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; +use sysinfo::{ + CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, +}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; @@ -166,8 +168,16 @@ impl Telemetry { let this = self.clone(); cx.spawn(|mut cx| async move { - let mut system = System::new_all(); - system.refresh_all(); + // Avoiding calling `System::new_all()`, as there have been crashes related to it + let refresh_kind = RefreshKind::new() + .with_memory() // For memory usage + .with_processes(ProcessRefreshKind::everything()) // For process usage + .with_cpu(CpuRefreshKind::everything()); // For core count + + let mut system = System::new_with_specifics(refresh_kind); + + // Avoiding calling `refresh_all()`, just update what we need + system.refresh_specifics(refresh_kind); loop { // Waiting some amount of time before the first query is important to get a reasonable value @@ -175,8 +185,7 @@ impl Telemetry { const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; - system.refresh_memory(); - system.refresh_processes(); + system.refresh_specifics(refresh_kind); let current_process = Pid::from_u32(std::process::id()); let Some(process) = system.processes().get(¤t_process) else { From 48b8853def364f133378addfe42755fd12a841ca Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 16 Oct 2023 14:39:59 -0400 Subject: [PATCH 31/31] zed 0.107.7 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8205eba8fb..8df7fa242f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10083,7 +10083,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.6" +version = "0.107.7" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 290234bbcd..197dda9c74 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.6" +version = "0.107.7" publish = false [lib]