From d9c08de58a7a164df43614791e5318d5d9824a05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Sep 2023 17:15:26 +0200 Subject: [PATCH 01/27] Revert "Revert "leverage file outline and selection as opposed to entire file"" --- crates/assistant/src/assistant.rs | 1 + crates/assistant/src/assistant_panel.rs | 117 ++------ crates/assistant/src/prompts.rs | 382 ++++++++++++++++++++++++ 3 files changed, 411 insertions(+), 89 deletions(-) create mode 100644 crates/assistant/src/prompts.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 258684db47096bac2d3df33d0289462dbc841214..6c9b14333e34cbf5fd49d8299ba7bd891b607526 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -1,6 +1,7 @@ pub mod assistant_panel; mod assistant_settings; mod codegen; +mod prompts; mod streaming_diff; use ai::completion::Role; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 42e5fb78979a6b8136c5c60d29e38e064df3435d..37d0d729fe64e0def057402fb9a24b796ebdf317 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,6 +1,7 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, + prompts::generate_content_prompt, MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; @@ -541,11 +542,25 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let multi_buffer = editor.read(cx).buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let snapshot = if multi_buffer.is_singleton() { + multi_buffer.as_singleton().unwrap().read(cx).snapshot() + } else { + return; + }; + let range = pending_assist.codegen.read(cx).range(); - let selected_text = snapshot.text_for_range(range.clone()).collect::(); + let language_range = snapshot.anchor_at( + range.start.to_offset(&multi_buffer_snapshot), + language::Bias::Left, + ) + ..snapshot.anchor_at( + range.end.to_offset(&multi_buffer_snapshot), + language::Bias::Right, + ); - let language = snapshot.language_at(range.start); + let language = snapshot.language_at(language_range.start); let language_name = if let Some(language) = language.as_ref() { if Arc::ptr_eq(language, &language::PLAIN_TEXT) { None @@ -557,93 +572,17 @@ impl AssistantPanel { }; let language_name = language_name.as_deref(); - let mut prompt = String::new(); - if let Some(language_name) = language_name { - writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); - } - match pending_assist.codegen.read(cx).kind() { - CodegenKind::Transform { .. } => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt, "```").unwrap(); + let codegen_kind = pending_assist.codegen.read(cx).kind().clone(); + let prompt = generate_content_prompt( + user_prompt.to_string(), + language_name, + &snapshot, + language_range, + cx, + codegen_kind, + ); - writeln!( - prompt, - "In particular, the user has selected the following text:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - writeln!(prompt, "{selected_text}").unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!(prompt).unwrap(); - writeln!( - prompt, - "Modify the selected text given the user prompt: {user_prompt}" - ) - .unwrap(); - writeln!( - prompt, - "You MUST reply only with the edited selected text, not the entire file." - ) - .unwrap(); - } - CodegenKind::Generate { .. } => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..range.start) { - write!(prompt, "{chunk}").unwrap(); - } - write!(prompt, "<|>").unwrap(); - for chunk in snapshot.text_for_range(range.start..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt).unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!( - prompt, - "Assume the cursor is located where the `<|>` marker is." - ) - .unwrap(); - writeln!( - prompt, - "Text can't be replaced, so assume your answer will be inserted at the cursor." - ) - .unwrap(); - writeln!( - prompt, - "Complete the text given the user prompt: {user_prompt}" - ) - .unwrap(); - } - } - if let Some(language_name) = language_name { - writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); - } - writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!(prompt, "Never make remarks about the output.").unwrap(); + dbg!(&prompt); let mut messages = Vec::new(); let mut model = settings::get::(cx) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs new file mode 100644 index 0000000000000000000000000000000000000000..ee58090d04dbff9d7da058a905c9e52b8fe1b0cd --- /dev/null +++ b/crates/assistant/src/prompts.rs @@ -0,0 +1,382 @@ +use gpui::AppContext; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use std::cmp; +use std::ops::Range; +use std::{fmt::Write, iter}; + +use crate::codegen::CodegenKind; + +fn outline_for_prompt( + buffer: &BufferSnapshot, + range: Range, + cx: &AppContext, +) -> Option { + let indent = buffer + .language_indent_size_at(0, cx) + .chars() + .collect::(); + let outline = buffer.outline(None)?; + let range = range.to_offset(buffer); + + let mut text = String::new(); + let mut items = outline.items.into_iter().peekable(); + + let mut intersected = false; + let mut intersection_indent = 0; + let mut extended_range = range.clone(); + + while let Some(item) = items.next() { + let item_range = item.range.to_offset(buffer); + if item_range.end < range.start || item_range.start > range.end { + text.extend(iter::repeat(indent.as_str()).take(item.depth)); + text.push_str(&item.text); + text.push('\n'); + } else { + intersected = true; + let is_terminal = items + .peek() + .map_or(true, |next_item| next_item.depth <= item.depth); + if is_terminal { + if item_range.start <= extended_range.start { + extended_range.start = item_range.start; + intersection_indent = item.depth; + } + extended_range.end = cmp::max(extended_range.end, item_range.end); + } else { + let name_start = item_range.start + item.name_ranges.first().unwrap().start; + let name_end = item_range.start + item.name_ranges.last().unwrap().end; + + if range.start > name_end { + text.extend(iter::repeat(indent.as_str()).take(item.depth)); + text.push_str(&item.text); + text.push('\n'); + } else { + if name_start <= extended_range.start { + extended_range.start = item_range.start; + intersection_indent = item.depth; + } + extended_range.end = cmp::max(extended_range.end, name_end); + } + } + } + + if intersected + && items.peek().map_or(true, |next_item| { + next_item.range.start.to_offset(buffer) > range.end + }) + { + intersected = false; + text.extend(iter::repeat(indent.as_str()).take(intersection_indent)); + text.extend(buffer.text_for_range(extended_range.start..range.start)); + text.push_str("<|START|"); + text.extend(buffer.text_for_range(range.clone())); + if range.start != range.end { + text.push_str("|END|>"); + } else { + text.push_str(">"); + } + text.extend(buffer.text_for_range(range.end..extended_range.end)); + text.push('\n'); + } + } + + Some(text) +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: &BufferSnapshot, + range: Range, + cx: &AppContext, + kind: CodegenKind, +) -> String { + let mut prompt = String::new(); + + // General Preamble + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap(); + } else { + writeln!(prompt, "You're an expert engineer.\n").unwrap(); + } + + let outline = outline_for_prompt(buffer, range.clone(), cx); + if let Some(outline) = outline { + writeln!( + prompt, + "The file you are currently working on has the following outline:" + ) + .unwrap(); + if let Some(language_name) = language_name { + let language_name = language_name.to_lowercase(); + writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + } else { + writeln!(prompt, "```\n{outline}\n```").unwrap(); + } + } + + // Assume for now that we are just generating + if range.clone().start == range.end { + writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); + } else { + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); + } + + match kind { + CodegenKind::Generate { position: _ } => { + writeln!( + prompt, + "Assume the cursor is located where the `<|START|` marker is." + ) + .unwrap(); + writeln!( + prompt, + "Text can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); + writeln!( + prompt, + "Generate text based on the users prompt: {user_prompt}" + ) + .unwrap(); + } + CodegenKind::Transform { range: _ } => { + writeln!( + prompt, + "Modify the users code selected text based upon the users prompt: {user_prompt}" + ) + .unwrap(); + writeln!( + prompt, + "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file." + ) + .unwrap(); + } + } + + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap(); + } + writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); + + prompt +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use std::sync::Arc; + + use gpui::AppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use settings::SettingsStore; + + pub(crate) fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_outline_query( + r#" + (struct_item + "struct" @context + name: (_) @name) @item + (enum_item + "enum" @context + name: (_) @name) @item + (enum_variant + name: (_) @name) @item + (field_declaration + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @name + "for"? @context + type: (_) @name) @item + (function_item + "fn" @context + name: (_) @name) @item + (mod_item + "mod" @context + name: (_) @name) @item + "#, + ) + .unwrap() + } + + #[gpui::test] + fn test_outline_for_prompt(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + language_settings::init(cx); + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + self.a + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_before(Point::new(1, 4)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + <|START|>a: usize + b + impl X + fn new + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(8, 14)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new() -> Self { + let <|START|a |END|>= 1; + let b = 2; + Self { a, b } + } + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(6, 0))..snapshot.anchor_before(Point::new(6, 0)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + <|START|> + fn new + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(13, 9)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new() -> Self { + let <|START|a = 1; + let b = 2; + Self { a, b } + } + + pub f|END|>n a(&self, param: bool) -> usize { + self.a + } + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(5, 6))..snapshot.anchor_before(Point::new(12, 0)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X<|START| { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + |END|> + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(18, 8))..snapshot.anchor_before(Point::new(18, 8)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new + fn a + pub fn b(&self) -> usize { + <|START|>self.b + } + "}) + ); + } +} From 53c25690f940e396b99d004ecefd353c15e1aa8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Sep 2023 20:37:07 +0200 Subject: [PATCH 02/27] WIP: Use a different approach to codegen outline --- crates/zed/src/languages/rust/summary.scm | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 crates/zed/src/languages/rust/summary.scm diff --git a/crates/zed/src/languages/rust/summary.scm b/crates/zed/src/languages/rust/summary.scm new file mode 100644 index 0000000000000000000000000000000000000000..7174eec3c384336d9c2d647428d60d0eb82dea2a --- /dev/null +++ b/crates/zed/src/languages/rust/summary.scm @@ -0,0 +1,6 @@ +(function_item + body: (block + "{" @keep + "}" @keep) @collapse) + +(use_declaration) @collapse From 219715449d406df651174ef85ec391ae4ed83795 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 12:36:17 -0600 Subject: [PATCH 03/27] More logging on collab by default --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index f6fde3cd92c91a448e194e87ff6668af89260382..2eb7de20fb7e9cae34375dc130c6d27aea01012e 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ web: cd ../zed.dev && PORT=3000 npm run dev -collab: cd crates/collab && cargo run serve +collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf From 1cfc2f0c0796b891e32225ad3067c1db3e35d18c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 16:02:36 -0600 Subject: [PATCH 04/27] Show host in titlebar Co-Authored-By: Max Brunsfeld --- crates/client/src/client.rs | 4 ++ crates/client/src/user.rs | 4 ++ crates/collab_ui/src/collab_titlebar_item.rs | 73 +++++++++++++++++++- crates/project/src/project.rs | 4 ++ crates/theme/src/theme.rs | 1 + crates/workspace/src/workspace.rs | 18 +++++ styles/src/style_tree/titlebar.ts | 8 ++- 7 files changed, 110 insertions(+), 2 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5eae700404c316cdf55d44cccb24470ded092c93..4ddfbc5a3478a100f3ddc7e309708b3aed9b964c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -453,6 +453,10 @@ impl Client { self.state.read().status.1.clone() } + pub fn is_connected(&self) -> bool { + matches!(&*self.status().borrow(), Status::Connected { .. }) + } + fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index b8cc8fb1b890fce59754f1a1bc93b6e2fa61ce2a..6aa41708e3ae3e3c3504ab82791278c8a1837c0a 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -595,6 +595,10 @@ impl UserStore { self.load_users(proto::FuzzySearchUsers { query }, cx) } + pub fn get_cached_user(&self, user_id: u64) -> Option> { + self.users.get(&user_id).cloned() + } + pub fn get_user( &mut self, user_id: u64, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9f3b7d2f30ffe360578d66d45a28b91bd149e51a..546b8ef407d7c809c24d76eb569053e87c3b3691 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -215,7 +215,13 @@ impl CollabTitlebarItem { let git_style = theme.titlebar.git_menu_button.clone(); let item_spacing = theme.titlebar.item_spacing; - let mut ret = Flex::row().with_child( + let mut ret = Flex::row(); + + if let Some(project_host) = self.collect_project_host(theme.clone(), cx) { + ret = ret.with_child(project_host) + } + + ret = ret.with_child( Stack::new() .with_child( MouseEventHandler::new::(0, cx, |mouse_state, cx| { @@ -283,6 +289,71 @@ impl CollabTitlebarItem { ret.into_any() } + fn collect_project_host( + &self, + theme: Arc, + cx: &mut ViewContext, + ) -> Option> { + if ActiveCall::global(cx).read(cx).room().is_none() { + return None; + } + let project = self.project.read(cx); + let user_store = self.user_store.read(cx); + + if project.is_local() { + return None; + } + + let Some(host) = project.host() else { + return None; + }; + let (Some(host_user), Some(participant_index)) = ( + user_store.get_cached_user(host.user_id), + user_store.participant_indices().get(&host.user_id), + ) else { + return None; + }; + + enum ProjectHost {} + enum ProjectHostTooltip {} + + let host_style = theme.titlebar.project_host.clone(); + let selection_style = theme + .editor + .selection_style_for_room_participant(participant_index.0); + let peer_id = host.peer_id.clone(); + + Some( + MouseEventHandler::new::(0, cx, |mouse_state, _| { + let mut host_style = host_style.style_for(mouse_state).clone(); + host_style.text.color = selection_style.cursor; + Label::new(host_user.github_login.clone(), host_style.text) + .contained() + .with_style(host_style.container) + .aligned() + .left() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + if let Some(task) = + workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx)) + { + task.detach_and_log_err(cx); + } + } + }) + .with_tooltip::( + 0, + host_user.github_login.clone() + " is sharing this project. Click to follow.", + None, + theme.tooltip.clone(), + cx, + ) + .into_any_named("project-host"), + ) + } + fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { let project = if active { Some(self.project.clone()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee8690ea709c8e7e9f5fd6ba5297ea624d7cd245..1ddf1a1f66baa9f0528cf668af129939a1af120c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -975,6 +975,10 @@ impl Project { &self.collaborators } + pub fn host(&self) -> Option<&Collaborator> { + self.collaborators.values().find(|c| c.replica_id == 0) + } + /// Collect all worktrees, including ones that don't appear in the project panel pub fn worktrees<'a>( &'a self, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2d839c09b08117a9024065b970f13f837eae8..b1595fb0d9de5f7be3ab19e5a643256cd5c3fb43 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -131,6 +131,7 @@ pub struct Titlebar { pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, pub git_menu_button: Toggleable>, + pub project_host: Interactive, pub item_spacing: f32, pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 44a70f9a08fd21641459884bbd81c5feb155c2aa..8d9a4c155093a9d9e323f1d6adc477558f00ce1d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2608,6 +2608,24 @@ impl Workspace { .and_then(|leader_id| self.toggle_follow(leader_id, cx)) } + pub fn follow( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option>> { + 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; + } + } + } + + // not currently following, so follow. + self.toggle_follow(leader_id, cx) + } + pub fn unfollow( &mut self, pane: &ViewHandle, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 672907b22cc9d4359645494bea9dfc326054eb6e..63c057a8eb44c45766eda0983565268663eef5d3 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -1,4 +1,4 @@ -import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component" +import { icon_button, text_button, toggleable_icon_button, toggleable_text_button } from "../component" import { interactive, toggleable } from "../element" import { useTheme, with_opacity } from "../theme" import { background, border, foreground, text } from "./components" @@ -191,6 +191,12 @@ export function titlebar(): any { color: "variant", }), + project_host: text_button({ + text_properties: { + weight: "bold" + } + }), + // Collaborators leader_avatar: { width: avatar_width, From 92bb9a5fdccb63df807362ccde8654b39dd69dca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 17:59:19 -0600 Subject: [PATCH 05/27] Make following more good Co-Authored-By: Max Brunsfeld --- crates/collab/src/tests.rs | 1 + crates/collab/src/tests/following_tests.rs | 1189 ++++++++++++++++++ crates/collab/src/tests/integration_tests.rs | 1139 +---------------- crates/collab_ui/src/collab_titlebar_item.rs | 71 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/workspace.rs | 52 +- 6 files changed, 1295 insertions(+), 1159 deletions(-) create mode 100644 crates/collab/src/tests/following_tests.rs diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index b0f5b96fde04dcd9249550ea5975299793e7a894..e78bbe3466318cfc44fbcf298cef65a86350a0b8 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext}; mod channel_buffer_tests; mod channel_message_tests; mod channel_tests; +mod following_tests; mod integration_tests; mod random_channel_buffer_tests; mod random_project_collaboration_tests; diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7acae39950654b1b7cb84ccb17c6f4004730566 --- /dev/null +++ b/crates/collab/src/tests/following_tests.rs @@ -0,0 +1,1189 @@ +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use editor::{Editor, ExcerptRange, MultiBuffer}; +use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; +use live_kit_client::MacOSDisplay; +use serde_json::json; +use std::sync::Arc; +use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::{test::TestItem, ItemHandle as _}, + shared_screen::SharedScreen, + SplitDirection, Workspace, +}; + +#[gpui::test(iterations = 10)] +async fn test_basic_following( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_d: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + let client_d = server.create_client(cx_d, "user_d").await; + server + .create_room(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let window_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = window_a.root(cx_a); + let window_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = window_b.root(cx_b); + + // Client A opens some editors. + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let peer_id_a = client_a.peer_id().unwrap(); + let peer_id_b = client_b.peer_id().unwrap(); + let peer_id_c = client_c.peer_id().unwrap(); + let peer_id_d = client_d.peer_id().unwrap(); + + // Client A updates their selections in those editors + editor_a1.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_a2.update(cx_a, |editor, cx| { + editor.handle_input("d", cx); + editor.handle_input("e", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![2..1]); + }); + + // When client B starts following client A, all visible view states are replicated to client B. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + cx_c.foreground().run_until_parked(); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_b2.project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![2..1] + ); + assert_eq!( + editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![3..2] + ); + + cx_c.foreground().run_until_parked(); + let active_call_c = cx_c.read(ActiveCall::global); + let project_c = client_c.build_remote_project(project_id, cx_c).await; + let window_c = client_c.build_workspace(&project_c, cx_c); + let workspace_c = window_c.root(cx_c); + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + drop(project_c); + + // Client C also follows client A. + workspace_c + .update(cx_c, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + cx_d.foreground().run_until_parked(); + let active_call_d = cx_d.read(ActiveCall::global); + let project_d = client_d.build_remote_project(project_id, cx_d).await; + let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); + active_call_d + .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) + .await + .unwrap(); + drop(project_d); + + // All clients see that clients B and C are following client A. + cx_c.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}" + ); + }); + } + + // Client C unfollows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx); + }); + + // 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}" + ); + }); + } + + // Client C re-follows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx); + }); + + // 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}" + ); + }); + } + + // Client D follows client C. + workspace_d + .update(cx_d, |workspace, cx| { + workspace.toggle_follow(peer_id_c, cx).unwrap() + }) + .await + .unwrap(); + + // 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}" + ); + }); + } + + // Client C closes the project. + window_c.remove(cx_c); + cx_c.drop_last(workspace_c); + + // Clients A and B see that client B is following A, and client C is not present in the followers. + cx_c.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}" + ); + }); + } + + // When client A activates a different editor, client B does so as well. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a1, cx) + }); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // When client A opens a multibuffer, client B does so as well. + let multibuffer_a = cx_a.add_model(|cx| { + let buffer_a1 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "1.txt").into(), cx) + .unwrap() + }); + let buffer_a2 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "2.txt").into(), cx) + .unwrap() + }); + let mut result = MultiBuffer::new(0); + result.push_excerpts( + buffer_a1, + [ExcerptRange { + context: 0..3, + primary: None, + }], + cx, + ); + result.push_excerpts( + buffer_a2, + [ExcerptRange { + context: 4..7, + primary: None, + }], + cx, + ); + result + }); + let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { + let editor = + cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor + }); + deterministic.run_until_parked(); + let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), + ); + + // When client A navigates back and forth, client B does so as well. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_forward(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // Changes to client A's editor are reflected on client B. + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); + }); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + }); + + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); + + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.set_scroll_position(vec2f(0., 100.), cx); + }); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[3..3]); + }); + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx) + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_b1.id() + ); + + // Client A starts following client B. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.toggle_follow(peer_id_b, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(peer_id_b) + ); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_a1.id() + ); + + // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + let display = MacOSDisplay::new(); + active_call_b + .update(cx_b, |call, cx| call.set_location(None, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_display_sources(vec![display.clone()]); + room.share_screen(cx) + }) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item isn't a shared screen") + }); + + // Client B activates Zed again, which causes the previous editor to become focused again. + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, cx) + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()); + deterministic.advance_clock(RECONNECT_TIMEOUT); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); +} + +#[gpui::test] +async fn test_following_tab_order( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + //Open 1, 3 in that order on client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); + + let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { + pane.update(cx, |pane, cx| { + pane.items() + .map(|item| { + item.project_path(cx) + .unwrap() + .path + .to_str() + .unwrap() + .to_owned() + }) + .collect::>() + }) + }; + + //Verify that the tabs opened in the order we expect + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); + + //Follow client B as client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.toggle_follow(client_b_id, cx).unwrap() + }) + .await + .unwrap(); + + //Open just 2 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Verify that newly opened followed file is at the end + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); + + //Open just 1 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); + deterministic.run_until_parked(); + + // Verify that following into 1 did not reorder + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_following_each_other( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + "4.txt": "four", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens some editors. + 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 + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + 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 + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a.update(cx_a, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_a + .update(cx_a, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_a1); + let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_b + .update(cx_b, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_b1); + let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + workspace.toggle_follow(leader_id, 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); + }); + + 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(); + + // 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. + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "3.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); + }); + + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + }); + + 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); + }); + + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_auto_unfollowing( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + // 2 clients connect to a server. + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B starts following client A. + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(vec2f(0., 3.), cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_simultaneously_following_each_other( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a.fs().insert_tree("/a", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + deterministic.run_until_parked(); + let client_a_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { + workspace.toggle_follow(client_b_id, cx).unwrap() + }); + let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_follow(client_a_id, cx).unwrap() + }); + + futures::try_join!(a_follow_b, b_follow_a).unwrap(); + workspace_a.read_with(cx_a, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_b_id) + ); + }); + workspace_b.read_with(cx_b, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a_id) + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_across_workspaces( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + // a and b join a channel/call + // a shares project 1 + // b shares project 2 + // + // + // b joins project 1 + // + // test: when a is in project 2 and b clicks follow (from unshared project), b should open project 2 and follow a + // test: when a is in project 1 and b clicks follow, b should open project 1 and follow a + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "w.rs": "", + "x.rs": "", + }), + ) + .await; + + client_b + .fs() + .insert_tree( + "/b", + json!({ + "y.rs": "", + "z.rs": "", + }), + ) + .await; + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; + let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + + let project_a_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!(cx_b.windows().len(), 1); + + workspace_b.update(cx_b, |workspace, cx| { + workspace + .follow(client_a.peer_id().unwrap(), cx) + .unwrap() + .detach() + }); + + deterministic.run_until_parked(); + let workspace_b_project_a = cx_b + .windows() + .iter() + .max_by_key(|window| window.id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_b); + + // assert that b is following a in project a in w.rs + workspace_b_project_a.update(cx_b, |workspace, _| { + assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); + assert_eq!( + client_a.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + }); + + // assert that there are no share notifications open +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b17b7b3fc22dac4007d8d6504d048220529e2dd8..4008a941dd2e76be691e8a9d54b5cb66f1f8c5a2 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7,14 +7,11 @@ use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, - ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, + ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, }; use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; -use gpui::{ - executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, - TestAppContext, ViewHandle, -}; +use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, @@ -38,12 +35,7 @@ use std::{ }, }; use unindent::Unindent as _; -use workspace::{ - dock::{test::TestPanel, DockPosition}, - item::{test::TestItem, ItemHandle as _}, - shared_screen::SharedScreen, - SplitDirection, Workspace, -}; +use workspace::Workspace; #[ctor::ctor] fn init_logger() { @@ -6388,455 +6380,49 @@ async fn test_contact_requests( } #[gpui::test(iterations = 10)] -async fn test_basic_following( +async fn test_join_call_after_screen_was_shared( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_d: &mut TestAppContext, ) { deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - let client_d = server.create_client(cx_d, "user_d").await; server - .create_room(&mut [ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - (&client_d, cx_d), - ]) + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one\none\none", - "2.txt": "two\ntwo\ntwo", - "3.txt": "three\nthree\nthree", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + // Call users B and C from client A. active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let window_a = client_a.build_workspace(&project_a, cx_a); - let workspace_a = window_a.root(cx_a); - let window_b = client_b.build_workspace(&project_b, cx_b); - let workspace_b = window_b.root(cx_b); - - // Client A opens some editors. - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let editor_a2 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B opens an editor. - let editor_b1 = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let peer_id_a = client_a.peer_id().unwrap(); - let peer_id_b = client_b.peer_id().unwrap(); - let peer_id_c = client_c.peer_id().unwrap(); - let peer_id_d = client_d.peer_id().unwrap(); - - // Client A updates their selections in those editors - editor_a1.update(cx_a, |editor, cx| { - editor.handle_input("a", cx); - editor.handle_input("b", cx); - editor.handle_input("c", cx); - editor.select_left(&Default::default(), cx); - assert_eq!(editor.selections.ranges(cx), vec![3..2]); - }); - editor_a2.update(cx_a, |editor, cx| { - editor.handle_input("d", cx); - editor.handle_input("e", cx); - editor.select_left(&Default::default(), cx); - assert_eq!(editor.selections.ranges(cx), vec![2..1]); - }); - - // When client B starts following client A, all visible view states are replicated to client B. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); - - cx_c.foreground().run_until_parked(); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - cx_b.read(|cx| editor_b2.project_path(cx)), - Some((worktree_id, "2.txt").into()) - ); - assert_eq!( - editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![2..1] - ); - assert_eq!( - editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![3..2] - ); - - cx_c.foreground().run_until_parked(); - let active_call_c = cx_c.read(ActiveCall::global); - let project_c = client_c.build_remote_project(project_id, cx_c).await; - let window_c = client_c.build_workspace(&project_c, cx_c); - let workspace_c = window_c.root(cx_c); - active_call_c - .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) - .await - .unwrap(); - drop(project_c); - - // Client C also follows client A. - workspace_c - .update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); - - cx_d.foreground().run_until_parked(); - let active_call_d = cx_d.read(ActiveCall::global); - let project_d = client_d.build_remote_project(project_id, cx_d).await; - let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); - active_call_d - .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) - .await - .unwrap(); - drop(project_d); - - // All clients see that clients B and C are following client A. - cx_c.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}" - ); - }); - } - - // Client C unfollows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); - }); - - // 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}" - ); - }); - } - - // Client C re-follows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); - }); - - // 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}" - ); - }); - } - - // Client D follows client C. - workspace_d - .update(cx_d, |workspace, cx| { - workspace.toggle_follow(peer_id_c, cx).unwrap() - }) - .await - .unwrap(); - - // 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}" - ); - }); - } - - // Client C closes the project. - window_c.remove(cx_c); - cx_c.drop_last(workspace_c); - - // Clients A and B see that client B is following A, and client C is not present in the followers. - cx_c.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}" - ); - }); - } - - // When client A activates a different editor, client B does so as well. - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a1, cx) - }); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - // When client A opens a multibuffer, client B does so as well. - let multibuffer_a = cx_a.add_model(|cx| { - let buffer_a1 = project_a.update(cx, |project, cx| { - project - .get_open_buffer(&(worktree_id, "1.txt").into(), cx) - .unwrap() - }); - let buffer_a2 = project_a.update(cx, |project, cx| { - project - .get_open_buffer(&(worktree_id, "2.txt").into(), cx) - .unwrap() - }); - let mut result = MultiBuffer::new(0); - result.push_excerpts( - buffer_a1, - [ExcerptRange { - context: 0..3, - primary: None, - }], - cx, - ); - result.push_excerpts( - buffer_a2, - [ExcerptRange { - context: 4..7, - primary: None, - }], - cx, - ); - result - }); - let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { - let editor = - cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); - workspace.add_item(Box::new(editor.clone()), cx); - editor - }); - deterministic.run_until_parked(); - let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), - multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), - ); - - // When client A navigates back and forth, client B does so as well. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_forward(workspace.active_pane().downgrade(), cx) + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - // Changes to client A's editor are reflected on client B. - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); - }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); - }); - - editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); - - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.set_scroll_position(vec2f(0., 100.), cx); - }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[3..3]); - }); - - // After unfollowing, client B stops receiving updates from client A. - workspace_b.update(cx_b, |workspace, cx| { - workspace.unfollow(&workspace.active_pane().clone(), cx) - }); - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a2, cx) - }); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); deterministic.run_until_parked(); assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_b1.id() + room_participants(&room_a, cx_a), + RoomParticipants { + remote: Default::default(), + pending: vec!["user_b".to_string()] + } ); - // Client A starts following client B. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.toggle_follow(peer_id_b, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - Some(peer_id_b) - ); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_a1.id() - ); + // User B receives the call. + let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); + let call_b = incoming_call_b.next().await.unwrap().unwrap(); + assert_eq!(call_b.calling_user.github_login, "user_a"); - // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + // User A shares their screen let display = MacOSDisplay::new(); - active_call_b - .update(cx_b, |call, cx| call.set_location(None, cx)) - .await - .unwrap(); - active_call_b - .update(cx_b, |call, cx| { + active_call_a + .update(cx_a, |call, cx| { call.room().unwrap().update(cx, |room, cx| { room.set_display_sources(vec![display.clone()]); room.share_screen(cx) @@ -6844,161 +6430,26 @@ async fn test_basic_following( }) .await .unwrap(); - deterministic.run_until_parked(); - let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { - workspace - .active_item(cx) - .expect("no active item") - .downcast::() - .expect("active item isn't a shared screen") + + client_b.user_store().update(cx_b, |user_store, _| { + user_store.clear_cache(); }); - // Client B activates Zed again, which causes the previous editor to become focused again. + // User B joins the room active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) - }); - - // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_item(&multibuffer_editor_b, cx) - }); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() - ) - }); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + assert!(incoming_call_b.next().await.unwrap().is_none()); - // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); - workspace_b.update(cx_b, |workspace, cx| { - workspace.add_panel(panel, cx); - workspace.toggle_panel_focus::(cx); - }); deterministic.run_until_parked(); assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - shared_screen.id() - ); - - // Toggling the focus back to the pane causes client A to return to the multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_panel_focus::(cx); - }); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() - ) - }); - - // Client B activates an item that doesn't implement following, - // so the previously-opened screen-sharing item gets activated. - let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); - workspace_b.update(cx_b, |workspace, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(unfollowable_item), true, true, None, cx) - }) - }); - deterministic.run_until_parked(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - shared_screen.id() - ); - - // Following interrupts when client B disconnects. - client_b.disconnect(&cx_b.to_async()); - deterministic.advance_clock(RECONNECT_TIMEOUT); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - None - ); -} - -#[gpui::test(iterations = 10)] -async fn test_join_call_after_screen_was_shared( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; - - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - // Call users B and C from client A. - active_call_a - .update(cx_a, |call, cx| { - call.invite(client_b.user_id().unwrap(), None, cx) - }) - .await - .unwrap(); - let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); - deterministic.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: Default::default(), - pending: vec!["user_b".to_string()] - } - ); - - // User B receives the call. - let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming()); - let call_b = incoming_call_b.next().await.unwrap().unwrap(); - assert_eq!(call_b.calling_user.github_login, "user_a"); - - // User A shares their screen - let display = MacOSDisplay::new(); - active_call_a - .update(cx_a, |call, cx| { - call.room().unwrap().update(cx, |room, cx| { - room.set_display_sources(vec![display.clone()]); - room.share_screen(cx) - }) - }) - .await - .unwrap(); - - client_b.user_store().update(cx_b, |user_store, _| { - user_store.clear_cache(); - }); - - // User B joins the room - active_call_b - .update(cx_b, |call, cx| call.accept_incoming(cx)) - .await - .unwrap(); - let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); - assert!(incoming_call_b.next().await.unwrap().is_none()); - - deterministic.run_until_parked(); - assert_eq!( - room_participants(&room_a, cx_a), - RoomParticipants { - remote: vec!["user_b".to_string()], - pending: vec![], - } + room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: vec![], + } ); assert_eq!( room_participants(&room_b, cx_b), @@ -7021,526 +6472,6 @@ async fn test_join_call_after_screen_was_shared( }); } -#[gpui::test] -async fn test_following_tab_order( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - - //Open 1, 3 in that order on client A - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap(); - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) - }) - .await - .unwrap(); - - let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { - pane.update(cx, |pane, cx| { - pane.items() - .map(|item| { - item.project_path(cx) - .unwrap() - .path - .to_str() - .unwrap() - .to_owned() - }) - .collect::>() - }) - }; - - //Verify that the tabs opened in the order we expect - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); - - //Follow client B as client A - workspace_a - .update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() - }) - .await - .unwrap(); - - //Open just 2 on client B - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - - // Verify that newly opened followed file is at the end - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); - - //Open just 1 on client B - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap(); - assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); - deterministic.run_until_parked(); - - // Verify that following into 1 did not reorder - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); -} - -#[gpui::test(iterations = 10)] -async fn test_peers_following_each_other( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - // Client A shares a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - "4.txt": "four", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Client B joins the project. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - // Client A opens some editors. - 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 - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B opens an editor. - 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 - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Clients A and B follow each other in split panes - workspace_a.update(cx_a, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); - workspace_a - .update(cx_a, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_a1); - let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); - workspace_b - .update(cx_b, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_b1); - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, 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); - }); - - 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(); - - // 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. - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .project_path(cx)), - Some((worktree_id, "3.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); - }); - - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - }); - - 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); - }); - - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_auto_unfollowing( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - - // 2 clients connect to a server. - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - // Client A shares a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let _editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B starts following client A. - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let leader_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - - // When client B moves, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B edits, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B scrolls, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| { - editor.set_scroll_position(vec2f(0., 3.), cx) - }); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B activates a different pane, it continues following client A in the original pane. - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) - }); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B activates a different item in the original pane, it automatically stops following client A. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); -} - -#[gpui::test(iterations = 10)] -async fn test_peers_simultaneously_following_each_other( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a.fs().insert_tree("/a", json!({})).await; - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - - deterministic.run_until_parked(); - let client_a_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - - let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() - }); - let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_follow(client_a_id, cx).unwrap() - }); - - futures::try_join!(a_follow_b, b_follow_a).unwrap(); - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!( - workspace.leader_for_pane(workspace.active_pane()), - Some(client_b_id) - ); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!( - workspace.leader_for_pane(workspace.active_pane()), - Some(client_a_id) - ); - }); -} - #[gpui::test(iterations = 10)] async fn test_on_input_format_from_host_to_guest( deterministic: Arc, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 546b8ef407d7c809c24d76eb569053e87c3b3691..879b375cd4aefb417ce5bb71fe40d3bc87c2ac5b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1090,55 +1090,30 @@ impl CollabTitlebarItem { }, ); - match (replica_id, location) { - // If the user's location isn't known, do nothing. - (_, None) => content.into_any(), - - // If the user is not in this project, but is in another share project, - // join that project. - (None, Some(ParticipantLocation::SharedProject { project_id })) => content - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project(project_id, user_id, app_state, cx) - .detach_and_log_err(cx); - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - format!("Follow {} into external project", user.github_login), - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(), - - // Otherwise, follow the user in the current window. - _ => content - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, item, cx| { - if let Some(workspace) = item.workspace.upgrade(cx) { - if let Some(task) = workspace - .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) - { - task.detach_and_log_err(cx); - } - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - if self_following { - format!("Unfollow {}", user.github_login) - } else { - format!("Follow {}", user.github_login) - }, - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(), + if Some(peer_id) == self_peer_id { + return content.into_any(); } + + content + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + let Some(workspace) = this.workspace.upgrade(cx) else { + return; + }; + if let Some(task) = + workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx)) + { + task.detach_and_log_err(cx); + } + }) + .with_tooltip::( + peer_id.as_u64() as usize, + format!("Follow {}", user.github_login), + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .into_any() } fn location_style( diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 40adeccd11abb25fea21e057cb3a9ac18f2accdf..c12cb261c8d8c41b22fc8320934bd62102b4385d 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -222,7 +222,7 @@ impl Member { |_, _| { Label::new( format!( - "Follow {} on their active project", + "Follow {} to their active project", leader_user.github_login, ), theme diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8d9a4c155093a9d9e323f1d6adc477558f00ce1d..38773fb8cc10f143651cdb701f039efca894e259 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2529,6 +2529,7 @@ impl Workspace { if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { + dbg!("oh no!"); return None; } } @@ -2613,16 +2614,50 @@ impl Workspace { leader_id: PeerId, cx: &mut ViewContext, ) -> Option>> { + let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + let project = self.project.read(cx); + + let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { + dbg!("no remote participant yet..."); + return None; + }; + + let other_project_id = match remote_participant.location { + call::ParticipantLocation::External => None, + call::ParticipantLocation::UnsharedProject => None, + call::ParticipantLocation::SharedProject { project_id } => { + if Some(project_id) == project.remote_id() { + None + } else { + Some(project_id) + } + } + }; + dbg!(other_project_id); + + // if they are active in another project, follow there. + if let Some(project_id) = other_project_id { + let app_state = self.app_state.clone(); + return Some(crate::join_remote_project( + project_id, + remote_participant.user.id, + app_state, + cx, + )); + } + + // 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 { + dbg!("focusing pane"); cx.focus(pane); return None; } } } - // not currently following, so follow. + // Otherwise, follow. self.toggle_follow(leader_id, cx) } @@ -4214,6 +4249,7 @@ pub fn join_remote_project( app_state: Arc, cx: &mut AppContext, ) -> Task> { + dbg!("huh??"); cx.spawn(|mut cx| async move { let existing_workspace = cx .windows() @@ -4232,8 +4268,10 @@ pub fn join_remote_project( .flatten(); let workspace = if let Some(existing_workspace) = existing_workspace { + dbg!("huh"); existing_workspace } else { + dbg!("huh/"); let active_call = cx.read(ActiveCall::global); let room = active_call .read_with(&cx, |call, _| call.room().cloned()) @@ -4249,6 +4287,7 @@ pub fn join_remote_project( }) .await?; + dbg!("huh//"); let window_bounds_override = window_bounds_env_override(&cx); let window = cx.add_window( (app_state.build_window_options)( @@ -4271,6 +4310,7 @@ pub fn join_remote_project( workspace.downgrade() }; + dbg!("huh///"); workspace.window().activate(&mut cx); cx.platform().activate(true); @@ -4293,12 +4333,12 @@ pub fn join_remote_project( Some(collaborator.peer_id) }); + dbg!(follow_peer_id); + if let Some(follow_peer_id) = follow_peer_id { - if !workspace.is_being_followed(follow_peer_id) { - workspace - .toggle_follow(follow_peer_id, cx) - .map(|follow| follow.detach_and_log_err(cx)); - } + workspace + .follow(follow_peer_id, cx) + .map(|follow| follow.detach_and_log_err(cx)); } } })?; From 64a55681e615d71ae5146d16b1d6f28d2237819c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 14:32:13 +0200 Subject: [PATCH 06/27] Summarize the contents of a file using the embedding query --- crates/assistant/src/assistant_panel.rs | 1 - crates/assistant/src/prompts.rs | 458 ++++++++++++---------- crates/language/src/buffer.rs | 12 +- crates/zed/src/languages/rust/summary.scm | 6 - 4 files changed, 253 insertions(+), 224 deletions(-) delete mode 100644 crates/zed/src/languages/rust/summary.scm diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 37d0d729fe64e0def057402fb9a24b796ebdf317..816047e325c9d5377a52c380d3b1c92d3b3a983f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -578,7 +578,6 @@ impl AssistantPanel { language_name, &snapshot, language_range, - cx, codegen_kind, ); diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index ee58090d04dbff9d7da058a905c9e52b8fe1b0cd..8699c77cd13b31791a10b1fb54fcf322ed25240f 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,86 +1,118 @@ -use gpui::AppContext; +use crate::codegen::CodegenKind; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; use std::cmp; use std::ops::Range; use std::{fmt::Write, iter}; -use crate::codegen::CodegenKind; +fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { + #[derive(Debug)] + struct Match { + collapse: Range, + keep: Vec>, + } -fn outline_for_prompt( - buffer: &BufferSnapshot, - range: Range, - cx: &AppContext, -) -> Option { - let indent = buffer - .language_indent_size_at(0, cx) - .chars() - .collect::(); - let outline = buffer.outline(None)?; - let range = range.to_offset(buffer); - - let mut text = String::new(); - let mut items = outline.items.into_iter().peekable(); - - let mut intersected = false; - let mut intersection_indent = 0; - let mut extended_range = range.clone(); - - while let Some(item) = items.next() { - let item_range = item.range.to_offset(buffer); - if item_range.end < range.start || item_range.start > range.end { - text.extend(iter::repeat(indent.as_str()).take(item.depth)); - text.push_str(&item.text); - text.push('\n'); - } else { - intersected = true; - let is_terminal = items - .peek() - .map_or(true, |next_item| next_item.depth <= item.depth); - if is_terminal { - if item_range.start <= extended_range.start { - extended_range.start = item_range.start; - intersection_indent = item.depth; + let selected_range = selected_range.to_offset(buffer); + let mut matches = buffer.matches(0..buffer.len(), |grammar| { + Some(&grammar.embedding_config.as_ref()?.query) + }); + let configs = matches + .grammars() + .iter() + .map(|g| g.embedding_config.as_ref().unwrap()) + .collect::>(); + let mut matches = iter::from_fn(move || { + while let Some(mat) = matches.peek() { + let config = &configs[mat.grammar_index]; + if let Some(collapse) = mat.captures.iter().find_map(|cap| { + if Some(cap.index) == config.collapse_capture_ix { + Some(cap.node.byte_range()) + } else { + None } - extended_range.end = cmp::max(extended_range.end, item_range.end); + }) { + let mut keep = Vec::new(); + for capture in mat.captures.iter() { + if Some(capture.index) == config.keep_capture_ix { + keep.push(capture.node.byte_range()); + } else { + continue; + } + } + matches.advance(); + return Some(Match { collapse, keep }); + } else { + matches.advance(); + } + } + None + }) + .peekable(); + + let mut summary = String::new(); + let mut offset = 0; + let mut flushed_selection = false; + while let Some(mut mat) = matches.next() { + // Keep extending the collapsed range if the next match surrounds + // the current one. + while let Some(next_mat) = matches.peek() { + if next_mat.collapse.start <= mat.collapse.start + && next_mat.collapse.end >= mat.collapse.end + { + mat = matches.next().unwrap(); } else { - let name_start = item_range.start + item.name_ranges.first().unwrap().start; - let name_end = item_range.start + item.name_ranges.last().unwrap().end; + break; + } + } + + if offset >= mat.collapse.start { + // Skip collapsed nodes that have already been summarized. + offset = cmp::max(offset, mat.collapse.end); + continue; + } - if range.start > name_end { - text.extend(iter::repeat(indent.as_str()).take(item.depth)); - text.push_str(&item.text); - text.push('\n'); + if offset <= selected_range.start && selected_range.start <= mat.collapse.end { + if !flushed_selection { + // The collapsed node ends after the selection starts, so we'll flush the selection first. + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|START|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); } else { - if name_start <= extended_range.start { - extended_range.start = item_range.start; - intersection_indent = item.depth; - } - extended_range.end = cmp::max(extended_range.end, name_end); + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|END|>"); } + offset = selected_range.end; + flushed_selection = true; } - } - if intersected - && items.peek().map_or(true, |next_item| { - next_item.range.start.to_offset(buffer) > range.end - }) - { - intersected = false; - text.extend(iter::repeat(indent.as_str()).take(intersection_indent)); - text.extend(buffer.text_for_range(extended_range.start..range.start)); - text.push_str("<|START|"); - text.extend(buffer.text_for_range(range.clone())); - if range.start != range.end { - text.push_str("|END|>"); - } else { - text.push_str(">"); + // If the selection intersects the collapsed node, we won't collapse it. + if selected_range.end >= mat.collapse.start { + continue; } - text.extend(buffer.text_for_range(range.end..extended_range.end)); - text.push('\n'); } + + summary.extend(buffer.text_for_range(offset..mat.collapse.start)); + for keep in mat.keep { + summary.extend(buffer.text_for_range(keep)); + } + offset = mat.collapse.end; + } + + // Flush selection if we haven't already done so. + if !flushed_selection && offset <= selected_range.start { + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|START|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|END|>"); + } + offset = selected_range.end; } - Some(text) + summary.extend(buffer.text_for_range(offset..buffer.len())); + summary } pub fn generate_content_prompt( @@ -88,7 +120,6 @@ pub fn generate_content_prompt( language_name: Option<&str>, buffer: &BufferSnapshot, range: Range, - cx: &AppContext, kind: CodegenKind, ) -> String { let mut prompt = String::new(); @@ -100,19 +131,17 @@ pub fn generate_content_prompt( writeln!(prompt, "You're an expert engineer.\n").unwrap(); } - let outline = outline_for_prompt(buffer, range.clone(), cx); - if let Some(outline) = outline { - writeln!( - prompt, - "The file you are currently working on has the following outline:" - ) - .unwrap(); - if let Some(language_name) = language_name { - let language_name = language_name.to_lowercase(); - writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); - } else { - writeln!(prompt, "```\n{outline}\n```").unwrap(); - } + let outline = summarize(buffer, range.clone()); + writeln!( + prompt, + "The file you are currently working on has the following outline:" + ) + .unwrap(); + if let Some(language_name) = language_name { + let language_name = language_name.to_lowercase(); + writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + } else { + writeln!(prompt, "```\n{outline}\n```").unwrap(); } // Assume for now that we are just generating @@ -183,39 +212,37 @@ pub(crate) mod tests { }, Some(tree_sitter_rust::language()), ) - .with_indents_query( + .with_embedding_query( r#" - (call_expression) @indent - (field_expression) @indent - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap() - .with_outline_query( - r#" - (struct_item - "struct" @context - name: (_) @name) @item - (enum_item - "enum" @context - name: (_) @name) @item - (enum_variant - name: (_) @name) @item - (field_declaration - name: (_) @name) @item - (impl_item - "impl" @context - trait: (_)? @name - "for"? @context - type: (_) @name) @item - (function_item - "fn" @context - name: (_) @name) @item - (mod_item - "mod" @context - name: (_) @name) @item - "#, + ( + [(line_comment) (attribute_item)]* @context + . + [ + (struct_item + name: (_) @name) + + (enum_item + name: (_) @name) + + (impl_item + trait: (_)? @name + "for"? @name + type: (_) @name) + + (trait_item + name: (_) @name) + + (function_item + name: (_) @name + body: (block + "{" @keep + "}" @keep) @collapse) + + (macro_definition + name: (_) @name) + ] @item + ) + "#, ) .unwrap() } @@ -251,132 +278,133 @@ pub(crate) mod tests { cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); let snapshot = buffer.read(cx).snapshot(); - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_before(Point::new(1, 4)), - cx, - ); assert_eq!( - outline.as_deref(), - Some(indoc! {" - struct X - <|START|>a: usize - b - impl X - fn new - fn a - fn b - "}) - ); + summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)), + indoc! {" + struct X { + <|START|>a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(8, 14)), - cx, + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} ); + assert_eq!( - outline.as_deref(), - Some(indoc! {" - struct X - a - b - impl X + summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + fn new() -> Self { let <|START|a |END|>= 1; let b = 2; Self { a, b } } - fn a - fn b - "}) - ); - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(6, 0))..snapshot.anchor_before(Point::new(6, 0)), - cx, + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} ); + assert_eq!( - outline.as_deref(), - Some(indoc! {" - struct X - a - b - impl X + summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { <|START|> - fn new - fn a - fn b - "}) - ); + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(13, 9)), - cx, + pub fn b(&self) -> usize {} + } + "} ); + assert_eq!( - outline.as_deref(), - Some(indoc! {" - struct X - a - b - impl X - fn new() -> Self { - let <|START|a = 1; - let b = 2; - Self { a, b } - } + summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } - pub f|END|>n a(&self, param: bool) -> usize { - self.a - } - fn b - "}) - ); + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(5, 6))..snapshot.anchor_before(Point::new(12, 0)), - cx, + pub fn b(&self) -> usize {} + } + <|START|>"} ); - assert_eq!( - outline.as_deref(), - Some(indoc! {" - struct X - a - b - impl X<|START| { - fn new() -> Self { - let a = 1; - let b = 2; - Self { a, b } + // Ensure nested functions get collapsed properly. + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + let a = 30; + fn nested() -> usize { + 3 } - |END|> - fn a - fn b - "}) - ); + self.a + nested() + } - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(18, 8))..snapshot.anchor_before(Point::new(18, 8)), - cx, - ); + pub fn b(&self) -> usize { + self.b + } + } + "}; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let snapshot = buffer.read(cx).snapshot(); assert_eq!( - outline.as_deref(), - Some(indoc! {" - struct X - a - b - impl X - fn new - fn a - pub fn b(&self) -> usize { - <|START|>self.b - } - "}) + summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), + indoc! {" + <|START|>struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} ); } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 38b2842c127f2a6ade0d43787a24a0c76ff13374..27b01543e1e3f04f9914b1da5c530ddd26a555c1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -8,8 +8,8 @@ use crate::{ language_settings::{language_settings, LanguageSettings}, outline::OutlineItem, syntax_map::{ - SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, - ToTreeSitterPoint, + SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, + SyntaxSnapshot, ToTreeSitterPoint, }, CodeLabel, LanguageScope, Outline, }; @@ -2467,6 +2467,14 @@ impl BufferSnapshot { Some(items) } + pub fn matches( + &self, + range: Range, + query: fn(&Grammar) -> Option<&tree_sitter::Query>, + ) -> SyntaxMapMatches { + self.syntax.matches(range, self, query) + } + /// Returns bracket range pairs overlapping or adjacent to `range` pub fn bracket_ranges<'a, T: ToOffset>( &'a self, diff --git a/crates/zed/src/languages/rust/summary.scm b/crates/zed/src/languages/rust/summary.scm deleted file mode 100644 index 7174eec3c384336d9c2d647428d60d0eb82dea2a..0000000000000000000000000000000000000000 --- a/crates/zed/src/languages/rust/summary.scm +++ /dev/null @@ -1,6 +0,0 @@ -(function_item - body: (block - "{" @keep - "}" @keep) @collapse) - -(use_declaration) @collapse From df7ac9b815423c8e9d8afdf5830a55177318a20a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 14:36:16 +0200 Subject: [PATCH 07/27] :lipstick: --- crates/assistant/src/assistant_panel.rs | 2 -- crates/assistant/src/prompts.rs | 9 ++------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 816047e325c9d5377a52c380d3b1c92d3b3a983f..4a4dc087904aee4314fa05f796ec9c5d17b64b1d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -581,8 +581,6 @@ impl AssistantPanel { codegen_kind, ); - dbg!(&prompt); - let mut messages = Vec::new(); let mut model = settings::get::(cx) .default_open_ai_model diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 8699c77cd13b31791a10b1fb54fcf322ed25240f..0646534e011dd5a457ba9765c343a44ad5ffd782 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -144,15 +144,9 @@ pub fn generate_content_prompt( writeln!(prompt, "```\n{outline}\n```").unwrap(); } - // Assume for now that we are just generating - if range.clone().start == range.end { - writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); - } else { - writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); - } - match kind { CodegenKind::Generate { position: _ } => { + writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); writeln!( prompt, "Assume the cursor is located where the `<|START|` marker is." @@ -170,6 +164,7 @@ pub fn generate_content_prompt( .unwrap(); } CodegenKind::Transform { range: _ } => { + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); writeln!( prompt, "Modify the users code selected text based upon the users prompt: {user_prompt}" From f52200a340649eac2e65895f01f83891e32891a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 15:21:58 +0200 Subject: [PATCH 08/27] Prevent deploying the inline assistant when selection spans multiple excerpts --- crates/assistant/src/assistant_panel.rs | 40 ++++++++++++++----------- crates/assistant/src/prompts.rs | 4 +-- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 4a4dc087904aee4314fa05f796ec9c5d17b64b1d..0d9f69011e3d0d53dbeca52d3902728fcf582079 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -274,13 +274,17 @@ impl AssistantPanel { return; }; + let selection = editor.read(cx).selections.newest_anchor().clone(); + if selection.start.excerpt_id() != selection.end.excerpt_id() { + return; + } + 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 selection = editor.read(cx).selections.newest_anchor().clone(); let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { CodegenKind::Generate { position: selection.start, @@ -542,25 +546,25 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } - let multi_buffer = editor.read(cx).buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let snapshot = if multi_buffer.is_singleton() { - multi_buffer.as_singleton().unwrap().read(cx).snapshot() + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let range = pending_assist.codegen.read(cx).range(); + let start = snapshot.point_to_buffer_offset(range.start); + let end = snapshot.point_to_buffer_offset(range.end); + let (buffer, range) = if let Some((start, end)) = start.zip(end) { + let (start_buffer, start_buffer_offset) = start; + let (end_buffer, end_buffer_offset) = end; + if start_buffer.remote_id() == end_buffer.remote_id() { + (start_buffer, start_buffer_offset..end_buffer_offset) + } else { + self.finish_inline_assist(inline_assist_id, false, cx); + return; + } } else { + self.finish_inline_assist(inline_assist_id, false, cx); return; }; - let range = pending_assist.codegen.read(cx).range(); - let language_range = snapshot.anchor_at( - range.start.to_offset(&multi_buffer_snapshot), - language::Bias::Left, - ) - ..snapshot.anchor_at( - range.end.to_offset(&multi_buffer_snapshot), - language::Bias::Right, - ); - - let language = snapshot.language_at(language_range.start); + let language = buffer.language_at(range.start); let language_name = if let Some(language) = language.as_ref() { if Arc::ptr_eq(language, &language::PLAIN_TEXT) { None @@ -576,8 +580,8 @@ impl AssistantPanel { let prompt = generate_content_prompt( user_prompt.to_string(), language_name, - &snapshot, - language_range, + &buffer, + range, codegen_kind, ); diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 0646534e011dd5a457ba9765c343a44ad5ffd782..2451369a184b18c312efa1a829b9c37c40e06579 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -119,7 +119,7 @@ pub fn generate_content_prompt( user_prompt: String, language_name: Option<&str>, buffer: &BufferSnapshot, - range: Range, + range: Range, kind: CodegenKind, ) -> String { let mut prompt = String::new(); @@ -131,7 +131,7 @@ pub fn generate_content_prompt( writeln!(prompt, "You're an expert engineer.\n").unwrap(); } - let outline = summarize(buffer, range.clone()); + let outline = summarize(buffer, range); writeln!( prompt, "The file you are currently working on has the following outline:" From a785eb914140bd83e1d0780929261d9f46157848 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:24:09 +0200 Subject: [PATCH 09/27] auto-update: Link to the current release's changelog, not the latest one (#3076) An user complained in zed-industries/community#2093 that we always link to the latest release changelog, not the one that they've just updated to. Release Notes: - Fixed changelog link in update notification always leading to the latest release changelog, not the one that was updated to. Fixes zed-industries/community#2093. --- crates/auto_update/src/auto_update.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 822886b58018e1cb8cef694a2cd2b8274a20c949..0d537b882a85fe5e7ce54f1270c8d7b28de1f9c4 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) { fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { - let server_url = &auto_updater.read(cx).server_url; + let auto_updater = auto_updater.read(cx); + let server_url = &auto_updater.server_url; + let current_version = auto_updater.current_version; let latest_release_url = if cx.has_global::() && *cx.global::() == ReleaseChannel::Preview { - format!("{server_url}/releases/preview/latest") + format!("{server_url}/releases/preview/{current_version}") } else { - format!("{server_url}/releases/stable/latest") + format!("{server_url}/releases/stable/{current_version}") }; cx.platform().open_url(&latest_release_url); } From d70014cfd065dcc65823ab8c0c465edf5dff5d08 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 15:36:10 +0200 Subject: [PATCH 10/27] Summarize file in the background --- crates/assistant/src/assistant_panel.rs | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 0d9f69011e3d0d53dbeca52d3902728fcf582079..b69c12a2a328ed8643315f091be11d764dcdc00d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -546,15 +546,16 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } + let codegen = pending_assist.codegen.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let range = pending_assist.codegen.read(cx).range(); + let range = codegen.read(cx).range(); let start = snapshot.point_to_buffer_offset(range.start); let end = snapshot.point_to_buffer_offset(range.end); let (buffer, range) = if let Some((start, end)) = start.zip(end) { let (start_buffer, start_buffer_offset) = start; let (end_buffer, end_buffer_offset) = end; if start_buffer.remote_id() == end_buffer.remote_id() { - (start_buffer, start_buffer_offset..end_buffer_offset) + (start_buffer.clone(), start_buffer_offset..end_buffer_offset) } else { self.finish_inline_assist(inline_assist_id, false, cx); return; @@ -574,17 +575,13 @@ impl AssistantPanel { } else { None }; - let language_name = language_name.as_deref(); - - let codegen_kind = pending_assist.codegen.read(cx).kind().clone(); - let prompt = generate_content_prompt( - user_prompt.to_string(), - language_name, - &buffer, - range, - codegen_kind, - ); + 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 @@ -600,18 +597,21 @@ impl AssistantPanel { model = conversation.model.clone(); } - messages.push(RequestMessage { - role: Role::User, - content: prompt, - }); - let request = OpenAIRequest { - model: model.full_name().into(), - messages, - stream: true, - }; - pending_assist - .codegen - .update(cx, |codegen, cx| codegen.start(request, cx)); + cx.spawn(|_, mut cx| async move { + let prompt = prompt.await; + + messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); + let request = OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + }; + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + }) + .detach(); } fn update_highlights_for_editor( From bf5d9e32240e5752630988fc99df5f7c82031660 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 17:50:52 +0200 Subject: [PATCH 11/27] Sort matches before processing them --- crates/assistant/src/prompts.rs | 63 ++++++++++----------- crates/zed/src/languages/rust/embedding.scm | 3 + 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 2451369a184b18c312efa1a829b9c37c40e06579..bf041dff523d57d62cfbc3f312a350ad4766d160 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,8 +1,8 @@ use crate::codegen::CodegenKind; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; -use std::cmp; +use std::cmp::{self, Reverse}; +use std::fmt::Write; use std::ops::Range; -use std::{fmt::Write, iter}; fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] @@ -12,59 +12,58 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> S } let selected_range = selected_range.to_offset(buffer); - let mut matches = buffer.matches(0..buffer.len(), |grammar| { + let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| { Some(&grammar.embedding_config.as_ref()?.query) }); - let configs = matches + let configs = ts_matches .grammars() .iter() .map(|g| g.embedding_config.as_ref().unwrap()) .collect::>(); - let mut matches = iter::from_fn(move || { - while let Some(mat) = matches.peek() { - let config = &configs[mat.grammar_index]; - if let Some(collapse) = mat.captures.iter().find_map(|cap| { - if Some(cap.index) == config.collapse_capture_ix { - Some(cap.node.byte_range()) + let mut matches = Vec::new(); + while let Some(mat) = ts_matches.peek() { + let config = &configs[mat.grammar_index]; + if let Some(collapse) = mat.captures.iter().find_map(|cap| { + if Some(cap.index) == config.collapse_capture_ix { + Some(cap.node.byte_range()) + } else { + None + } + }) { + let mut keep = Vec::new(); + for capture in mat.captures.iter() { + if Some(capture.index) == config.keep_capture_ix { + keep.push(capture.node.byte_range()); } else { - None - } - }) { - let mut keep = Vec::new(); - for capture in mat.captures.iter() { - if Some(capture.index) == config.keep_capture_ix { - keep.push(capture.node.byte_range()); - } else { - continue; - } + continue; } - matches.advance(); - return Some(Match { collapse, keep }); - } else { - matches.advance(); } + ts_matches.advance(); + matches.push(Match { collapse, keep }); + } else { + ts_matches.advance(); } - None - }) - .peekable(); + } + matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end))); + let mut matches = matches.into_iter().peekable(); let mut summary = String::new(); let mut offset = 0; let mut flushed_selection = false; - while let Some(mut mat) = matches.next() { + while let Some(mat) = matches.next() { // Keep extending the collapsed range if the next match surrounds // the current one. while let Some(next_mat) = matches.peek() { - if next_mat.collapse.start <= mat.collapse.start - && next_mat.collapse.end >= mat.collapse.end + if mat.collapse.start <= next_mat.collapse.start + && mat.collapse.end >= next_mat.collapse.end { - mat = matches.next().unwrap(); + matches.next().unwrap(); } else { break; } } - if offset >= mat.collapse.start { + if offset > mat.collapse.start { // Skip collapsed nodes that have already been summarized. offset = cmp::max(offset, mat.collapse.end); continue; diff --git a/crates/zed/src/languages/rust/embedding.scm b/crates/zed/src/languages/rust/embedding.scm index e4218382a9b1ceb7e087b0d9247d5a4e66b77236..c4ed7d20976fb9c56f39aec1c8a32bba5f405f15 100644 --- a/crates/zed/src/languages/rust/embedding.scm +++ b/crates/zed/src/languages/rust/embedding.scm @@ -2,6 +2,9 @@ [(line_comment) (attribute_item)]* @context . [ + (attribute_item) @collapse + (use_declaration) @collapse + (struct_item name: (_) @name) From 9dc292772af147d27ef0b75d228543f3e818408b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 09:53:30 -0600 Subject: [PATCH 12/27] Add a screen for gpui tests Allows me to test notifications --- crates/gpui/src/platform/test.rs | 35 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index e8579a0006014585024d03a1b875183a66233169..7b4813ffa991819789d3573dbb0ad4df73b1fa4b 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -103,6 +103,7 @@ pub struct Platform { current_clipboard_item: Mutex>, cursor: Mutex, active_window: Arc>>, + active_screen: Screen, } impl Platform { @@ -113,6 +114,7 @@ impl Platform { current_clipboard_item: Default::default(), cursor: Mutex::new(CursorStyle::Arrow), active_window: Default::default(), + active_screen: Screen::new(), } } } @@ -136,12 +138,16 @@ impl super::Platform for Platform { fn quit(&self) {} - fn screen_by_id(&self, _id: uuid::Uuid) -> Option> { - None + fn screen_by_id(&self, uuid: uuid::Uuid) -> Option> { + if self.active_screen.uuid == uuid { + Some(Rc::new(self.active_screen.clone())) + } else { + None + } } fn screens(&self) -> Vec> { - Default::default() + vec![Rc::new(self.active_screen.clone())] } fn open_window( @@ -158,6 +164,7 @@ impl super::Platform for Platform { WindowBounds::Fixed(rect) => rect.size(), }, self.active_window.clone(), + Rc::new(self.active_screen.clone()), )) } @@ -170,6 +177,7 @@ impl super::Platform for Platform { handle, vec2f(24., 24.), self.active_window.clone(), + Rc::new(self.active_screen.clone()), )) } @@ -238,8 +246,18 @@ impl super::Platform for Platform { fn restart(&self) {} } -#[derive(Debug)] -pub struct Screen; +#[derive(Debug, Clone)] +pub struct Screen { + uuid: uuid::Uuid, +} + +impl Screen { + fn new() -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + } + } +} impl super::Screen for Screen { fn as_any(&self) -> &dyn Any { @@ -255,7 +273,7 @@ impl super::Screen for Screen { } fn display_uuid(&self) -> Option { - Some(uuid::Uuid::new_v4()) + Some(self.uuid) } } @@ -275,6 +293,7 @@ pub struct Window { pub(crate) edited: bool, pub(crate) pending_prompts: RefCell>>, active_window: Arc>>, + screen: Rc, } impl Window { @@ -282,6 +301,7 @@ impl Window { handle: AnyWindowHandle, size: Vector2F, active_window: Arc>>, + screen: Rc, ) -> Self { Self { handle, @@ -299,6 +319,7 @@ impl Window { edited: false, pending_prompts: Default::default(), active_window, + screen, } } @@ -329,7 +350,7 @@ impl super::Window for Window { } fn screen(&self) -> Rc { - Rc::new(Screen) + self.screen.clone() } fn mouse_position(&self) -> Vector2F { From 39af2bb0a45e1ad272e16ac92b8cf90ec3e776a1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 10:56:32 -0600 Subject: [PATCH 13/27] Ensure notifications are dismissed Before this change if you joined a project without clicking on the notification it would never disappear. Fix a related bug where if you have more than one monitor, the notification was only dismissed from one of them. --- crates/call/src/room.rs | 7 ++++ crates/collab/src/tests/following_tests.rs | 39 ++++++++++++++++--- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 2 +- .../src/project_shared_notification.rs | 21 ++++++++-- crates/workspace/src/workspace.rs | 11 ------ 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 26a531cc31d47721271689a6407afebf27237429..130a7a64f09ca8421d61faf37af561e7f1a57d6a 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -44,6 +44,12 @@ pub enum Event { RemoteProjectUnshared { project_id: u64, }, + RemoteProjectJoined { + project_id: u64, + }, + RemoteProjectInvitationDiscarded { + project_id: u64, + }, Left, } @@ -1015,6 +1021,7 @@ impl Room { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.spawn(|this, mut cx| async move { let project = Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index d7acae39950654b1b7cb84ccb17c6f4004730566..696923e505bc3eebea1d09e47d0e367c842c8287 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,7 +1,10 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; +use collab_ui::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; -use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; +use gpui::{ + executor::Deterministic, geometry::vector::vec2f, AppContext, TestAppContext, ViewHandle, +}; use live_kit_client::MacOSDisplay; use serde_json::json; use std::sync::Arc; @@ -1073,6 +1076,24 @@ 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, @@ -1126,17 +1147,22 @@ async fn test_following_across_workspaces( let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); + cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); + let project_a_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); + /* let project_b_id = active_call_b .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + */ active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) @@ -1157,7 +1183,9 @@ async fn test_following_across_workspaces( .unwrap(); deterministic.run_until_parked(); - assert_eq!(cx_b.windows().len(), 1); + assert_eq!(cx_b.windows().len(), 2); + + assert_eq!(visible_push_notifications(cx_b).len(), 1); workspace_b.update(cx_b, |workspace, cx| { workspace @@ -1186,4 +1214,5 @@ async fn test_following_across_workspaces( }); // assert that there are no share notifications open + assert_eq!(visible_push_notifications(cx_b).len(), 0); } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 879b375cd4aefb417ce5bb71fe40d3bc87c2ac5b..d85aca164a412b3651fbdd197ef4c3418ec8bb93 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -948,7 +948,7 @@ impl CollabTitlebarItem { fn render_face_pile( &self, user: &User, - replica_id: Option, + _replica_id: Option, peer_id: PeerId, location: Option, muted: bool, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 84a9b3b6b6427a9b0383e59657e265d0963afd10..57d6f7b4f6b7e17b9426c75cbac2e9b80491c048 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -7,7 +7,7 @@ mod face_pile; mod incoming_call_notification; mod notifications; mod panel_settings; -mod project_shared_notification; +pub mod project_shared_notification; mod sharing_status_indicator; use call::{report_call_event_for_room, ActiveCall, Room}; diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 21fa7d4ee6e84babb4d2e778134758812e7fb872..5e362403f04f040855c76057175cd21e56a1f641 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -40,7 +40,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { .push(window); } } - room::Event::RemoteProjectUnshared { project_id } => { + room::Event::RemoteProjectUnshared { project_id } + | room::Event::RemoteProjectInvitationDiscarded { project_id } => { if let Some(windows) = notification_windows.remove(&project_id) { for window in windows { window.remove(cx); @@ -54,6 +55,13 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } } + room::Event::RemoteProjectJoined { project_id } => { + if let Some(windows) = notification_windows.remove(&project_id) { + for window in windows { + window.remove(cx); + } + } + } _ => {} }) .detach(); @@ -82,7 +90,6 @@ impl ProjectSharedNotification { } fn join(&mut self, cx: &mut ViewContext) { - cx.remove_window(); if let Some(app_state) = self.app_state.upgrade() { workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) .detach_and_log_err(cx); @@ -90,7 +97,15 @@ impl ProjectSharedNotification { } fn dismiss(&mut self, cx: &mut ViewContext) { - cx.remove_window(); + if let Some(active_room) = + ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned()) + { + active_room.update(cx, |_, cx| { + cx.emit(room::Event::RemoteProjectInvitationDiscarded { + project_id: self.project_id, + }); + }); + } } fn render_owner(&self, cx: &mut ViewContext) -> AnyElement { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 38773fb8cc10f143651cdb701f039efca894e259..c90b1753201668f21add8a9a133b251b31f89be3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2529,7 +2529,6 @@ impl Workspace { if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { - dbg!("oh no!"); return None; } } @@ -2618,7 +2617,6 @@ impl Workspace { let project = self.project.read(cx); let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { - dbg!("no remote participant yet..."); return None; }; @@ -2633,7 +2631,6 @@ impl Workspace { } } }; - dbg!(other_project_id); // if they are active in another project, follow there. if let Some(project_id) = other_project_id { @@ -2650,7 +2647,6 @@ impl Workspace { 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 { - dbg!("focusing pane"); cx.focus(pane); return None; } @@ -4249,7 +4245,6 @@ pub fn join_remote_project( app_state: Arc, cx: &mut AppContext, ) -> Task> { - dbg!("huh??"); cx.spawn(|mut cx| async move { let existing_workspace = cx .windows() @@ -4268,10 +4263,8 @@ pub fn join_remote_project( .flatten(); let workspace = if let Some(existing_workspace) = existing_workspace { - dbg!("huh"); existing_workspace } else { - dbg!("huh/"); let active_call = cx.read(ActiveCall::global); let room = active_call .read_with(&cx, |call, _| call.room().cloned()) @@ -4287,7 +4280,6 @@ pub fn join_remote_project( }) .await?; - dbg!("huh//"); let window_bounds_override = window_bounds_env_override(&cx); let window = cx.add_window( (app_state.build_window_options)( @@ -4310,7 +4302,6 @@ pub fn join_remote_project( workspace.downgrade() }; - dbg!("huh///"); workspace.window().activate(&mut cx); cx.platform().activate(true); @@ -4333,8 +4324,6 @@ pub fn join_remote_project( Some(collaborator.peer_id) }); - dbg!(follow_peer_id); - if let Some(follow_peer_id) = follow_peer_id { workspace .follow(follow_peer_id, cx) From 7f44083a969aa0232b42353bcb06a88462d41be5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 11:03:55 -0600 Subject: [PATCH 14/27] Remove unused function --- crates/client/src/client.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 4ddfbc5a3478a100f3ddc7e309708b3aed9b964c..5eae700404c316cdf55d44cccb24470ded092c93 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -453,10 +453,6 @@ impl Client { self.state.read().status.1.clone() } - pub fn is_connected(&self) -> bool { - matches!(&*self.status().borrow(), Status::Connected { .. }) - } - fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); From 3d68fcad0bed07e4ce151bb2c3d41ccdea450c0b Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 2 Oct 2023 13:18:49 -0400 Subject: [PATCH 15/27] Detach completion confirmation task when selecting with mouse Otherwise the spawn to resolve the additional edits never runs causing autocomplete to never add imports automatically when clicking with the mouse --- crates/editor/src/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ca19ad24cb7761bcf1a169d846366946190c54d8..e0b8af1c71cecab808b443efeadb80593afc0358 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1057,7 +1057,8 @@ impl CompletionsMenu { item_ix: Some(item_ix), }, cx, - ); + ) + .map(|task| task.detach()); }) .into_any(), ); From 9e1f7c4c18b53d98bcf9927bdf1144d6ff78c397 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 2 Oct 2023 18:20:47 -0400 Subject: [PATCH 16/27] Mainline GPUI2 UI work (#3079) This PR mainlines the current state of new GPUI2-based UI from the `gpui2-ui` branch. Release Notes: - N/A --------- Co-authored-by: Nate Butler Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Nate --- Cargo.lock | 4 +- crates/fs/Cargo.toml | 6 +- crates/fs/src/fs.rs | 27 - crates/gpui/Cargo.toml | 2 +- crates/project/src/project.rs | 27 +- crates/storybook/Cargo.toml | 2 + .../src/stories/components/breadcrumb.rs | 33 +- .../src/stories/components/buffer.rs | 8 +- .../src/stories/components/chat_panel.rs | 48 +- .../src/stories/components/facepile.rs | 2 +- .../storybook/src/stories/components/panel.rs | 7 +- .../src/stories/components/project_panel.rs | 8 +- .../src/stories/components/tab_bar.rs | 34 +- .../src/stories/components/toolbar.rs | 60 ++- .../storybook/src/stories/elements/avatar.rs | 2 +- crates/storybook/src/stories/elements/icon.rs | 2 +- crates/storybook/src/stories/kitchen_sink.rs | 3 + crates/storybook/src/storybook.rs | 39 +- crates/ui/Cargo.toml | 1 + crates/ui/src/components.rs | 4 +- crates/ui/src/components/breadcrumb.rs | 60 ++- crates/ui/src/components/buffer.rs | 40 +- crates/ui/src/components/chat_panel.rs | 75 +-- crates/ui/src/components/editor.rs | 25 - crates/ui/src/components/editor_pane.rs | 60 +++ crates/ui/src/components/panel.rs | 16 +- crates/ui/src/components/player_stack.rs | 9 +- crates/ui/src/components/project_panel.rs | 87 ++-- crates/ui/src/components/tab.rs | 2 +- crates/ui/src/components/tab_bar.rs | 57 +-- crates/ui/src/components/terminal.rs | 11 +- crates/ui/src/components/title_bar.rs | 29 +- crates/ui/src/components/toolbar.rs | 40 +- crates/ui/src/components/workspace.rs | 101 ++-- crates/ui/src/elements/icon.rs | 2 + crates/ui/src/elements/input.rs | 1 + crates/ui/src/elements/player.rs | 3 +- crates/ui/src/prelude.rs | 19 +- crates/ui/src/static_data.rs | 482 ++++++++++++++++-- 39 files changed, 1043 insertions(+), 395 deletions(-) delete mode 100644 crates/ui/src/components/editor.rs create mode 100644 crates/ui/src/components/editor_pane.rs diff --git a/Cargo.lock b/Cargo.lock index 76de671620a2539ce9e1f37b085c9cbbb1b30ad2..f146c4b8d5bee026b8c0440db9ce0a4db9724ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2790,7 +2790,6 @@ dependencies = [ "lazy_static", "libc", "log", - "lsp", "parking_lot 0.11.2", "regex", "rope", @@ -7403,6 +7402,8 @@ dependencies = [ "anyhow", "chrono", "clap 4.4.4", + "fs", + "futures 0.3.28", "gpui2", "itertools 0.11.0", "log", @@ -8638,6 +8639,7 @@ dependencies = [ "anyhow", "chrono", "gpui2", + "rand 0.8.5", "serde", "settings", "smallvec", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 78146c3a9dbc5e63de61fa1e253ebf16cbebfcd8..441ce6f9c7b678101d676d7e8f31a90143c42d3b 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -9,8 +9,6 @@ path = "src/fs.rs" [dependencies] collections = { path = "../collections" } -gpui = { path = "../gpui" } -lsp = { path = "../lsp" } rope = { path = "../rope" } text = { path = "../text" } util = { path = "../util" } @@ -34,8 +32,10 @@ log.workspace = true libc = "0.2" time.workspace = true +gpui = { path = "../gpui", optional = true} + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } [features] -test-support = [] +test-support = ["gpui/test-support"] diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 97175cb55e7f1c8edb494857d1e28ad16d4ee6d1..1d95db9b6cf20ddbe1cf87c5c94c73eb0e666d62 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -93,33 +93,6 @@ pub struct Metadata { pub is_dir: bool, } -impl From for CreateOptions { - fn from(options: lsp::CreateFileOptions) -> Self { - Self { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - } - } -} - -impl From for RenameOptions { - fn from(options: lsp::RenameFileOptions) -> Self { - Self { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - } - } -} - -impl From for RemoveOptions { - fn from(options: lsp::DeleteFileOptions) -> Self { - Self { - recursive: options.recursive.unwrap_or(false), - ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), - } - } -} - pub struct RealFs; #[async_trait::async_trait] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 95b7ccb559980e746eeaaa3a5eb07b73166997b4..6aeef558c03fc5c05bc82e0253702af42d9e0c85 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -11,7 +11,7 @@ path = "src/gpui.rs" doctest = false [features] -test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"] +test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"] [dependencies] collections = { path = "../collections" } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee8690ea709c8e7e9f5fd6ba5297ea624d7cd245..11945931578fece1b1b02c5da8a91e3c7f5072d8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4957,8 +4957,16 @@ impl Project { if abs_path.ends_with("/") { fs.create_dir(&abs_path).await?; } else { - fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default()) - .await?; + fs.create_file( + &abs_path, + op.options + .map(|options| fs::CreateOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), + ) + .await?; } } @@ -4974,7 +4982,12 @@ impl Project { fs.rename( &source_abs_path, &target_abs_path, - op.options.map(Into::into).unwrap_or_default(), + op.options + .map(|options| fs::RenameOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), ) .await?; } @@ -4984,7 +4997,13 @@ impl Project { .uri .to_file_path() .map_err(|_| anyhow!("can't convert URI to path"))?; - let options = op.options.map(Into::into).unwrap_or_default(); + let options = op + .options + .map(|options| fs::RemoveOptions { + recursive: options.recursive.unwrap_or(false), + ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), + }) + .unwrap_or_default(); if abs_path.ends_with("/") { fs.remove_dir(&abs_path, options).await?; } else { diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 73c83616139f1d395753cd2c22c507c26655a60e..43890dd01a78f1b34ab7cb5f65ae67a17c3f3d23 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -12,6 +12,8 @@ path = "src/storybook.rs" anyhow.workspace = true clap = { version = "4.4", features = ["derive", "string"] } chrono = "0.4" +fs = { path = "../fs" } +futures.workspace = true gpui2 = { path = "../gpui2" } itertools = "0.11.0" log.workspace = true diff --git a/crates/storybook/src/stories/components/breadcrumb.rs b/crates/storybook/src/stories/components/breadcrumb.rs index 8d144c01741ee189d8727376093588a839c3d874..002b6140e13e98731ff40be7ddefcdc11ab4bf67 100644 --- a/crates/storybook/src/stories/components/breadcrumb.rs +++ b/crates/storybook/src/stories/components/breadcrumb.rs @@ -1,5 +1,8 @@ +use std::path::PathBuf; +use std::str::FromStr; + use ui::prelude::*; -use ui::Breadcrumb; +use ui::{Breadcrumb, HighlightedText, Symbol}; use crate::story::Story; @@ -8,9 +11,35 @@ pub struct BreadcrumbStory {} impl BreadcrumbStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + Story::container(cx) .child(Story::title_for::<_, Breadcrumb>(cx)) .child(Story::label(cx, "Default")) - .child(Breadcrumb::new()) + .child(Breadcrumb::new( + PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), + vec![ + Symbol(vec![ + HighlightedText { + text: "impl ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "BreadcrumbStory".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "render".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + ], + )) } } diff --git a/crates/storybook/src/stories/components/buffer.rs b/crates/storybook/src/stories/components/buffer.rs index 8d9e70a2828287e1d8d4816c32c95f198e24f272..0b3268421bf3ace9a429c68fc23ff9f9baa16b47 100644 --- a/crates/storybook/src/stories/components/buffer.rs +++ b/crates/storybook/src/stories/components/buffer.rs @@ -12,8 +12,10 @@ pub struct BufferStory {} impl BufferStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + Story::container(cx) - .child(Story::title_for::<_, Buffer>(cx)) + .child(Story::title_for::<_, Buffer>(cx)) .child(Story::label(cx, "Default")) .child(div().w(rems(64.)).h_96().child(empty_buffer_example())) .child(Story::label(cx, "Hello World (Rust)")) @@ -21,14 +23,14 @@ impl BufferStory { div() .w(rems(64.)) .h_96() - .child(hello_world_rust_buffer_example(cx)), + .child(hello_world_rust_buffer_example(&theme)), ) .child(Story::label(cx, "Hello World (Rust) with Status")) .child( div() .w(rems(64.)) .h_96() - .child(hello_world_rust_buffer_with_status_example(cx)), + .child(hello_world_rust_buffer_with_status_example(&theme)), ) } } diff --git a/crates/storybook/src/stories/components/chat_panel.rs b/crates/storybook/src/stories/components/chat_panel.rs index 804290b7ca3bc3b1b3e554a36cb02e0d06299307..e87ac0afa29cc60fbb61a674517d8e4bb1381b29 100644 --- a/crates/storybook/src/stories/components/chat_panel.rs +++ b/crates/storybook/src/stories/components/chat_panel.rs @@ -1,6 +1,6 @@ use chrono::DateTime; use ui::prelude::*; -use ui::{ChatMessage, ChatPanel}; +use ui::{ChatMessage, ChatPanel, Panel}; use crate::story::Story; @@ -12,23 +12,35 @@ impl ChatPanelStory { Story::container(cx) .child(Story::title_for::<_, ChatPanel>(cx)) .child(Story::label(cx, "Default")) - .child(ChatPanel::new(ScrollState::default())) + .child(Panel::new( + ScrollState::default(), + |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()], + Box::new(()), + )) .child(Story::label(cx, "With Mesages")) - .child(ChatPanel::new(ScrollState::default()).with_messages(vec![ - ChatMessage::new( - "osiewicz".to_string(), - "is this thing on?".to_string(), - DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - .unwrap() - .naive_local(), - ), - ChatMessage::new( - "maxdeviant".to_string(), - "Reading you loud and clear!".to_string(), - DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - .unwrap() - .naive_local(), - ), - ])) + .child(Panel::new( + ScrollState::default(), + |_, _| { + vec![ChatPanel::new(ScrollState::default()) + .with_messages(vec![ + ChatMessage::new( + "osiewicz".to_string(), + "is this thing on?".to_string(), + DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") + .unwrap() + .naive_local(), + ), + ChatMessage::new( + "maxdeviant".to_string(), + "Reading you loud and clear!".to_string(), + DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") + .unwrap() + .naive_local(), + ), + ]) + .into_any()] + }, + Box::new(()), + )) } } diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs index a32ffa3693c476090f52117efa9751b26f648765..bbd08ae984eb972fdb16134795fb458f5348a92f 100644 --- a/crates/storybook/src/stories/components/facepile.rs +++ b/crates/storybook/src/stories/components/facepile.rs @@ -11,7 +11,7 @@ impl FacepileStory { let players = static_players(); Story::container(cx) - .child(Story::title_for::<_, ui::Facepile>(cx)) + .child(Story::title_for::<_, Facepile>(cx)) .child(Story::label(cx, "Default")) .child( div() diff --git a/crates/storybook/src/stories/components/panel.rs b/crates/storybook/src/stories/components/panel.rs index 38e7033d44878a84c1b195ddf1f2788a05b25a28..39a5ceafa26107822b4d36005818bf620b39efbb 100644 --- a/crates/storybook/src/stories/components/panel.rs +++ b/crates/storybook/src/stories/components/panel.rs @@ -14,9 +14,10 @@ impl PanelStory { .child(Panel::new( ScrollState::default(), |_, _| { - (0..100) - .map(|ix| Label::new(format!("Item {}", ix + 1)).into_any()) - .collect() + vec![div() + .overflow_y_scroll(ScrollState::default()) + .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))) + .into_any()] }, Box::new(()), )) diff --git a/crates/storybook/src/stories/components/project_panel.rs b/crates/storybook/src/stories/components/project_panel.rs index ff4eb6099b46460d262afce44207e71ad7d67b61..cba71cd21a58695075738679a5626cff5db2f59e 100644 --- a/crates/storybook/src/stories/components/project_panel.rs +++ b/crates/storybook/src/stories/components/project_panel.rs @@ -1,5 +1,5 @@ use ui::prelude::*; -use ui::ProjectPanel; +use ui::{Panel, ProjectPanel}; use crate::story::Story; @@ -11,6 +11,10 @@ impl ProjectPanelStory { Story::container(cx) .child(Story::title_for::<_, ProjectPanel>(cx)) .child(Story::label(cx, "Default")) - .child(ProjectPanel::new(ScrollState::default())) + .child(Panel::new( + ScrollState::default(), + |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()], + Box::new(()), + )) } } diff --git a/crates/storybook/src/stories/components/tab_bar.rs b/crates/storybook/src/stories/components/tab_bar.rs index 4c116caf7b0b6a3f51491d8de3329e7622160795..b5fa45dfd6009b941cddba9a2cb139cb55cd810b 100644 --- a/crates/storybook/src/stories/components/tab_bar.rs +++ b/crates/storybook/src/stories/components/tab_bar.rs @@ -1,5 +1,5 @@ use ui::prelude::*; -use ui::TabBar; +use ui::{Tab, TabBar}; use crate::story::Story; @@ -11,6 +11,36 @@ impl TabBarStory { Story::container(cx) .child(Story::title_for::<_, TabBar>(cx)) .child(Story::label(cx, "Default")) - .child(TabBar::new(ScrollState::default())) + .child(TabBar::new(vec![ + Tab::new() + .title("Cargo.toml".to_string()) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("Channels Panel".to_string()) + .current(false), + Tab::new() + .title("channels_panel.rs".to_string()) + .current(true) + .git_status(GitStatus::Modified), + Tab::new() + .title("workspace.rs".to_string()) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("icon_button.rs".to_string()) + .current(false), + Tab::new() + .title("storybook.rs".to_string()) + .current(false) + .git_status(GitStatus::Created), + Tab::new().title("theme.rs".to_string()).current(false), + Tab::new() + .title("theme_registry.rs".to_string()) + .current(false), + Tab::new() + .title("styleable_helpers.rs".to_string()) + .current(false), + ])) } } diff --git a/crates/storybook/src/stories/components/toolbar.rs b/crates/storybook/src/stories/components/toolbar.rs index cfe3c978402635d616f261883641522d3ded0e06..1413c463b4366a2326b195fd9e3472b7acc29d37 100644 --- a/crates/storybook/src/stories/components/toolbar.rs +++ b/crates/storybook/src/stories/components/toolbar.rs @@ -1,5 +1,9 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + use ui::prelude::*; -use ui::Toolbar; +use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar}; use crate::story::Story; @@ -8,9 +12,59 @@ pub struct ToolbarStory {} impl ToolbarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + struct LeftItemsPayload { + pub theme: Arc, + } + Story::container(cx) - .child(Story::title_for::<_, Toolbar>(cx)) + .child(Story::title_for::<_, Toolbar>(cx)) .child(Story::label(cx, "Default")) - .child(Toolbar::new()) + .child(Toolbar::new( + |_, payload| { + let payload = payload.downcast_ref::().unwrap(); + + let theme = payload.theme.clone(); + + vec![Breadcrumb::new( + PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), + vec![ + Symbol(vec![ + HighlightedText { + text: "impl ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "ToolbarStory".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "render".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + ], + ) + .into_any()] + }, + Box::new(LeftItemsPayload { + theme: theme.clone(), + }), + |_, _| { + vec![ + IconButton::new(Icon::InlayHint).into_any(), + IconButton::new(Icon::MagnifyingGlass).into_any(), + IconButton::new(Icon::MagicWand).into_any(), + ] + }, + Box::new(()), + )) } } diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs index d47c667f614fc2338b1c69a5f4e51883b013253d..a277fa6a1e8d5d43f1ee904afa0fe7585a276139 100644 --- a/crates/storybook/src/stories/elements/avatar.rs +++ b/crates/storybook/src/stories/elements/avatar.rs @@ -9,7 +9,7 @@ pub struct AvatarStory {} impl AvatarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { Story::container(cx) - .child(Story::title_for::<_, ui::Avatar>(cx)) + .child(Story::title_for::<_, Avatar>(cx)) .child(Story::label(cx, "Default")) .child(Avatar::new( "https://avatars.githubusercontent.com/u/1714999?v=4", diff --git a/crates/storybook/src/stories/elements/icon.rs b/crates/storybook/src/stories/elements/icon.rs index 21838bd839a58d2752924702cc1ac2623f552b6e..66d3abc0b34bf53d77959ad1371f2ffbc4b3e5b0 100644 --- a/crates/storybook/src/stories/elements/icon.rs +++ b/crates/storybook/src/stories/elements/icon.rs @@ -12,7 +12,7 @@ impl IconStory { let icons = Icon::iter(); Story::container(cx) - .child(Story::title_for::<_, ui::IconElement>(cx)) + .child(Story::title_for::<_, IconElement>(cx)) .child(Story::label(cx, "All Icons")) .child(div().flex().gap_3().children(icons.map(IconElement::new))) } diff --git a/crates/storybook/src/stories/kitchen_sink.rs b/crates/storybook/src/stories/kitchen_sink.rs index 3bb902b0db37c8522552464e35709af0c6c0470c..ae826f934e8fac4b211086c848a94acff0f35128 100644 --- a/crates/storybook/src/stories/kitchen_sink.rs +++ b/crates/storybook/src/stories/kitchen_sink.rs @@ -19,5 +19,8 @@ impl KitchenSinkStory { .child(div().flex().flex_col().children_any(element_stories)) .child(Story::label(cx, "Components")) .child(div().flex().flex_col().children_any(component_stories)) + // Add a bit of space at the bottom of the kitchen sink so elements + // don't end up squished right up against the bottom of the screen. + .child(div().p_4()) } } diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index f4a2f697044438bed364642f24b06b10b78fab5a..afae0d5ebe0a82f6f8b2dd8f8934c032ad61ddd6 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -4,7 +4,7 @@ mod stories; mod story; mod story_selector; -use std::sync::Arc; +use std::{process::Command, sync::Arc}; use ::theme as legacy_theme; use clap::Parser; @@ -38,11 +38,44 @@ struct Args { theme: Option, } +async fn watch_zed_changes(fs: Arc) -> Option<()> { + if std::env::var("ZED_HOT_RELOAD").is_err() { + return None; + } + use futures::StreamExt; + let mut events = fs + .watch(".".as_ref(), std::time::Duration::from_millis(100)) + .await; + let mut current_child: Option = None; + while let Some(events) = events.next().await { + if !events.iter().any(|event| { + event + .path + .to_str() + .map(|path| path.contains("/crates/")) + .unwrap_or_default() + }) { + continue; + } + let child = current_child.take().map(|mut child| child.kill()); + log::info!("Storybook changed, rebuilding..."); + current_child = Some( + Command::new("cargo") + .args(["run", "-p", "storybook"]) + .spawn() + .ok()?, + ); + } + Some(()) +} + fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); let args = Args::parse(); + let fs = Arc::new(fs::RealFs); + gpui2::App::new(Assets).unwrap().run(move |cx| { let mut store = SettingsStore::default(); store @@ -63,6 +96,10 @@ fn main() { }) .and_then(|theme_name| theme_registry.get(&theme_name).ok()); + cx.spawn(|_| async move { + watch_zed_changes(fs).await; + }) + .detach(); cx.add_window( gpui2::WindowOptions { bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 821e93a34006e59d849176c8e2a493c86833499b..7bd9d912a0fb05ae0b921e3e5c7b7f74debb984e 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -13,3 +13,4 @@ settings = { path = "../settings" } smallvec.workspace = true strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } +rand = "0.8" diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index f96964bd27573e3c54d1dfed48dc5f9aff4f0af7..0af13040f75bd748b2bbe0ce99c8bb61ab7d1246 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -5,7 +5,7 @@ mod chat_panel; mod collab_panel; mod command_palette; mod context_menu; -mod editor; +mod editor_pane; mod facepile; mod icon_button; mod keybinding; @@ -31,7 +31,7 @@ pub use chat_panel::*; pub use collab_panel::*; pub use command_palette::*; pub use context_menu::*; -pub use editor::*; +pub use editor_pane::*; pub use facepile::*; pub use icon_button::*; pub use keybinding::*; diff --git a/crates/ui/src/components/breadcrumb.rs b/crates/ui/src/components/breadcrumb.rs index 30b40011a5dfc99a444845d28d14930f2e042701..c14e89ee7b6793196d55d15b391a43d51e307873 100644 --- a/crates/ui/src/components/breadcrumb.rs +++ b/crates/ui/src/components/breadcrumb.rs @@ -1,17 +1,35 @@ -use crate::prelude::*; +use std::path::PathBuf; + +use gpui2::elements::div::Div; + use crate::{h_stack, theme}; +use crate::{prelude::*, HighlightedText}; + +#[derive(Clone)] +pub struct Symbol(pub Vec); #[derive(Element)] -pub struct Breadcrumb {} +pub struct Breadcrumb { + path: PathBuf, + symbols: Vec, +} impl Breadcrumb { - pub fn new() -> Self { - Self {} + pub fn new(path: PathBuf, symbols: Vec) -> Self { + Self { path, symbols } + } + + fn render_separator(&self, theme: &Theme) -> Div { + div() + .child(" › ") + .text_color(HighlightColor::Default.hsla(theme)) } fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let symbols_len = self.symbols.len(); + h_stack() .px_1() // TODO: Read font from theme (or settings?). @@ -21,11 +39,33 @@ impl Breadcrumb { .rounded_md() .hover() .fill(theme.highest.base.hovered.background) - // TODO: Replace hardcoded breadcrumbs. - .child("crates/ui/src/components/toolbar.rs") - .child(" › ") - .child("impl Breadcrumb") - .child(" › ") - .child("fn render") + .child(self.path.clone().to_str().unwrap().to_string()) + .child(if !self.symbols.is_empty() { + self.render_separator(&theme) + } else { + div() + }) + .child( + div().flex().children( + self.symbols + .iter() + .enumerate() + // TODO: Could use something like `intersperse` here instead. + .flat_map(|(ix, symbol)| { + let mut items = + vec![div().flex().children(symbol.0.iter().map(|segment| { + div().child(segment.text.clone()).text_color(segment.color) + }))]; + + let is_last_segment = ix == symbols_len - 1; + if !is_last_segment { + items.push(self.render_separator(&theme)); + } + + items + }) + .collect::>(), + ), + ) } } diff --git a/crates/ui/src/components/buffer.rs b/crates/ui/src/components/buffer.rs index 88c5a59563f8c4300523b924c2f2f35d9a518961..00e5daee55c5d5c542daf7f1ed3d8418b0662a13 100644 --- a/crates/ui/src/components/buffer.rs +++ b/crates/ui/src/components/buffer.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use gpui2::{Hsla, WindowContext}; use crate::prelude::*; @@ -33,6 +31,7 @@ pub struct BufferRow { pub show_line_number: bool, } +#[derive(Clone)] pub struct BufferRows { pub show_line_numbers: bool, pub rows: Vec, @@ -108,9 +107,8 @@ impl BufferRow { } } -#[derive(Element)] -pub struct Buffer { - view_type: PhantomData, +#[derive(Element, Clone)] +pub struct Buffer { scroll_state: ScrollState, rows: Option, readonly: bool, @@ -119,10 +117,9 @@ pub struct Buffer { path: Option, } -impl Buffer { +impl Buffer { pub fn new() -> Self { Self { - view_type: PhantomData, scroll_state: ScrollState::default(), rows: Some(BufferRows::default()), readonly: false, @@ -161,7 +158,7 @@ impl Buffer { self } - fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement { + fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement { let theme = theme(cx); let system_color = SystemColor::new(); @@ -172,28 +169,35 @@ impl Buffer { }; let line_number_color = if row.current { - HighlightColor::Default.hsla(cx) + HighlightColor::Default.hsla(&theme) } else { - HighlightColor::Comment.hsla(cx) + HighlightColor::Comment.hsla(&theme) }; h_stack() .fill(line_background) + .w_full() .gap_2() - .px_2() - .child(h_stack().w_4().h_full().px_1().when(row.code_action, |c| { - div().child(IconElement::new(Icon::Bolt)) - })) + .px_1() + .child( + h_stack() + .w_4() + .h_full() + .px_0p5() + .when(row.code_action, |c| { + div().child(IconElement::new(Icon::Bolt)) + }), + ) .when(row.show_line_number, |this| { this.child( - h_stack().justify_end().px_1().w_4().child( + h_stack().justify_end().px_0p5().w_3().child( div() .text_color(line_number_color) .child(row.line_number.to_string()), ), ) }) - .child(div().mx_1().w_1().h_full().fill(row.status.hsla(cx))) + .child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx))) .children(row.line.map(|line| { div() .flex() @@ -205,7 +209,7 @@ impl Buffer { })) } - fn render_rows(&self, cx: &WindowContext) -> Vec> { + fn render_rows(&self, cx: &WindowContext) -> Vec> { match &self.rows { Some(rows) => rows .rows @@ -216,7 +220,7 @@ impl Buffer { } } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let rows = self.render_rows(cx); v_stack() diff --git a/crates/ui/src/components/chat_panel.rs b/crates/ui/src/components/chat_panel.rs index e5a2d6a556a0ceb6da4d372ff0d2911c8fe949f4..5ae66967b62518b62bd668da58308ab5f6c6d155 100644 --- a/crates/ui/src/components/chat_panel.rs +++ b/crates/ui/src/components/chat_panel.rs @@ -4,13 +4,12 @@ use chrono::NaiveDateTime; use crate::prelude::*; use crate::theme::theme; -use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide}; +use crate::{Icon, IconButton, Input, Label, LabelColor}; #[derive(Element)] pub struct ChatPanel { view_type: PhantomData, scroll_state: ScrollState, - current_side: PanelSide, messages: Vec, } @@ -19,16 +18,10 @@ impl ChatPanel { Self { view_type: PhantomData, scroll_state, - current_side: PanelSide::default(), messages: Vec::new(), } } - pub fn side(mut self, side: PanelSide) -> Self { - self.current_side = side; - self - } - pub fn with_messages(mut self, messages: Vec) -> Self { self.messages = messages; self @@ -37,38 +30,33 @@ impl ChatPanel { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - struct PanelPayload { - pub scroll_state: ScrollState, - pub messages: Vec, - } - - Panel::new( - self.scroll_state.clone(), - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - vec![div() + div() + .flex() + .flex_col() + .justify_between() + .h_full() + .px_2() + .gap_2() + // Header + .child( + div() .flex() - .flex_col() - .h_full() - .px_2() - .gap_2() - // Header + .justify_between() + .py_2() + .child(div().flex().child(Label::new("#design"))) .child( div() .flex() - .justify_between() - .gap_2() - .child(div().flex().child(Label::new("#design"))) - .child( - div() - .flex() - .items_center() - .gap_px() - .child(IconButton::new(Icon::File)) - .child(IconButton::new(Icon::AudioOn)), - ), - ) + .items_center() + .gap_px() + .child(IconButton::new(Icon::File)) + .child(IconButton::new(Icon::AudioOn)), + ), + ) + .child( + div() + .flex() + .flex_col() // Chat Body .child( div() @@ -76,19 +64,12 @@ impl ChatPanel { .flex() .flex_col() .gap_3() - .overflow_y_scroll(payload.scroll_state.clone()) - .children(payload.messages.clone()), + .overflow_y_scroll(self.scroll_state.clone()) + .children(self.messages.clone()), ) // Composer - .child(div().flex().gap_2().child(Input::new("Message #design"))) - .into_any()] - }, - Box::new(PanelPayload { - scroll_state: self.scroll_state.clone(), - messages: self.messages.clone(), - }), - ) - .side(self.current_side) + .child(div().flex().my_2().child(Input::new("Message #design"))), + ) } } diff --git a/crates/ui/src/components/editor.rs b/crates/ui/src/components/editor.rs deleted file mode 100644 index 105ed86c4019a5e8d94baf0b9f68ca72147f032f..0000000000000000000000000000000000000000 --- a/crates/ui/src/components/editor.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::{Buffer, Toolbar}; - -#[derive(Element)] -struct Editor { - view_type: PhantomData, - toolbar: Toolbar, - buffer: Buffer, -} - -impl Editor { - pub fn new(toolbar: Toolbar, buffer: Buffer) -> Self { - Self { - view_type: PhantomData, - toolbar, - buffer, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div().child(self.toolbar.clone()) - } -} diff --git a/crates/ui/src/components/editor_pane.rs b/crates/ui/src/components/editor_pane.rs new file mode 100644 index 0000000000000000000000000000000000000000..561081164c8f9c8982ef2915ba5a612428f64ed4 --- /dev/null +++ b/crates/ui/src/components/editor_pane.rs @@ -0,0 +1,60 @@ +use std::marker::PhantomData; +use std::path::PathBuf; + +use crate::prelude::*; +use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar}; + +pub struct Editor { + pub tabs: Vec, + pub path: PathBuf, + pub symbols: Vec, + pub buffer: Buffer, +} + +#[derive(Element)] +pub struct EditorPane { + view_type: PhantomData, + editor: Editor, +} + +impl EditorPane { + pub fn new(editor: Editor) -> Self { + Self { + view_type: PhantomData, + editor, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + struct LeftItemsPayload { + path: PathBuf, + symbols: Vec, + } + + v_stack() + .w_full() + .h_full() + .flex_1() + .child(TabBar::new(self.editor.tabs.clone())) + .child(Toolbar::new( + |_, payload| { + let payload = payload.downcast_ref::().unwrap(); + + vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()] + }, + Box::new(LeftItemsPayload { + path: self.editor.path.clone(), + symbols: self.editor.symbols.clone(), + }), + |_, _| { + vec![ + IconButton::new(Icon::InlayHint).into_any(), + IconButton::new(Icon::MagnifyingGlass).into_any(), + IconButton::new(Icon::MagicWand).into_any(), + ] + }, + Box::new(()), + )) + .child(self.editor.buffer.clone()) + } +} diff --git a/crates/ui/src/components/panel.rs b/crates/ui/src/components/panel.rs index 9d64945cc1b69d7cda4d3c2fcdb5693402901556..cbcf502670f4957c90558c84e97a45f44ca93b16 100644 --- a/crates/ui/src/components/panel.rs +++ b/crates/ui/src/components/panel.rs @@ -105,16 +105,12 @@ impl Panel { let theme = theme(cx); let panel_base; - let current_width = if let Some(width) = self.width { - width - } else { - self.initial_width - }; + let current_width = self.width.unwrap_or(self.initial_width); match self.current_side { PanelSide::Left => { panel_base = v_stack() - .overflow_y_scroll(self.scroll_state.clone()) + .flex_initial() .h_full() .w(current_width) .fill(theme.middle.base.default.background) @@ -123,20 +119,20 @@ impl Panel { } PanelSide::Right => { panel_base = v_stack() - .overflow_y_scroll(self.scroll_state.clone()) + .flex_initial() .h_full() .w(current_width) .fill(theme.middle.base.default.background) - .border_r() + .border_l() .border_color(theme.middle.base.default.border); } PanelSide::Bottom => { panel_base = v_stack() - .overflow_y_scroll(self.scroll_state.clone()) + .flex_initial() .w_full() .h(current_width) .fill(theme.middle.base.default.background) - .border_r() + .border_t() .border_color(theme.middle.base.default.border); } } diff --git a/crates/ui/src/components/player_stack.rs b/crates/ui/src/components/player_stack.rs index 4c00aaf2cfc32f67f9f9f1819fd6e5e327d14204..7df6f065fb280f4c821f1bb5b2488691d0eef874 100644 --- a/crates/ui/src/components/player_stack.rs +++ b/crates/ui/src/components/player_stack.rs @@ -38,9 +38,8 @@ impl PlayerStack { div().flex().justify_center().w_full().child( div() .w_4() - .h_1() - .rounded_bl_sm() - .rounded_br_sm() + .h_0p5() + .rounded_sm() .fill(player.cursor_color(cx)), ), ) @@ -50,7 +49,7 @@ impl PlayerStack { .items_center() .justify_center() .h_6() - .px_1() + .pl_1() .rounded_lg() .fill(if followers.is_none() { system_color.transparent @@ -59,7 +58,7 @@ impl PlayerStack { }) .child(Avatar::new(player.avatar_src().to_string())) .children(followers.map(|followers| { - div().neg_mr_1().child(Facepile::new(followers.into_iter())) + div().neg_ml_2().child(Facepile::new(followers.into_iter())) })), ) } diff --git a/crates/ui/src/components/project_panel.rs b/crates/ui/src/components/project_panel.rs index cf6f080b1ce5736ce210723a42183603869a2549..1f32c698e583d79b7fd327b24b9671f1ad0af0ac 100644 --- a/crates/ui/src/components/project_panel.rs +++ b/crates/ui/src/components/project_panel.rs @@ -1,17 +1,15 @@ use std::marker::PhantomData; -use std::sync::Arc; use crate::prelude::*; use crate::{ static_project_panel_project_items, static_project_panel_single_items, theme, Input, List, - ListHeader, Panel, PanelSide, Theme, + ListHeader, }; #[derive(Element)] pub struct ProjectPanel { view_type: PhantomData, scroll_state: ScrollState, - current_side: PanelSide, } impl ProjectPanel { @@ -19,69 +17,42 @@ impl ProjectPanel { Self { view_type: PhantomData, scroll_state, - current_side: PanelSide::default(), } } - pub fn side(mut self, side: PanelSide) -> Self { - self.current_side = side; - self - } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - struct PanelPayload { - pub theme: Arc, - pub scroll_state: ScrollState, - } - - Panel::new( - self.scroll_state.clone(), - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - let theme = payload.theme.clone(); - - vec![div() + let theme = theme(cx); + + div() + .flex() + .flex_col() + .w_full() + .h_full() + .px_2() + .fill(theme.middle.base.default.background) + .child( + div() + .w_56() .flex() .flex_col() - .w_56() - .h_full() - .px_2() - .fill(theme.middle.base.default.background) + .overflow_y_scroll(ScrollState::default()) .child( - div() - .w_56() - .flex() - .flex_col() - .overflow_y_scroll(payload.scroll_state.clone()) - .child( - List::new(static_project_panel_single_items()) - .header( - ListHeader::new("FILES").set_toggle(ToggleState::Toggled), - ) - .empty_message("No files in directory") - .set_toggle(ToggleState::Toggled), - ) - .child( - List::new(static_project_panel_project_items()) - .header( - ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled), - ) - .empty_message("No folders in directory") - .set_toggle(ToggleState::Toggled), - ), + List::new(static_project_panel_single_items()) + .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled)) + .empty_message("No files in directory") + .set_toggle(ToggleState::Toggled), ) .child( - Input::new("Find something...") - .value("buffe".to_string()) - .state(InteractionState::Focused), - ) - .into_any()] - }, - Box::new(PanelPayload { - theme: theme(cx), - scroll_state: self.scroll_state.clone(), - }), - ) + List::new(static_project_panel_project_items()) + .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled)) + .empty_message("No folders in directory") + .set_toggle(ToggleState::Toggled), + ), + ) + .child( + Input::new("Find something...") + .value("buffe".to_string()) + .state(InteractionState::Focused), + ) } } diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 9c034d25356216dde94a305e84b0b668cf9f72aa..9eb1122775474382b61151ef6c370438abe38ec6 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor}; -#[derive(Element)] +#[derive(Element, Clone)] pub struct Tab { title: String, icon: Option, diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index 43fef77e2ca942ebc9f97ccf04251a9f89b13d72..8addcb87b1599545355fc01d5b3c972515d251f0 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -7,20 +7,27 @@ use crate::{theme, Icon, IconButton, Tab}; pub struct TabBar { view_type: PhantomData, scroll_state: ScrollState, + tabs: Vec, } impl TabBar { - pub fn new(scroll_state: ScrollState) -> Self { + pub fn new(tabs: Vec) -> Self { Self { view_type: PhantomData, - scroll_state, + scroll_state: ScrollState::default(), + tabs, } } + pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) { + self.scroll_state = scroll_state; + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let can_navigate_back = true; let can_navigate_forward = false; + div() .w_full() .flex() @@ -54,51 +61,7 @@ impl TabBar { div() .flex() .overflow_x_scroll(self.scroll_state.clone()) - .child( - Tab::new() - .title("Cargo.toml".to_string()) - .current(false) - .git_status(GitStatus::Modified), - ) - .child( - Tab::new() - .title("Channels Panel".to_string()) - .current(false), - ) - .child( - Tab::new() - .title("channels_panel.rs".to_string()) - .current(true) - .git_status(GitStatus::Modified), - ) - .child( - Tab::new() - .title("workspace.rs".to_string()) - .current(false) - .git_status(GitStatus::Modified), - ) - .child( - Tab::new() - .title("icon_button.rs".to_string()) - .current(false), - ) - .child( - Tab::new() - .title("storybook.rs".to_string()) - .current(false) - .git_status(GitStatus::Created), - ) - .child(Tab::new().title("theme.rs".to_string()).current(false)) - .child( - Tab::new() - .title("theme_registry.rs".to_string()) - .current(false), - ) - .child( - Tab::new() - .title("styleable_helpers.rs".to_string()) - .current(false), - ), + .children(self.tabs.clone()), ), ) // Right Side diff --git a/crates/ui/src/components/terminal.rs b/crates/ui/src/components/terminal.rs index f5c3a42a647d0c997df9c52fc338e972dadc85f0..909cb886ce01d8a8dff3212e0d5d44d09a433b95 100644 --- a/crates/ui/src/components/terminal.rs +++ b/crates/ui/src/components/terminal.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use gpui2::geometry::{relative, rems, Size}; use crate::prelude::*; @@ -20,6 +22,7 @@ impl Terminal { div() .flex() .flex_col() + .w_full() .child( // Terminal Tabs. div() @@ -70,8 +73,12 @@ impl Terminal { width: relative(1.).into(), height: rems(36.).into(), }, - |_, _| vec![], - Box::new(()), + |_, payload| { + let theme = payload.downcast_ref::>().unwrap(); + + vec![crate::static_data::terminal_buffer(&theme).into_any()] + }, + Box::new(theme), )) } } diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs index 196b896396b16732404ba4faed69b9a3b9b6be67..dd3d6c1fba652a139235e79eb0823002861901f6 100644 --- a/crates/ui/src/components/title_bar.rs +++ b/crates/ui/src/components/title_bar.rs @@ -2,16 +2,24 @@ use std::marker::PhantomData; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use crate::prelude::*; +use crate::{prelude::*, PlayerWithCallStatus}; use crate::{ - static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor, - PlayerStack, ToolDivider, TrafficLights, + theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights, }; +#[derive(Clone)] +pub struct Livestream { + pub players: Vec, + pub channel: Option, // projects + // windows +} + #[derive(Element)] pub struct TitleBar { view_type: PhantomData, + /// If the window is active from the OS's perspective. is_active: Arc, + livestream: Option, } impl TitleBar { @@ -28,14 +36,24 @@ impl TitleBar { Self { view_type: PhantomData, is_active, + livestream: None, } } + pub fn set_livestream(mut self, livestream: Option) -> Self { + self.livestream = livestream; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let has_focus = cx.window_is_active(); - let player_list = static_players_with_call_status().into_iter(); + let player_list = if let Some(livestream) = &self.livestream { + livestream.players.clone().into_iter() + } else { + vec![].into_iter() + }; div() .flex() @@ -61,7 +79,8 @@ impl TitleBar { .child(Button::new("zed")) .child(Button::new("nate/gpui2-ui-components")), ) - .children(player_list.map(|p| PlayerStack::new(p))), + .children(player_list.map(|p| PlayerStack::new(p))) + .child(IconButton::new(Icon::Plus)), ) .child( div() diff --git a/crates/ui/src/components/toolbar.rs b/crates/ui/src/components/toolbar.rs index aedd6347433bbab8ef5c6272e3e47cc8282a0b43..e0953bf3b2d21f7e95e0e22a027a6e8a78f0dd3f 100644 --- a/crates/ui/src/components/toolbar.rs +++ b/crates/ui/src/components/toolbar.rs @@ -1,33 +1,49 @@ use crate::prelude::*; -use crate::{theme, Breadcrumb, Icon, IconButton}; +use crate::theme; #[derive(Clone)] pub struct ToolbarItem {} -#[derive(Element, Clone)] -pub struct Toolbar { - items: Vec, +#[derive(Element)] +pub struct Toolbar { + left_items: HackyChildren, + left_items_payload: HackyChildrenPayload, + right_items: HackyChildren, + right_items_payload: HackyChildrenPayload, } -impl Toolbar { - pub fn new() -> Self { - Self { items: Vec::new() } +impl Toolbar { + pub fn new( + left_items: HackyChildren, + left_items_payload: HackyChildrenPayload, + right_items: HackyChildren, + right_items_payload: HackyChildrenPayload, + ) -> Self { + Self { + left_items, + left_items_payload, + right_items, + right_items_payload, + } } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); div() + .fill(theme.highest.base.default.background) .p_2() .flex() .justify_between() - .child(Breadcrumb::new()) .child( div() .flex() - .child(IconButton::new(Icon::InlayHint)) - .child(IconButton::new(Icon::MagnifyingGlass)) - .child(IconButton::new(Icon::MagicWand)), + .children_any((self.left_items)(cx, self.left_items_payload.as_ref())), + ) + .child( + div() + .flex() + .children_any((self.right_items)(cx, self.right_items_payload.as_ref())), ) } } diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs index 0c6331dc9bcfd5fe57295473e9a3c27270e24a3e..b609546f7fecc39c06708172e488c714bbcfeafa 100644 --- a/crates/ui/src/components/workspace.rs +++ b/crates/ui/src/components/workspace.rs @@ -1,10 +1,15 @@ +use std::sync::Arc; + use chrono::DateTime; use gpui2::geometry::{relative, rems, Size}; -use crate::prelude::*; use crate::{ - theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, - ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, + hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status, + Livestream, +}; +use crate::{ + theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides, + PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, }; #[derive(Element, Default)] @@ -17,6 +22,8 @@ pub struct WorkspaceElement { impl WorkspaceElement { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx).clone(); + let temp_size = rems(36.).into(); let root_group = PaneGroup::new_groups( @@ -29,8 +36,15 @@ impl WorkspaceElement { width: relative(1.).into(), height: temp_size, }, - |_, _| vec![Terminal::new().into_any()], - Box::new(()), + |_, payload| { + let theme = payload.downcast_ref::>().unwrap(); + + vec![EditorPane::new(hello_world_rust_editor_with_status_example( + &theme, + )) + .into_any()] + }, + Box::new(theme.clone()), ), Pane::new( ScrollState::default(), @@ -51,8 +65,15 @@ impl WorkspaceElement { width: relative(1.).into(), height: relative(1.).into(), }, - |_, _| vec![Terminal::new().into_any()], - Box::new(()), + |_, payload| { + let theme = payload.downcast_ref::>().unwrap(); + + vec![EditorPane::new(hello_world_rust_editor_with_status_example( + &theme, + )) + .into_any()] + }, + Box::new(theme.clone()), )], SplitDirection::Vertical, ), @@ -60,8 +81,6 @@ impl WorkspaceElement { SplitDirection::Horizontal, ); - let theme = theme(cx).clone(); - div() .size_full() .flex() @@ -72,7 +91,10 @@ impl WorkspaceElement { .items_start() .text_color(theme.lowest.base.default.foreground) .fill(theme.lowest.base.default.background) - .child(TitleBar::new(cx)) + .child(TitleBar::new(cx).set_livestream(Some(Livestream { + players: random_players_with_call_status(7), + channel: Some("gpui2-ui".to_string()), + }))) .child( div() .flex_1() @@ -84,8 +106,12 @@ impl WorkspaceElement { .border_b() .border_color(theme.lowest.base.default.border) .child( - ProjectPanel::new(self.left_panel_scroll_state.clone()) - .side(PanelSide::Left), + Panel::new( + self.left_panel_scroll_state.clone(), + |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()], + Box::new(()), + ) + .side(PanelSide::Left), ) .child( v_stack() @@ -110,26 +136,37 @@ impl WorkspaceElement { .side(PanelSide::Bottom), ), ) - .child(ChatPanel::new(ScrollState::default()).with_messages(vec![ - ChatMessage::new( - "osiewicz".to_string(), - "is this thing on?".to_string(), - DateTime::parse_from_rfc3339( - "2023-09-27T15:40:52.707Z", - ) - .unwrap() - .naive_local(), - ), - ChatMessage::new( - "maxdeviant".to_string(), - "Reading you loud and clear!".to_string(), - DateTime::parse_from_rfc3339( - "2023-09-28T15:40:52.707Z", - ) - .unwrap() - .naive_local(), - ), - ])), + .child( + Panel::new( + self.right_panel_scroll_state.clone(), + |_, payload| { + vec![ChatPanel::new(ScrollState::default()) + .with_messages(vec![ + ChatMessage::new( + "osiewicz".to_string(), + "is this thing on?".to_string(), + DateTime::parse_from_rfc3339( + "2023-09-27T15:40:52.707Z", + ) + .unwrap() + .naive_local(), + ), + ChatMessage::new( + "maxdeviant".to_string(), + "Reading you loud and clear!".to_string(), + DateTime::parse_from_rfc3339( + "2023-09-28T15:40:52.707Z", + ) + .unwrap() + .naive_local(), + ), + ]) + .into_any()] + }, + Box::new(()), + ) + .side(PanelSide::Right), + ), ) .child(StatusBar::new()) } diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index ca357b4f023de7f832d7b911834e75a76f03e781..6d4053a4aec4b07191a91de3fbcc03c58c365d6b 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -84,6 +84,7 @@ pub enum Icon { Plus, Quote, Screen, + SelectAll, Split, SplitMessage, Terminal, @@ -131,6 +132,7 @@ impl Icon { Icon::Plus => "icons/plus.svg", Icon::Quote => "icons/quote.svg", Icon::Screen => "icons/desktop.svg", + Icon::SelectAll => "icons/select-all.svg", Icon::Split => "icons/split.svg", Icon::SplitMessage => "icons/split_message.svg", Icon::Terminal => "icons/terminal.svg", diff --git a/crates/ui/src/elements/input.rs b/crates/ui/src/elements/input.rs index 1a860028d28ddb8d58b19d0147bcf027027e42ab..fd860f30c2cd02afce02802ef6476386ffc007ca 100644 --- a/crates/ui/src/elements/input.rs +++ b/crates/ui/src/elements/input.rs @@ -81,6 +81,7 @@ impl Input { div() .h_7() + .w_full() .px_2() .border() .border_color(border_color_default) diff --git a/crates/ui/src/elements/player.rs b/crates/ui/src/elements/player.rs index e9e269a2cbdaf44fb197942658cd987040e9aa62..465542dc7f39a0ce397e978da70520c7aa137897 100644 --- a/crates/ui/src/elements/player.rs +++ b/crates/ui/src/elements/player.rs @@ -65,7 +65,7 @@ impl PlayerCallStatus { } } -#[derive(Clone)] +#[derive(PartialEq, Clone)] pub struct Player { index: usize, avatar_src: String, @@ -73,6 +73,7 @@ pub struct Player { status: PlayerStatus, } +#[derive(Clone)] pub struct PlayerWithCallStatus { player: Player, call_status: PlayerCallStatus, diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index c3cecbfc6109eb4cc194fe51e39de18af69f1b5b..b19b2becd177ef34f0e41cd0713ce73c0d4553cf 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -2,7 +2,7 @@ pub use gpui2::elements::div::{div, ScrollState}; pub use gpui2::style::{StyleHelpers, Styleable}; pub use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant}; +pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant, Theme}; use gpui2::{hsla, rgb, Hsla, WindowContext}; use strum::EnumIter; @@ -40,8 +40,7 @@ pub enum HighlightColor { } impl HighlightColor { - pub fn hsla(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); + pub fn hsla(&self, theme: &Theme) -> Hsla { let system_color = SystemColor::new(); match self { @@ -74,7 +73,7 @@ impl HighlightColor { } } -#[derive(Default, PartialEq, EnumIter)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum FileSystemStatus { #[default] None, @@ -92,7 +91,7 @@ impl FileSystemStatus { } } -#[derive(Default, PartialEq, EnumIter, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum GitStatus { #[default] None, @@ -130,7 +129,7 @@ impl GitStatus { } } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum DiagnosticStatus { #[default] None, @@ -139,14 +138,14 @@ pub enum DiagnosticStatus { Info, } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum IconSide { #[default] Left, Right, } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum OrderMethod { #[default] Ascending, @@ -154,14 +153,14 @@ pub enum OrderMethod { MostRecent, } -#[derive(Default, PartialEq, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum Shape { #[default] Circle, RoundedRectangle, } -#[derive(Default, PartialEq, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum DisclosureControlVisibility { #[default] OnHover, diff --git a/crates/ui/src/static_data.rs b/crates/ui/src/static_data.rs index fed2d40a7380c9832fecb54363bd03dbf3606c38..b8c4e18f14c31cfb018fe581353e43d2fca971f6 100644 --- a/crates/ui/src/static_data.rs +++ b/crates/ui/src/static_data.rs @@ -1,12 +1,109 @@ -use gpui2::WindowContext; +use std::path::PathBuf; +use std::str::FromStr; + +use rand::Rng; use crate::{ - Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText, - Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus, - ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, - ToggleState, + Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor, + HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry, + ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player, + PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, Theme, ToggleState, + VideoStatus, }; +pub fn static_tabs_example() -> Vec { + vec![ + Tab::new() + .title("wip.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .fs_status(FileSystemStatus::Deleted), + Tab::new() + .title("Cargo.toml".to_string()) + .icon(Icon::FileToml) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("Channels Panel".to_string()) + .icon(Icon::Hash) + .current(false), + Tab::new() + .title("channels_panel.rs".to_string()) + .icon(Icon::FileRust) + .current(true) + .git_status(GitStatus::Modified), + Tab::new() + .title("workspace.rs".to_string()) + .current(false) + .icon(Icon::FileRust) + .git_status(GitStatus::Modified), + Tab::new() + .title("icon_button.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("storybook.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .git_status(GitStatus::Created), + Tab::new() + .title("theme.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("theme_registry.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("styleable_helpers.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + ] +} + +pub fn static_tabs_1() -> Vec { + vec![ + Tab::new() + .title("project_panel.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .fs_status(FileSystemStatus::Deleted), + Tab::new() + .title("tab_bar.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("workspace.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("tab.rs".to_string()) + .icon(Icon::FileRust) + .current(true) + .git_status(GitStatus::Modified), + ] +} + +pub fn static_tabs_2() -> Vec { + vec![ + Tab::new() + .title("tab_bar.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .fs_status(FileSystemStatus::Deleted), + Tab::new() + .title("static_data.rs".to_string()) + .icon(Icon::FileRust) + .current(true) + .git_status(GitStatus::Modified), + ] +} + +pub fn static_tabs_3() -> Vec { + vec![Tab::new().git_status(GitStatus::Created).current(true)] +} + pub fn static_players() -> Vec { vec![ Player::new( @@ -37,6 +134,154 @@ pub fn static_players() -> Vec { ] } +#[derive(Debug)] +pub struct PlayerData { + pub url: String, + pub name: String, +} +pub fn static_player_data() -> Vec { + vec![ + PlayerData { + url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(), + name: "iamnbutler".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/326587?v=4".into(), + name: "maxbrunsfeld".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/482957?v=4".into(), + name: "as-cii".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/1789?v=4".into(), + name: "nathansobo".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(), + name: "ForLoveOfCats".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(), + name: "SomeoneToIgnore".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(), + name: "JosephTLyons".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(), + name: "osiewicz".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(), + name: "KCaverly".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(), + name: "maxdeviant".into(), + }, + ] +} +pub fn create_static_players(player_data: Vec) -> Vec { + let mut players = Vec::new(); + for data in player_data { + players.push(Player::new(players.len(), data.url, data.name)); + } + players +} +pub fn static_player_1(data: &Vec) -> Player { + Player::new(1, data[0].url.clone(), data[0].name.clone()) +} +pub fn static_player_2(data: &Vec) -> Player { + Player::new(2, data[1].url.clone(), data[1].name.clone()) +} +pub fn static_player_3(data: &Vec) -> Player { + Player::new(3, data[2].url.clone(), data[2].name.clone()) +} +pub fn static_player_4(data: &Vec) -> Player { + Player::new(4, data[3].url.clone(), data[3].name.clone()) +} +pub fn static_player_5(data: &Vec) -> Player { + Player::new(5, data[4].url.clone(), data[4].name.clone()) +} +pub fn static_player_6(data: &Vec) -> Player { + Player::new(6, data[5].url.clone(), data[5].name.clone()) +} +pub fn static_player_7(data: &Vec) -> Player { + Player::new(7, data[6].url.clone(), data[6].name.clone()) +} +pub fn static_player_8(data: &Vec) -> Player { + Player::new(8, data[7].url.clone(), data[7].name.clone()) +} +pub fn static_player_9(data: &Vec) -> Player { + Player::new(9, data[8].url.clone(), data[8].name.clone()) +} +pub fn static_player_10(data: &Vec) -> Player { + Player::new(10, data[9].url.clone(), data[9].name.clone()) +} +pub fn static_livestream() -> Livestream { + Livestream { + players: random_players_with_call_status(7), + channel: Some("gpui2-ui".to_string()), + } +} +pub fn populate_player_call_status( + player: Player, + followers: Option>, +) -> PlayerCallStatus { + let mut rng = rand::thread_rng(); + let in_current_project: bool = rng.gen(); + let disconnected: bool = rng.gen(); + let voice_activity: f32 = rng.gen(); + let mic_status = if rng.gen_bool(0.5) { + MicStatus::Muted + } else { + MicStatus::Unmuted + }; + let video_status = if rng.gen_bool(0.5) { + VideoStatus::On + } else { + VideoStatus::Off + }; + let screen_share_status = if rng.gen_bool(0.5) { + ScreenShareStatus::Shared + } else { + ScreenShareStatus::NotShared + }; + PlayerCallStatus { + mic_status, + voice_activity, + video_status, + screen_share_status, + in_current_project, + disconnected, + following: None, + followers, + } +} +pub fn random_players_with_call_status(number_of_players: usize) -> Vec { + let players = create_static_players(static_player_data()); + let mut player_status = vec![]; + for i in 0..number_of_players { + let followers = if i == 0 { + Some(vec![ + players[1].clone(), + players[3].clone(), + players[5].clone(), + players[6].clone(), + ]) + } else if i == 1 { + Some(vec![players[2].clone(), players[6].clone()]) + } else { + None + }; + let call_status = populate_player_call_status(players[i].clone(), followers); + player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status)); + } + player_status +} + pub fn static_players_with_call_status() -> Vec { let players = static_players(); let mut player_0_status = PlayerCallStatus::new(); @@ -123,7 +368,7 @@ pub fn static_project_panel_project_items() -> Vec { .left_icon(Icon::FolderOpen.into()) .indent_level(3) .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("derrive_element.rs")) + ListEntry::new(Label::new("derive_element.rs")) .left_icon(Icon::FileRust.into()) .indent_level(4), ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) @@ -337,33 +582,49 @@ pub fn example_editor_actions() -> Vec { ] } -pub fn empty_buffer_example() -> Buffer { +pub fn empty_editor_example() -> Editor { + Editor { + tabs: static_tabs_example(), + path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), + symbols: vec![], + buffer: empty_buffer_example(), + } +} + +pub fn empty_buffer_example() -> Buffer { Buffer::new().set_rows(Some(BufferRows::default())) } -pub fn hello_world_rust_buffer_example(cx: &WindowContext) -> Buffer { - Buffer::new() - .set_title("hello_world.rs".to_string()) - .set_path("src/hello_world.rs".to_string()) - .set_language("rust".to_string()) - .set_rows(Some(BufferRows { - show_line_numbers: true, - rows: hello_world_rust_buffer_rows(cx), - })) +pub fn hello_world_rust_editor_example(theme: &Theme) -> Editor { + Editor { + tabs: static_tabs_example(), + path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), + symbols: vec![Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "main".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ])], + buffer: hello_world_rust_buffer_example(theme), + } } -pub fn hello_world_rust_buffer_with_status_example(cx: &WindowContext) -> Buffer { +pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer { Buffer::new() .set_title("hello_world.rs".to_string()) .set_path("src/hello_world.rs".to_string()) .set_language("rust".to_string()) .set_rows(Some(BufferRows { show_line_numbers: true, - rows: hello_world_rust_with_status_buffer_rows(cx), + rows: hello_world_rust_buffer_rows(theme), })) } -pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { +pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { let show_line_number = true; vec![ @@ -375,15 +636,15 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { highlighted_texts: vec![ HighlightedText { text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(cx), + color: HighlightColor::Keyword.hsla(&theme), }, HighlightedText { text: "main".to_string(), - color: HighlightColor::Function.hsla(cx), + color: HighlightColor::Function.hsla(&theme), }, HighlightedText { text: "() {".to_string(), - color: HighlightColor::Default.hsla(cx), + color: HighlightColor::Default.hsla(&theme), }, ], }), @@ -399,7 +660,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { highlighted_texts: vec![HighlightedText { text: " // Statements here are executed when the compiled binary is called." .to_string(), - color: HighlightColor::Comment.hsla(cx), + color: HighlightColor::Comment.hsla(&theme), }], }), cursors: None, @@ -422,7 +683,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: " // Print text to the console.".to_string(), - color: HighlightColor::Comment.hsla(cx), + color: HighlightColor::Comment.hsla(&theme), }], }), cursors: None, @@ -433,10 +694,34 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { line_number: 5, code_action: false, current: false, + line: Some(HighlightedLine { + highlighted_texts: vec![ + HighlightedText { + text: " println!(".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "\"Hello, world!\"".to_string(), + color: HighlightColor::String.hsla(&theme), + }, + HighlightedText { + text: ");".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + ], + }), + cursors: None, + status: GitStatus::None, + show_line_number, + }, + BufferRow { + line_number: 6, + code_action: false, + current: false, line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "}".to_string(), - color: HighlightColor::Default.hsla(cx), + color: HighlightColor::Default.hsla(&theme), }], }), cursors: None, @@ -446,7 +731,36 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { ] } -pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec { +pub fn hello_world_rust_editor_with_status_example(theme: &Theme) -> Editor { + Editor { + tabs: static_tabs_example(), + path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), + symbols: vec![Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "main".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ])], + buffer: hello_world_rust_buffer_with_status_example(theme), + } +} + +pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer { + Buffer::new() + .set_title("hello_world.rs".to_string()) + .set_path("src/hello_world.rs".to_string()) + .set_language("rust".to_string()) + .set_rows(Some(BufferRows { + show_line_numbers: true, + rows: hello_world_rust_with_status_buffer_rows(theme), + })) +} + +pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec { let show_line_number = true; vec![ @@ -458,15 +772,15 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec Vec Vec Vec Vec Vec Vec Buffer { + Buffer::new() + .set_title("zed — fish".to_string()) + .set_rows(Some(BufferRows { + show_line_numbers: false, + rows: terminal_buffer_rows(theme), + })) +} + +pub fn terminal_buffer_rows(theme: &Theme) -> Vec { + let show_line_number = false; + + vec![ + BufferRow { + line_number: 1, + code_action: false, + current: false, + line: Some(HighlightedLine { + highlighted_texts: vec![ + HighlightedText { + text: "maxdeviant ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "in ".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "profaned-capital ".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + HighlightedText { + text: "in ".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "~/p/zed ".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + HighlightedText { + text: "on ".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "î‚  gpui2-ui ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + ], + }), + cursors: None, + status: GitStatus::None, + show_line_number, + }, + BufferRow { + line_number: 2, + code_action: false, + current: false, + line: Some(HighlightedLine { + highlighted_texts: vec![HighlightedText { + text: "λ ".to_string(), + color: HighlightColor::String.hsla(&theme), + }], + }), + cursors: None, + status: GitStatus::None, + show_line_number, + }, + ] +} From 27d784b23e81a8f763587ebb3cb6fa09f4327e5f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 16:27:12 -0600 Subject: [PATCH 17/27] Fix bug in following Prior to this change you could only follow across workspaces when you were heading to the first window. --- crates/collab/src/tests/following_tests.rs | 132 +++++++++++++++--- .../src/project_shared_notification.rs | 8 +- crates/workspace/src/workspace.rs | 15 +- 3 files changed, 118 insertions(+), 37 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 696923e505bc3eebea1d09e47d0e367c842c8287..657d71afd45e9342a29770c450bafd1ace943087 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -2,12 +2,10 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; use collab_ui::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; -use gpui::{ - executor::Deterministic, geometry::vector::vec2f, AppContext, TestAppContext, ViewHandle, -}; +use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; use serde_json::json; -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use workspace::{ dock::{test::TestPanel, DockPosition}, item::{test::TestItem, ItemHandle as _}, @@ -1104,11 +1102,10 @@ async fn test_following_across_workspaces( // a shares project 1 // b shares project 2 // - // - // b joins project 1 - // - // test: when a is in project 2 and b clicks follow (from unshared project), b should open project 2 and follow a - // test: when a is in project 1 and b clicks follow, b should open project 1 and follow a + // b follows a: causes project 2 to be joined, and b to follow a. + // b opens a different file in project 2, a follows b + // b opens a different file in project 1, a cannot follow b + // b shares the project, a joins the project and follows b deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -1153,16 +1150,10 @@ async fn test_following_across_workspaces( cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); - let project_a_id = active_call_a + active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - /* - let project_b_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) - .await - .unwrap(); - */ active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) @@ -1173,18 +1164,14 @@ async fn test_following_across_workspaces( .await .unwrap(); - let editor_a = workspace_a + workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) }) .await - .unwrap() - .downcast::() .unwrap(); deterministic.run_until_parked(); - assert_eq!(cx_b.windows().len(), 2); - assert_eq!(visible_push_notifications(cx_b).len(), 1); workspace_b.update(cx_b, |workspace, cx| { @@ -1205,14 +1192,115 @@ async fn test_following_across_workspaces( .root(cx_b); // assert that b is following a in project a in w.rs - workspace_b_project_a.update(cx_b, |workspace, _| { + workspace_b_project_a.update(cx_b, |workspace, cx| { assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); assert_eq!( client_a.peer_id(), workspace.leader_for_pane(workspace.active_pane()) ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs")); }); + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b_project_a.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + // assert that there are no share notifications open assert_eq!(visible_push_notifications(cx_b).len(), 0); + + // b moves to x.rs in a's project, and a follows + workspace_b_project_a + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + workspace_b_project_a.update(cx_b, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + }); + + workspace_a.update(cx_a, |workspace, cx| { + workspace + .follow(client_b.peer_id().unwrap(), cx) + .unwrap() + .detach() + }); + + deterministic.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_pane().read(cx).active_item().unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + }); + + // b moves to y.rs in b's project, a is still following but can't yet see + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) + }) + .await + .unwrap(); + + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!(visible_push_notifications(cx_a).len(), 1); + cx_a.update(|cx| { + workspace::join_remote_project( + project_b_id, + client_b.user_id().unwrap(), + client_a.app_state.clone(), + cx, + ) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert_eq!(visible_push_notifications(cx_a).len(), 0); + let workspace_a_project_b = cx_a + .windows() + .iter() + .max_by_key(|window| window.id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_a); + + workspace_a_project_b.update(cx_a, |workspace, cx| { + assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); + }); } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 5e362403f04f040855c76057175cd21e56a1f641..28ccee768b9f0c0592b184c92ef229ea96ffa677 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -41,6 +41,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } room::Event::RemoteProjectUnshared { project_id } + | room::Event::RemoteProjectJoined { project_id } | room::Event::RemoteProjectInvitationDiscarded { project_id } => { if let Some(windows) = notification_windows.remove(&project_id) { for window in windows { @@ -55,13 +56,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } } - room::Event::RemoteProjectJoined { project_id } => { - if let Some(windows) = notification_windows.remove(&project_id) { - for window in windows { - window.remove(cx); - } - } - } _ => {} }) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c90b1753201668f21add8a9a133b251b31f89be3..6e62a9bf168701c388321920c35ec6c36e35bc74 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4246,21 +4246,20 @@ pub fn join_remote_project( cx: &mut AppContext, ) -> Task> { cx.spawn(|mut cx| async move { - let existing_workspace = cx - .windows() - .into_iter() - .find_map(|window| { - window.downcast::().and_then(|window| { - window.read_root_with(&cx, |workspace, cx| { + let windows = cx.windows(); + let existing_workspace = windows.into_iter().find_map(|window| { + window.downcast::().and_then(|window| { + window + .read_root_with(&cx, |workspace, cx| { if workspace.project().read(cx).remote_id() == Some(project_id) { Some(cx.handle().downgrade()) } else { None } }) - }) + .unwrap_or(None) }) - .flatten(); + }); let workspace = if let Some(existing_workspace) = existing_workspace { existing_workspace From 528fa5c57b69a6987c734aa8ce0a1dfcd9617b1f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 16:51:02 -0600 Subject: [PATCH 18/27] Refactor to remove toggle_follow --- .../collab/src/tests/channel_buffer_tests.rs | 4 +-- crates/collab/src/tests/following_tests.rs | 30 +++++++++---------- crates/workspace/src/workspace.rs | 22 +++++++------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 05abda5af3c7e77530860dc4cc3bcde490cd7e9e..46005244c15b3f4e33a6fca09cb9ba1c2ae2e9fb 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -702,9 +702,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client B follows client A. workspace_b .update(cx_b, |workspace, cx| { - workspace - .toggle_follow(client_a.peer_id().unwrap(), cx) - .unwrap() + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() }) .await .unwrap(); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 657d71afd45e9342a29770c450bafd1ace943087..6d374b79208a5fb6f76ad25acce7ac7e1535032e 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -126,7 +126,7 @@ async fn test_basic_following( // When client B starts following client A, all visible view states are replicated to client B. workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() + workspace.follow(peer_id_a, cx).unwrap() }) .await .unwrap(); @@ -166,7 +166,7 @@ async fn test_basic_following( // Client C also follows client A. workspace_c .update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() + workspace.follow(peer_id_a, cx).unwrap() }) .await .unwrap(); @@ -201,7 +201,7 @@ async fn test_basic_following( // Client C unfollows client A. workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); + workspace.unfollow(&workspace.active_pane().clone(), cx); }); // All clients see that clients B is following client A. @@ -224,7 +224,7 @@ async fn test_basic_following( // Client C re-follows client A. workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); + workspace.follow(peer_id_a, cx); }); // All clients see that clients B and C are following client A. @@ -248,7 +248,7 @@ async fn test_basic_following( // Client D follows client C. workspace_d .update(cx_d, |workspace, cx| { - workspace.toggle_follow(peer_id_c, cx).unwrap() + workspace.follow(peer_id_c, cx).unwrap() }) .await .unwrap(); @@ -439,7 +439,7 @@ async fn test_basic_following( // Client A starts following client B. workspace_a .update(cx_a, |workspace, cx| { - workspace.toggle_follow(peer_id_b, cx).unwrap() + workspace.follow(peer_id_b, cx).unwrap() }) .await .unwrap(); @@ -644,7 +644,7 @@ async fn test_following_tab_order( //Follow client B as client A workspace_a .update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() + workspace.follow(client_b_id, cx).unwrap() }) .await .unwrap(); @@ -756,7 +756,7 @@ async fn test_peers_following_each_other( .update(cx_a, |workspace, cx| { assert_ne!(*workspace.active_pane(), pane_a1); let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -767,7 +767,7 @@ async fn test_peers_following_each_other( .update(cx_b, |workspace, cx| { assert_ne!(*workspace.active_pane(), pane_b1); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -914,7 +914,7 @@ async fn test_auto_unfollowing( }); workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -939,7 +939,7 @@ async fn test_auto_unfollowing( workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -957,7 +957,7 @@ async fn test_auto_unfollowing( workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -977,7 +977,7 @@ async fn test_auto_unfollowing( workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -1053,10 +1053,10 @@ async fn test_peers_simultaneously_following_each_other( }); let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() + workspace.follow(client_b_id, cx).unwrap() }); let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_follow(client_a_id, cx).unwrap() + workspace.follow(client_a_id, cx).unwrap() }); futures::try_join!(a_follow_b, b_follow_a).unwrap(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6e62a9bf168701c388321920c35ec6c36e35bc74..f7bb4092291f2b677d8c4b0e0fbc0bdd887ad535 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2520,19 +2520,13 @@ impl Workspace { cx.notify(); } - pub fn toggle_follow( + fn start_following( &mut self, leader_id: PeerId, cx: &mut ViewContext, ) -> Option>> { let pane = self.active_pane().clone(); - if let Some(prev_leader_id) = self.unfollow(&pane, cx) { - if leader_id == prev_leader_id { - return None; - } - } - self.last_leaders_by_pane .insert(pane.downgrade(), leader_id); self.follower_states_by_leader @@ -2603,9 +2597,15 @@ impl Workspace { None }; - next_leader_id - .or_else(|| collaborators.keys().copied().next()) - .and_then(|leader_id| self.toggle_follow(leader_id, cx)) + let pane = self.active_pane.clone(); + let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) + else { + return None; + }; + if Some(leader_id) == self.unfollow(&pane, cx) { + return None; + } + self.follow(leader_id, cx) } pub fn follow( @@ -2654,7 +2654,7 @@ impl Workspace { } // Otherwise, follow. - self.toggle_follow(leader_id, cx) + self.start_following(leader_id, cx) } pub fn unfollow( From 892350fa2d6894290edd68ab2520da1d9e21636f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 2 Oct 2023 19:35:31 -0400 Subject: [PATCH 19/27] Add memory and cpu events Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- Cargo.lock | 5 ++-- Cargo.toml | 1 + crates/client/Cargo.toml | 9 +++--- crates/client/src/telemetry.rs | 55 +++++++++++++++++++++++++++++++++- crates/feedback/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- 6 files changed, 65 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76de671620a2539ce9e1f37b085c9cbbb1b30ad2..3b714455ef36a8bdf8957ee41910ab7b042d7e50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,6 +1415,7 @@ dependencies = [ "settings", "smol", "sum_tree", + "sysinfo", "tempfile", "text", "thiserror", @@ -7606,9 +7607,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.27.8" +version = "0.29.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" +checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", diff --git a/Cargo.toml b/Cargo.toml index f09d44e8da0e3c7894509ee4db09c836f935708e..801435ee2a041d41e34e32949c706e4ef392e9a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } smallvec = { version = "1.6", features = ["union"] } smol = { version = "1.2" } +sysinfo = "0.29.10" tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e3038e5bcc49bd41b756062b676e00f4f355867a..9e371ec8bd8aefb647e904f292d3289be9992fb0 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -33,15 +33,16 @@ parking_lot.workspace = true postage.workspace = true rand.workspace = true schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true smol.workspace = true +sysinfo.workspace = true +tempfile = "3" thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid = { version = "1.1.2", features = ["v4"] } url = "2.2" -serde.workspace = true -serde_derive.workspace = true -tempfile = "3" +uuid = { version = "1.1.2", features = ["v4"] } [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 1596e6d850216f631bd8ecbfd0a8b6dda02e0138..8d51a3d1fe7f3a2d37351e596a248db5e4fa1cac 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -4,6 +4,7 @@ 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 tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; @@ -88,6 +89,16 @@ pub enum ClickhouseEvent { kind: AssistantKind, model: &'static str, }, + Cpu { + usage_as_percent: f32, + core_count: u32, + }, + Memory { + memory_in_bytes: u64, + virtual_memory_in_bytes: u64, + start_time_in_seconds: u64, + run_time_in_seconds: u64, + }, } #[cfg(debug_assertions)] @@ -136,7 +147,7 @@ impl Telemetry { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } - pub fn start(self: &Arc, installation_id: Option) { + pub fn start(self: &Arc, installation_id: Option, cx: &mut AppContext) { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); @@ -145,6 +156,48 @@ impl Telemetry { if has_clickhouse_events { self.flush_clickhouse_events(); } + + let this = self.clone(); + cx.spawn(|mut cx| async move { + let mut system = System::new_all(); + system.refresh_all(); + + loop { + // Waiting some amount of time before the first query is important to get a reasonable value + // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage + const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); + smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; + + let telemetry_settings = cx.update(|cx| *settings::get::(cx)); + + system.refresh_memory(); + system.refresh_processes(); + + let current_process = Pid::from_u32(std::process::id()); + let Some(process) = system.processes().get(¤t_process) else { + let process = current_process; + log::error!("Failed to find own process {process:?} in system process table"); + // TODO: Fire an error telemetry event + return; + }; + + let memory_event = ClickhouseEvent::Memory { + memory_in_bytes: process.memory(), + virtual_memory_in_bytes: process.virtual_memory(), + start_time_in_seconds: process.start_time(), + run_time_in_seconds: process.run_time(), + }; + + let cpu_event = ClickhouseEvent::Cpu { + usage_as_percent: process.cpu_usage(), + core_count: system.cpus().len() as u32, + }; + + this.report_clickhouse_event(memory_event, telemetry_settings); + this.report_clickhouse_event(cpu_event, telemetry_settings); + } + }) + .detach(); } pub fn set_authenticated_user_info( diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 07b6ad790c8dcd553b8edb193f7d37179e212634..651d32ba915f076572655e39fa3ae15a54e615e9 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -33,7 +33,7 @@ lazy_static.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true -sysinfo = "0.27.1" +sysinfo.workspace = true tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } urlencoding = "2.1.2" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7991cabde23aee4d5affb81975b42fb64005c358..d6f3be2b464f3453ca83f602b56d974989d53cf6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -177,7 +177,7 @@ fn main() { }) .detach(); - client.telemetry().start(installation_id); + client.telemetry().start(installation_id, cx); let app_state = Arc::new(AppState { languages, From d7867cd1e283080788117e61b5246478ccd8469d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 19:38:45 -0600 Subject: [PATCH 20/27] Add/fix mouse interactions in current call sidebar --- crates/collab_ui/src/collab_panel.rs | 265 +++++++++++++++++---------- 1 file changed, 171 insertions(+), 94 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16a9ec563b16199ec24c4b63e3933d19d330f19b..22ab57397460c0e2479bf50e526c23de39bb3783 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -47,7 +47,7 @@ use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::ItemHandle, - Workspace, + FollowNextCollaborator, Workspace, }; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -404,6 +404,7 @@ enum ListEntry { Header(Section), CallParticipant { user: Arc, + peer_id: Option, is_pending: bool, }, ParticipantProject { @@ -508,14 +509,19 @@ impl CollabPanel { let is_collapsed = this.collapsed_sections.contains(section); this.render_header(*section, &theme, is_selected, is_collapsed, cx) } - ListEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant( - user, - *is_pending, - is_selected, - &theme.collab_panel, - ) - } + ListEntry::CallParticipant { + user, + peer_id, + is_pending, + } => Self::render_call_participant( + user, + *peer_id, + this.user_store.clone(), + *is_pending, + is_selected, + &theme, + cx, + ), ListEntry::ParticipantProject { project_id, worktree_root_names, @@ -528,7 +534,7 @@ impl CollabPanel { Some(*project_id) == current_project_id, *is_last, is_selected, - &theme.collab_panel, + &theme, cx, ), ListEntry::ParticipantScreen { peer_id, is_last } => { @@ -793,6 +799,7 @@ impl CollabPanel { let user_id = user.id; self.entries.push(ListEntry::CallParticipant { user, + peer_id: None, is_pending: false, }); let mut projects = room.local_participant().projects.iter().peekable(); @@ -830,6 +837,7 @@ impl CollabPanel { let participant = &room.remote_participants()[&user_id]; self.entries.push(ListEntry::CallParticipant { user: participant.user.clone(), + peer_id: Some(participant.peer_id), is_pending: false, }); let mut projects = participant.projects.iter().peekable(); @@ -871,6 +879,7 @@ impl CollabPanel { self.entries .extend(matches.iter().map(|mat| ListEntry::CallParticipant { user: room.pending_participants()[mat.candidate_id].clone(), + peer_id: None, is_pending: true, })); } @@ -1174,46 +1183,97 @@ impl CollabPanel { fn render_call_participant( user: &User, + peer_id: Option, + user_store: ModelHandle, is_pending: bool, is_selected: bool, - theme: &theme::CollabPanel, + theme: &theme::Theme, + cx: &mut ViewContext, ) -> AnyElement { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true), - ) - .with_children(if is_pending { - Some( - Label::new("Calling", theme.calling_indicator.text.clone()) + enum CallParticipant {} + enum CallParticipantTooltip {} + + let collab_theme = &theme.collab_panel; + + let is_current_user = + user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + + let content = + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let style = if is_current_user { + *collab_theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()) + } else { + *collab_theme + .contact_row + .in_state(is_selected) + .style_for(mouse_state) + }; + + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::from_data(avatar) + .with_style(collab_theme.contact_avatar) + .aligned() + .left() + })) + .with_child( + Label::new( + user.github_login.clone(), + collab_theme.contact_username.text.clone(), + ) .contained() - .with_style(theme.calling_indicator.container) - .aligned(), - ) - } else { - None + .with_style(collab_theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .with_children(if is_pending { + Some( + Label::new("Calling", collab_theme.calling_indicator.text.clone()) + .contained() + .with_style(collab_theme.calling_indicator.container) + .aligned(), + ) + } else if is_current_user { + Some( + Label::new("You", collab_theme.calling_indicator.text.clone()) + .contained() + .with_style(collab_theme.calling_indicator.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(collab_theme.row_height) + .contained() + .with_style(style) + }); + + if is_current_user || is_pending || peer_id.is_none() { + return content.into_any(); + } + + let tooltip = format!("Follow {}", user.github_login); + + content + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace + .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx)) + .map(|task| task.detach_and_log_err(cx)); + } }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + user.id as usize, + tooltip, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, ) .into_any() } @@ -1225,74 +1285,91 @@ impl CollabPanel { is_current: bool, is_last: bool, is_selected: bool, - theme: &theme::CollabPanel, + theme: &theme::Theme, cx: &mut ViewContext, ) -> AnyElement { enum JoinProject {} + enum JoinProjectTooltip {} - let host_avatar_width = theme + let collab_theme = &theme.collab_panel; + let host_avatar_width = collab_theme .contact_avatar .width - .or(theme.contact_avatar.height) + .or(collab_theme.contact_avatar.height) .unwrap_or(0.); - let tree_branch = theme.tree_branch; + let tree_branch = collab_theme.tree_branch; let project_name = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") }; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { - let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - let row = theme - .project_row - .in_state(is_selected) - .style_for(mouse_state); + let content = + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { + let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + let row = if is_current { + collab_theme + .project_row + .in_state(true) + .style_for(&mut Default::default()) + } else { + collab_theme + .project_row + .in_state(is_selected) + .style_for(mouse_state) + }; - Flex::row() - .with_child(render_tree_branch( - tree_branch, - &row.name.text, - is_last, - vec2f(host_avatar_width, theme.row_height), - cx.font_cache(), - )) - .with_child( - Svg::new("icons/file_icons/folder.svg") - .with_color(theme.channel_hash.color) - .constrained() - .with_width(theme.channel_hash.width) - .aligned() - .left(), - ) - .with_child( - Label::new(project_name, row.name.text.clone()) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - }) - .with_cursor_style(if !is_current { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if !is_current { + Flex::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, collab_theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/file_icons/folder.svg") + .with_color(collab_theme.channel_hash.color) + .constrained() + .with_width(collab_theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new(project_name.clone(), row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false), + ) + .constrained() + .with_height(collab_theme.row_height) + .contained() + .with_style(row.container) + }); + + if is_current { + return content.into_any(); + } + + content + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { let app_state = workspace.read(cx).app_state().clone(); workspace::join_remote_project(project_id, host_user_id, app_state, cx) .detach_and_log_err(cx); } - } - }) - .into_any() + }) + .with_tooltip::( + project_id as usize, + format!("Open {}", project_name), + None, + theme.tooltip.clone(), + cx, + ) + .into_any() } fn render_participant_screen( From 18e7305b6d8fa1377b09e79fceb5bfae9f885d07 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 23:20:06 -0600 Subject: [PATCH 21/27] Change channel join behavior - Clicking on a channel name now joins the channel if you are not in it - (or opens the notes if you are already there). - When joining a channel, previously shared projects are opened automatically. - If there are no previously shared projects, the notes are opened. --- crates/call/src/call.rs | 6 ++-- crates/call/src/room.rs | 27 +++++++++++++++ crates/collab_ui/src/collab_panel.rs | 51 ++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index ca0d06beb6a44e53b2f7593a74349f30968bd945..d86ed1be37e38da86bb9715e187528447e5b1abc 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -291,10 +291,10 @@ impl ActiveCall { &mut self, channel_id: u64, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(())); + return Task::ready(Ok(room)); } else { room.update(cx, |room, cx| room.clear_state(cx)); } @@ -309,7 +309,7 @@ impl ActiveCall { this.update(&mut cx, |this, cx| { this.report_call_event("join channel", cx) }); - Ok(()) + Ok(room) }) } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 130a7a64f09ca8421d61faf37af561e7f1a57d6a..f24a8e9a9cf84743aa5a588f5be8e03dcc739aa0 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -594,6 +594,33 @@ impl Room { .map_or(&[], |v| v.as_slice()) } + /// projects_to_join returns a list of shared projects sorted such + /// that the most 'active' projects appear last. + pub fn projects_to_join(&self) -> Vec<(u64, u64)> { + let mut projects = HashMap::default(); + let mut hosts = HashMap::default(); + for participant in self.remote_participants.values() { + match participant.location { + ParticipantLocation::SharedProject { project_id } => { + *projects.entry(project_id).or_insert(0) += 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); + } + } + + let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); + pairs.sort_by_key(|(_, count)| 0 - *count as i32); + + pairs + .into_iter() + .map(|(project_id, _)| (project_id, hosts[&project_id])) + .collect() + } + async fn handle_room_updated( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 22ab57397460c0e2479bf50e526c23de39bb3783..eeae37bbe558029c5b5000d6824af922f3931688 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,7 +8,7 @@ use crate::{ panel_settings, CollaborationPanelSettings, }; use anyhow::Result; -use call::ActiveCall; +use call::{participant, ActiveCall}; use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use channel_modal::ChannelModal; use client::{proto::PeerId, Client, Contact, User, UserStore}; @@ -1929,7 +1929,7 @@ impl CollabPanel { })) .into_any() } else if row_hovered { - Svg::new("icons/speaker-loud.svg") + Svg::new("icons/file.svg") .with_color(theme.channel_hash.color) .constrained() .with_width(theme.channel_hash.width) @@ -1939,7 +1939,11 @@ impl CollabPanel { } }) .on_click(MouseButton::Left, move |_, this, cx| { - this.join_channel_call(channel_id, cx); + if !is_active { + this.join_channel_call(channel_id, cx); + } else { + this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) + } }), ) .align_children_center() @@ -1968,6 +1972,12 @@ impl CollabPanel { }) .on_click(MouseButton::Left, move |_, this, cx| { if this.drag_target_channel.take().is_none() { + if is_active { + this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) + } else { + this.join_channel_call(channel_id, cx) + } + this.join_channel_chat(channel_id, cx); } }) @@ -2991,10 +3001,37 @@ impl CollabPanel { .detach_and_log_err(cx); } - fn join_channel_call(&self, channel: u64, cx: &mut ViewContext) { - ActiveCall::global(cx) - .update(cx, |call, cx| call.join_channel(channel, cx)) - .detach_and_log_err(cx); + fn join_channel_call(&self, channel_id: u64, cx: &mut ViewContext) { + let join = ActiveCall::global(cx).update(cx, |call, cx| call.join_channel(channel_id, cx)); + let workspace = self.workspace.clone(); + + cx.spawn(|_, mut cx| async move { + let room = join.await?; + + let tasks = room.update(&mut cx, |room, cx| { + let Some(workspace) = workspace.upgrade(cx) else { + return vec![]; + }; + let projects = room.projects_to_join(); + + if projects.is_empty() { + ChannelView::open(channel_id, workspace, cx).detach(); + return vec![]; + } + room.projects_to_join() + .into_iter() + .map(|(project_id, user_id)| { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_remote_project(project_id, user_id, app_state, cx) + }) + .collect() + }); + for task in tasks { + task.await?; + } + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); } fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext) { From 9f160537ef4ea8fec1a82c45c7c70e62973b24f3 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 11:56:45 +0300 Subject: [PATCH 22/27] move collapsed only matches outside item parent in embedding.scm --- .../semantic_index/src/semantic_index_tests.rs | 17 +++++++++++++++++ crates/zed/src/languages/rust/embedding.scm | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index f2cae8a55701e910379d672225c19ac18a489897..182010ca8339e9cc8ec1ff06ac31741eb4fb78ae 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -305,6 +305,11 @@ async fn test_code_context_retrieval_rust() { todo!(); } } + + #[derive(Clone)] + struct D { + name: String + } " .unindent(); @@ -361,6 +366,15 @@ async fn test_code_context_retrieval_rust() { .unindent(), text.find("fn function_2").unwrap(), ), + ( + " + #[derive(Clone)] + struct D { + name: String + }" + .unindent(), + text.find("struct D").unwrap(), + ), ], ); } @@ -1422,6 +1436,9 @@ fn rust_lang() -> Arc { name: (_) @name) ] @item ) + + (attribute_item) @collapse + (use_declaration) @collapse "#, ) .unwrap(), diff --git a/crates/zed/src/languages/rust/embedding.scm b/crates/zed/src/languages/rust/embedding.scm index c4ed7d20976fb9c56f39aec1c8a32bba5f405f15..286b1d13571ad62964e3f38415fc4cbbb04e4e99 100644 --- a/crates/zed/src/languages/rust/embedding.scm +++ b/crates/zed/src/languages/rust/embedding.scm @@ -2,8 +2,6 @@ [(line_comment) (attribute_item)]* @context . [ - (attribute_item) @collapse - (use_declaration) @collapse (struct_item name: (_) @name) @@ -29,3 +27,6 @@ name: (_) @name) ] @item ) + +(attribute_item) @collapse +(use_declaration) @collapse From b10255a6ddfb119399873a63cbe31d08c2c85f82 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 3 Oct 2023 13:27:32 -0400 Subject: [PATCH 23/27] Update cpu and memory event code Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/client/src/telemetry.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 8d51a3d1fe7f3a2d37351e596a248db5e4fa1cac..38a4115ddd687a9a1ccc4e4d72675e3091d51ef4 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -90,14 +90,12 @@ pub enum ClickhouseEvent { model: &'static str, }, Cpu { - usage_as_percent: f32, + usage_as_percentage: f32, core_count: u32, }, Memory { memory_in_bytes: u64, virtual_memory_in_bytes: u64, - start_time_in_seconds: u64, - run_time_in_seconds: u64, }, } @@ -168,8 +166,6 @@ impl Telemetry { const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; - let telemetry_settings = cx.update(|cx| *settings::get::(cx)); - system.refresh_memory(); system.refresh_processes(); @@ -184,15 +180,15 @@ impl Telemetry { let memory_event = ClickhouseEvent::Memory { memory_in_bytes: process.memory(), virtual_memory_in_bytes: process.virtual_memory(), - start_time_in_seconds: process.start_time(), - run_time_in_seconds: process.run_time(), }; let cpu_event = ClickhouseEvent::Cpu { - usage_as_percent: process.cpu_usage(), + usage_as_percentage: process.cpu_usage(), core_count: system.cpus().len() as u32, }; + let telemetry_settings = cx.update(|cx| *settings::get::(cx)); + this.report_clickhouse_event(memory_event, telemetry_settings); this.report_clickhouse_event(cpu_event, telemetry_settings); } From 66dfa47c668555688515c926b0cb1c0fd35aa3a8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 11:36:01 -0600 Subject: [PATCH 24/27] Update collab ui to join channels again --- crates/collab_ui/src/collab_panel.rs | 126 +++++++++++++++++++++------ 1 file changed, 100 insertions(+), 26 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index eeae37bbe558029c5b5000d6824af922f3931688..08c5dd70ad0cb7172ec4f07dfa99d030098a7a56 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,7 +8,7 @@ use crate::{ panel_settings, CollaborationPanelSettings, }; use anyhow::Result; -use call::{participant, ActiveCall}; +use call::ActiveCall; use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use channel_modal::ChannelModal; use client::{proto::PeerId, Client, Contact, User, UserStore}; @@ -95,6 +95,11 @@ pub struct JoinChannelCall { pub channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct JoinChannelChat { + pub channel_id: u64, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct StartMoveChannelFor { channel_id: ChannelId, @@ -151,6 +156,7 @@ impl_actions!( ToggleCollapse, OpenChannelNotes, JoinChannelCall, + JoinChannelChat, LinkChannel, StartMoveChannelFor, StartLinkChannelFor, @@ -198,6 +204,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::collapse_selected_channel); cx.add_action(CollabPanel::expand_selected_channel); cx.add_action(CollabPanel::open_channel_notes); + cx.add_action(CollabPanel::join_channel_chat); cx.add_action( |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { @@ -471,6 +478,12 @@ impl CollabPanel { .iter() .position(|entry| !matches!(entry, ListEntry::Header(_))); } + } else if let editor::Event::Blurred = event { + let query = this.filter_editor.read(cx).text(cx); + if query.is_empty() { + this.selection.take(); + this.update_entries(true, cx); + } } }) .detach(); @@ -555,7 +568,7 @@ impl CollabPanel { &*channel, *depth, path.to_owned(), - &theme.collab_panel, + &theme, is_selected, ix, cx, @@ -1827,12 +1840,13 @@ impl CollabPanel { channel: &Channel, depth: usize, path: ChannelPath, - theme: &theme::CollabPanel, + theme: &theme::Theme, is_selected: bool, ix: usize, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let collab_theme = &theme.collab_panel; let has_children = self.channel_store.read(cx).has_children(channel_id); let other_selected = self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); @@ -1851,6 +1865,8 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; enum ChannelCall {} + enum IconTooltip {} + enum ChannelTooltip {} let mut is_dragged_over = false; if cx @@ -1886,18 +1902,29 @@ impl CollabPanel { Flex::::row() .with_child( Svg::new("icons/hash.svg") - .with_color(theme.channel_hash.color) + .with_color(collab_theme.channel_hash.color) .constrained() - .with_width(theme.channel_hash.width) + .with_width(collab_theme.channel_hash.width) .aligned() .left(), ) .with_child( - Label::new(channel.name.clone(), theme.channel_name.text.clone()) + Label::new(channel.name.clone(), collab_theme.channel_name.text.clone()) .contained() - .with_style(theme.channel_name.container) + .with_style(collab_theme.channel_name.container) .aligned() .left() + .with_tooltip::( + channel_id as usize, + if is_active { + "Open channel notes" + } else { + "Join channel" + }, + None, + theme.tooltip.clone(), + cx, + ) .flex(1., true), ) .with_child( @@ -1907,14 +1934,14 @@ impl CollabPanel { if !participants.is_empty() { let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - FacePile::new(theme.face_overlap) + FacePile::new(collab_theme.face_overlap) .with_children( participants .iter() .filter_map(|user| { Some( Image::from_data(user.avatar.clone()?) - .with_style(theme.channel_avatar), + .with_style(collab_theme.channel_avatar), ) }) .take(FACEPILE_LIMIT), @@ -1922,28 +1949,48 @@ impl CollabPanel { .with_children((extra_count > 0).then(|| { Label::new( format!("+{}", extra_count), - theme.extra_participant_label.text.clone(), + collab_theme.extra_participant_label.text.clone(), ) .contained() - .with_style(theme.extra_participant_label.container) + .with_style(collab_theme.extra_participant_label.container) })) + .with_tooltip::( + channel_id as usize, + if is_active { + "Open Channel Notes" + } else { + "Join channel" + }, + None, + theme.tooltip.clone(), + cx, + ) .into_any() } else if row_hovered { Svg::new("icons/file.svg") - .with_color(theme.channel_hash.color) + .with_color(collab_theme.channel_hash.color) .constrained() - .with_width(theme.channel_hash.width) + .with_width(collab_theme.channel_hash.width) + .with_tooltip::( + channel_id as usize, + "Open channel notes", + None, + theme.tooltip.clone(), + cx, + ) .into_any() } else { Empty::new().into_any() } }) .on_click(MouseButton::Left, move |_, this, cx| { - if !is_active { - this.join_channel_call(channel_id, 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.open_channel_notes(&OpenChannelNotes { channel_id }, cx) - } + this.join_channel_call(channel_id, cx); + }; }), ) .align_children_center() @@ -1955,19 +2002,19 @@ impl CollabPanel { }), ) .with_id(ix) - .with_style(theme.disclosure.clone()) + .with_style(collab_theme.disclosure.clone()) .element() .constrained() - .with_height(theme.row_height) + .with_height(collab_theme.row_height) .contained() .with_style(select_state( - theme + collab_theme .channel_row .in_state(is_selected || is_active || is_dragged_over), )) .with_padding_left( - theme.channel_row.default_style().padding.left - + theme.channel_indent * depth as f32, + collab_theme.channel_row.default_style().padding.left + + collab_theme.channel_indent * depth as f32, ) }) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1977,8 +2024,6 @@ impl CollabPanel { } else { this.join_channel_call(channel_id, cx) } - - this.join_channel_chat(channel_id, cx); } }) .on_click(MouseButton::Right, { @@ -2402,6 +2447,13 @@ impl CollabPanel { }, )); + items.push(ContextMenuItem::action( + "Open Chat", + JoinChannelChat { + channel_id: path.channel_id(), + }, + )); + if self.channel_store.read(cx).is_user_admin(path.channel_id()) { let parent_id = path.parent_id(); @@ -2598,7 +2650,28 @@ impl CollabPanel { } } ListEntry::Channel { channel, .. } => { - self.join_channel_chat(channel.id, cx); + let is_active = iife!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + + dbg!(call_channel, channel.id); + Some(call_channel == channel.id) + }) + .unwrap_or(false); + dbg!(is_active); + if is_active { + self.open_channel_notes( + &OpenChannelNotes { + channel_id: channel.id, + }, + cx, + ) + } else { + self.join_channel_call(channel.id, cx) + } } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), _ => {} @@ -3034,7 +3107,8 @@ impl CollabPanel { .detach_and_log_err(cx); } - fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext) { + fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { + let channel_id = action.channel_id; if let Some(workspace) = self.workspace.upgrade(cx) { cx.app_context().defer(move |cx| { workspace.update(cx, |workspace, cx| { From d8bfe77a3b4e307393e928156fdac1d8c2c83254 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 12:00:02 -0600 Subject: [PATCH 25/27] Scroll so that collab panel is in good state for calls --- crates/collab_ui/src/collab_panel.rs | 84 ++++++++++++++++------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 08c5dd70ad0cb7172ec4f07dfa99d030098a7a56..a7080ad0514657871390acb1f4ee9777c5d4424b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -781,9 +781,16 @@ impl CollabPanel { let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); let old_entries = mem::take(&mut self.entries); + let mut scroll_to_top = false; if let Some(room) = ActiveCall::global(cx).read(cx).room() { self.entries.push(ListEntry::Header(Section::ActiveCall)); + if !old_entries + .iter() + .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + { + scroll_to_top = true; + } if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); @@ -1151,44 +1158,49 @@ impl CollabPanel { } let old_scroll_top = self.list_state.logical_scroll_top(); + self.list_state.reset(self.entries.len()); - // Attempt to maintain the same scroll position. - if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { - let new_scroll_top = self - .entries - .iter() - .position(|entry| entry == old_top_entry) - .map(|item_ix| ListOffset { - item_ix, - offset_in_item: old_scroll_top.offset_in_item, - }) - .or_else(|| { - let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; - let item_ix = self - .entries - .iter() - .position(|entry| entry == entry_after_old_top)?; - Some(ListOffset { + if scroll_to_top { + self.list_state.scroll_to(ListOffset::default()); + } else { + // Attempt to maintain the same scroll position. + if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { + let new_scroll_top = self + .entries + .iter() + .position(|entry| entry == old_top_entry) + .map(|item_ix| ListOffset { item_ix, - offset_in_item: 0., + offset_in_item: old_scroll_top.offset_in_item, }) - }) - .or_else(|| { - let entry_before_old_top = - old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; - let item_ix = self - .entries - .iter() - .position(|entry| entry == entry_before_old_top)?; - Some(ListOffset { - item_ix, - offset_in_item: 0., + .or_else(|| { + let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_after_old_top)?; + Some(ListOffset { + item_ix, + offset_in_item: 0., + }) }) - }); + .or_else(|| { + let entry_before_old_top = + old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_before_old_top)?; + Some(ListOffset { + item_ix, + offset_in_item: 0., + }) + }); - self.list_state - .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); + self.list_state + .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); + } } cx.notify(); @@ -1989,7 +2001,7 @@ impl CollabPanel { if is_active || participants.is_empty() { this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); } else { - this.join_channel_call(channel_id, cx); + this.join_channel(channel_id, cx); }; }), ) @@ -2022,7 +2034,7 @@ impl CollabPanel { if is_active { this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) } else { - this.join_channel_call(channel_id, cx) + this.join_channel(channel_id, cx) } } }) @@ -2670,7 +2682,7 @@ impl CollabPanel { cx, ) } else { - self.join_channel_call(channel.id, cx) + self.join_channel(channel.id, cx) } } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), @@ -3074,7 +3086,7 @@ impl CollabPanel { .detach_and_log_err(cx); } - fn join_channel_call(&self, channel_id: u64, cx: &mut ViewContext) { + fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { let join = ActiveCall::global(cx).update(cx, |call, cx| call.join_channel(channel_id, cx)); let workspace = self.workspace.clone(); From d696b394c45daa5212ece6e4914ba684aa925441 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 12:54:39 -0600 Subject: [PATCH 26/27] Tooltips for contacts --- crates/collab_ui/src/collab_panel.rs | 114 ++++++++++++++++++--------- 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a7080ad0514657871390acb1f4ee9777c5d4424b..ce18a7b92d0eb12136cd834fb92f6faaa6c4b0bf 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -621,7 +621,7 @@ impl CollabPanel { contact, *calling, &this.project, - &theme.collab_panel, + &theme, is_selected, cx, ), @@ -1658,15 +1658,19 @@ impl CollabPanel { contact: &Contact, calling: bool, project: &ModelHandle, - theme: &theme::CollabPanel, + theme: &theme::Theme, is_selected: bool, cx: &mut ViewContext, ) -> AnyElement { + enum ContactTooltip {}; + + let collab_theme = &theme.collab_panel; let online = contact.online; let busy = contact.busy || calling; let user_id = contact.user.id; let github_login = contact.user.github_login.clone(); let initial_project = project.clone(); + let mut event_handler = MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { Flex::row() @@ -1677,9 +1681,9 @@ impl CollabPanel { .collapsed() .contained() .with_style(if busy { - theme.contact_status_busy + collab_theme.contact_status_busy } else { - theme.contact_status_free + collab_theme.contact_status_free }) .aligned(), ) @@ -1689,7 +1693,7 @@ impl CollabPanel { Stack::new() .with_child( Image::from_data(avatar) - .with_style(theme.contact_avatar) + .with_style(collab_theme.contact_avatar) .aligned() .left(), ) @@ -1698,58 +1702,94 @@ impl CollabPanel { .with_child( Label::new( contact.user.github_login.clone(), - theme.contact_username.text.clone(), + collab_theme.contact_username.text.clone(), ) .contained() - .with_style(theme.contact_username.container) + .with_style(collab_theme.contact_username.container) .aligned() .left() .flex(1., true), ) - .with_child( - MouseEventHandler::new::( - contact.user.id as usize, - cx, - |mouse_state, _| { - let button_style = theme.contact_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .flex_float() - }, + .with_children(if state.hovered() { + Some( + MouseEventHandler::new::( + contact.user.id as usize, + cx, + |mouse_state, _| { + let button_style = + collab_theme.contact_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .flex_float() + }, + ) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_contact(user_id, &github_login, cx); + }) + .flex_float(), ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_contact(user_id, &github_login, cx); - }) - .flex_float(), - ) + } else { + None + }) .with_children(if calling { Some( - Label::new("Calling", theme.calling_indicator.text.clone()) + Label::new("Calling", collab_theme.calling_indicator.text.clone()) .contained() - .with_style(theme.calling_indicator.container) + .with_style(collab_theme.calling_indicator.container) .aligned(), ) } else { None }) .constrained() - .with_height(theme.row_height) + .with_height(collab_theme.row_height) .contained() - .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if online && !busy { - this.call(user_id, Some(initial_project.clone()), cx); - } + .with_style( + *collab_theme + .contact_row + .in_state(is_selected) + .style_for(state), + ) }); - if online { - event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand); - } + if online && !busy { + let room = ActiveCall::global(cx).read(cx).room(); + let label = if room.is_some() { + format!("Invite {} to join call", contact.user.github_login) + } else { + format!("Call {}", contact.user.github_login) + }; - event_handler.into_any() + event_handler + .on_click(MouseButton::Left, move |_, this, cx| { + this.call(user_id, Some(initial_project.clone()), cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + contact.user.id as usize, + label, + None, + theme.tooltip.clone(), + cx, + ) + .into_any() + } else { + event_handler + .with_tooltip::( + contact.user.id as usize, + format!( + "{} is {}", + contact.user.github_login, + if busy { "on a call" } else { "offline" } + ), + None, + theme.tooltip.clone(), + cx, + ) + .into_any() + } } fn render_contact_placeholder( From 044fb9e2f5ac99afdd2cd6b10f7d5adea3ef258c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 13:41:24 -0600 Subject: [PATCH 27/27] Confirm on switching channels --- crates/collab_ui/src/collab_panel.rs | 29 ++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ce18a7b92d0eb12136cd834fb92f6faaa6c4b0bf..ab6261c568f23fa70a0831120febc3ea3720f234 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1662,7 +1662,7 @@ impl CollabPanel { is_selected: bool, cx: &mut ViewContext, ) -> AnyElement { - enum ContactTooltip {}; + enum ContactTooltip {} let collab_theme = &theme.collab_panel; let online = contact.online; @@ -1671,7 +1671,7 @@ impl CollabPanel { let github_login = contact.user.github_login.clone(); let initial_project = project.clone(); - let mut event_handler = + let event_handler = MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { @@ -3127,11 +3127,28 @@ impl CollabPanel { } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { - let join = ActiveCall::global(cx).update(cx, |call, cx| call.join_channel(channel_id, cx)); let workspace = self.workspace.clone(); - + let window = cx.window(); + let active_call = ActiveCall::global(cx); cx.spawn(|_, mut cx| async move { - let room = join.await?; + if active_call.read_with(&mut cx, |active_call, _| active_call.room().is_some()) { + let answer = window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Yes, Join Channel", "Cancel"], + &mut cx, + ); + + if let Some(mut answer) = answer { + if answer.next().await == Some(1) { + return anyhow::Ok(()); + } + } + } + + let room = active_call + .update(&mut cx, |call, cx| call.join_channel(channel_id, cx)) + .await?; let tasks = room.update(&mut cx, |room, cx| { let Some(workspace) = workspace.upgrade(cx) else { @@ -3154,7 +3171,7 @@ impl CollabPanel { for task in tasks { task.await?; } - Ok::<(), anyhow::Error>(()) + anyhow::Ok(()) }) .detach_and_log_err(cx); }