From 53194ede5e3d985163cb3eb220430bc569ad62ed Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Sep 2023 13:13:39 -0600 Subject: [PATCH 01/67] Use SaveAll instead of Save If we're closing items we should not be writing files that have not changed (e.g. empty untitled buffers) --- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e271008637..e8f46d4b27 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -921,7 +921,7 @@ impl Pane { ) })?; match answer.next().await { - Some(0) => save_intent = SaveIntent::Save, + Some(0) => save_intent = SaveIntent::SaveAll, Some(1) => save_intent = SaveIntent::Skip, _ => {} } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f081eb9efa..48f8b99e47 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1409,7 +1409,7 @@ impl Workspace { ) })?; match answer.next().await { - Some(0) => save_intent = SaveIntent::Save, + Some(0) => save_intent = SaveIntent::SaveAll, Some(1) => save_intent = SaveIntent::Skip, _ => {} } From d9c08de58a7a164df43614791e5318d5d9824a05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 Sep 2023 17:15:26 +0200 Subject: [PATCH 02/67] Revert "Revert "leverage file outline and selection as opposed to entire file"" --- crates/assistant/src/assistant.rs | 1 + crates/assistant/src/assistant_panel.rs | 119 ++------ crates/assistant/src/prompts.rs | 382 ++++++++++++++++++++++++ 3 files changed, 412 insertions(+), 90 deletions(-) create mode 100644 crates/assistant/src/prompts.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 258684db47..6c9b14333e 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 42e5fb7897..37d0d729fe 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 range = pending_assist.codegen.read(cx).range(); - let selected_text = snapshot.text_for_range(range.clone()).collect::(); + 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 language = snapshot.language_at(range.start); + 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_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 0000000000..ee58090d04 --- /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 03/67] 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 0000000000..7174eec3c3 --- /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 04/67] More logging on collab by default --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index f6fde3cd92..2eb7de20fb 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 05/67] 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 5eae700404..4ddfbc5a34 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 b8cc8fb1b8..6aa41708e3 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 9f3b7d2f30..546b8ef407 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 ee8690ea70..1ddf1a1f66 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 1ca2d839c0..b1595fb0d9 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 44a70f9a08..8d9a4c1550 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 672907b22c..63c057a8eb 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 06/67] 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 | 1075 +--------------- 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, 1263 insertions(+), 1127 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 b0f5b96fde..e78bbe3466 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 0000000000..d7acae3995 --- /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 b17b7b3fc2..4008a941dd 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() { @@ -6387,547 +6379,6 @@ async fn test_contact_requests( } } -#[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(iterations = 10)] async fn test_join_call_after_screen_was_shared( deterministic: Arc, @@ -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 546b8ef407..879b375cd4 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 40adeccd11..c12cb261c8 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 8d9a4c1550..38773fb8cc 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 1469c02998a81649d87b000c898452be4fadf9e1 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 26 Sep 2023 13:29:23 -0700 Subject: [PATCH 07/67] Add observed_channel_notes table and implement note diffing --- .../20221109000000_test_schema.sql | 11 + .../20230925210437_add_observed_notes.sql | 9 + crates/collab/src/db/queries/buffers.rs | 235 ++++++++++++++++-- crates/collab/src/db/tables.rs | 1 + .../src/db/tables/observed_note_edits.rs | 42 ++++ crates/collab/src/db/tests/buffer_tests.rs | 108 ++++++++ 6 files changed, 384 insertions(+), 22 deletions(-) create mode 100644 crates/collab/migrations/20230925210437_add_observed_notes.sql create mode 100644 crates/collab/src/db/tables/observed_note_edits.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index d8325755f8..982d2b290e 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -289,3 +289,14 @@ CREATE TABLE "user_features" ( CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); + + +CREATE TABLE "observed_channel_note_edits" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +CREATE UNIQUE INDEX "index_observed_notes_user_and_channel_id" ON "observed_channel_note_edits" ("user_id", "channel_id"); diff --git a/crates/collab/migrations/20230925210437_add_observed_notes.sql b/crates/collab/migrations/20230925210437_add_observed_notes.sql new file mode 100644 index 0000000000..ce3547b281 --- /dev/null +++ b/crates/collab/migrations/20230925210437_add_observed_notes.sql @@ -0,0 +1,9 @@ +CREATE TABLE "observed_channel_note_edits" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +CREATE UNIQUE INDEX "index_observed_notes_user_and_channel_id" ON "observed_channel_note_edits" ("user_id", "channel_id"); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 4b149faf2a..51848f1e61 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,5 +1,6 @@ use super::*; use prost::Message; +use sea_query::Order::Desc; use text::{EditOperation, UndoOperation}; pub struct LeftChannelBuffer { @@ -74,7 +75,62 @@ impl Database { .await?; collaborators.push(collaborator); - let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; + let (base_text, operations, max_operation) = + self.get_buffer_state(&buffer, &tx).await?; + + // Save the last observed operation + if let Some(max_operation) = max_operation { + observed_note_edits::Entity::insert(observed_note_edits::ActiveModel { + user_id: ActiveValue::Set(user_id), + channel_id: ActiveValue::Set(channel_id), + epoch: ActiveValue::Set(max_operation.0), + lamport_timestamp: ActiveValue::Set(max_operation.1), + }) + .on_conflict( + OnConflict::columns([ + observed_note_edits::Column::UserId, + observed_note_edits::Column::ChannelId, + ]) + .update_columns([ + observed_note_edits::Column::Epoch, + observed_note_edits::Column::LamportTimestamp, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + let buffer_max = buffer_operation::Entity::find() + .filter(buffer_operation::Column::BufferId.eq(buffer.id)) + .filter(buffer_operation::Column::Epoch.eq(buffer.epoch.saturating_sub(1))) + .order_by(buffer_operation::Column::Epoch, Desc) + .order_by(buffer_operation::Column::LamportTimestamp, Desc) + .one(&*tx) + .await? + .map(|model| (model.epoch, model.lamport_timestamp)); + + if let Some(buffer_max) = buffer_max { + observed_note_edits::Entity::insert(observed_note_edits::ActiveModel { + user_id: ActiveValue::Set(user_id), + channel_id: ActiveValue::Set(channel_id), + epoch: ActiveValue::Set(buffer_max.0), + lamport_timestamp: ActiveValue::Set(buffer_max.1), + }) + .on_conflict( + OnConflict::columns([ + observed_note_edits::Column::UserId, + observed_note_edits::Column::ChannelId, + ]) + .update_columns([ + observed_note_edits::Column::Epoch, + observed_note_edits::Column::LamportTimestamp, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + } Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), @@ -373,27 +429,35 @@ impl Database { channel_id: ChannelId, ) -> Result> { self.transaction(|tx| async move { - #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] - enum QueryUserIds { - UserId, - } - - let users: Vec = channel_buffer_collaborator::Entity::find() - .select_only() - .column(channel_buffer_collaborator::Column::UserId) - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), - ) - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; - - Ok(users) + self.get_channel_buffer_collaborators_internal(channel_id, &*tx) + .await }) .await } + async fn get_channel_buffer_collaborators_internal( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } + + let users: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::UserId) + .filter( + Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + Ok(users) + } + pub async fn update_channel_buffer( &self, channel_id: ChannelId, @@ -418,7 +482,12 @@ impl Database { .iter() .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) .collect::>(); + if !operations.is_empty() { + // get current channel participants and save the max operation above + self.save_max_operation_for_collaborators(operations.as_slice(), channel_id, &*tx) + .await?; + buffer_operation::Entity::insert_many(operations) .on_conflict( OnConflict::columns([ @@ -455,6 +524,60 @@ impl Database { .await } + async fn save_max_operation_for_collaborators( + &self, + operations: &[buffer_operation::ActiveModel], + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result<()> { + let max_operation = operations + .iter() + .map(|storage_model| { + ( + storage_model.epoch.clone(), + storage_model.lamport_timestamp.clone(), + ) + }) + .max_by( + |(epoch_a, lamport_timestamp_a), (epoch_b, lamport_timestamp_b)| { + epoch_a.as_ref().cmp(epoch_b.as_ref()).then( + lamport_timestamp_a + .as_ref() + .cmp(lamport_timestamp_b.as_ref()), + ) + }, + ) + .unwrap(); + + let users = self + .get_channel_buffer_collaborators_internal(channel_id, tx) + .await?; + + observed_note_edits::Entity::insert_many(users.iter().map(|id| { + observed_note_edits::ActiveModel { + user_id: ActiveValue::Set(*id), + channel_id: ActiveValue::Set(channel_id), + epoch: max_operation.0.clone(), + lamport_timestamp: ActiveValue::Set(*max_operation.1.as_ref()), + } + })) + .on_conflict( + OnConflict::columns([ + observed_note_edits::Column::UserId, + observed_note_edits::Column::ChannelId, + ]) + .update_columns([ + observed_note_edits::Column::Epoch, + observed_note_edits::Column::LamportTimestamp, + ]) + .to_owned(), + ) + .exec(tx) + .await?; + + Ok(()) + } + async fn get_buffer_operation_serialization_version( &self, buffer_id: BufferId, @@ -491,7 +614,7 @@ impl Database { &self, buffer: &buffer::Model, tx: &DatabaseTransaction, - ) -> Result<(String, Vec)> { + ) -> Result<(String, Vec, Option<(i32, i32)>)> { let id = buffer.id; let (base_text, version) = if buffer.epoch > 0 { let snapshot = buffer_snapshot::Entity::find() @@ -519,13 +642,21 @@ impl Database { .stream(&*tx) .await?; let mut operations = Vec::new(); + + let mut max_epoch: Option = None; + let mut max_timestamp: Option = None; while let Some(row) = rows.next().await { + let row = row?; + + max_assign(&mut max_epoch, row.epoch); + max_assign(&mut max_timestamp, row.lamport_timestamp); + operations.push(proto::Operation { - variant: Some(operation_from_storage(row?, version)?), + variant: Some(operation_from_storage(row, version)?), }) } - Ok((base_text, operations)) + Ok((base_text, operations, max_epoch.zip(max_timestamp))) } async fn snapshot_channel_buffer( @@ -534,7 +665,7 @@ impl Database { tx: &DatabaseTransaction, ) -> Result<()> { let buffer = self.get_channel_buffer(channel_id, tx).await?; - let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + let (base_text, operations, _) = self.get_buffer_state(&buffer, tx).await?; if operations.is_empty() { return Ok(()); } @@ -567,6 +698,66 @@ impl Database { Ok(()) } + + pub async fn has_buffer_changed(&self, user_id: UserId, channel_id: ChannelId) -> Result { + self.transaction(|tx| async move { + let user_max = observed_note_edits::Entity::find() + .filter(observed_note_edits::Column::UserId.eq(user_id)) + .filter(observed_note_edits::Column::ChannelId.eq(channel_id)) + .one(&*tx) + .await? + .map(|model| (model.epoch, model.lamport_timestamp)); + + let channel_buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await?; + + let Some(channel_buffer) = channel_buffer else { + return Ok(false); + }; + + let mut channel_max = buffer_operation::Entity::find() + .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) + .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch)) + .order_by(buffer_operation::Column::Epoch, Desc) + .order_by(buffer_operation::Column::LamportTimestamp, Desc) + .one(&*tx) + .await? + .map(|model| (model.epoch, model.lamport_timestamp)); + + // If there are no edits in this epoch + if channel_max.is_none() { + // check if this user observed the last edit of the previous epoch + channel_max = buffer_operation::Entity::find() + .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) + .filter( + buffer_operation::Column::Epoch.eq(channel_buffer.epoch.saturating_sub(1)), + ) + .order_by(buffer_operation::Column::Epoch, Desc) + .order_by(buffer_operation::Column::LamportTimestamp, Desc) + .one(&*tx) + .await? + .map(|model| (model.epoch, model.lamport_timestamp)); + } + + Ok(user_max != channel_max) + }) + .await + } +} + +fn max_assign(max: &mut Option, val: T) { + if let Some(max_val) = max { + if val > *max_val { + *max = Some(val); + } + } else { + *max = Some(val); + } } fn operation_to_storage( diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 81200df3e7..e8deb1501e 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -12,6 +12,7 @@ pub mod contact; pub mod feature_flag; pub mod follower; pub mod language_server; +pub mod observed_note_edits; pub mod project; pub mod project_collaborator; pub mod room; diff --git a/crates/collab/src/db/tables/observed_note_edits.rs b/crates/collab/src/db/tables/observed_note_edits.rs new file mode 100644 index 0000000000..01e0e212ef --- /dev/null +++ b/crates/collab/src/db/tables/observed_note_edits.rs @@ -0,0 +1,42 @@ +use crate::db::{ChannelId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "observed_channel_note_edits")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + pub channel_id: ChannelId, + pub epoch: i32, + pub lamport_timestamp: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 9808a9955b..76edc13136 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -163,3 +163,111 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(buffer_response_b.base_text, "hello, cruel world"); assert_eq!(buffer_response_b.operations, &[]); } + +test_both_dbs!( + test_channel_buffers_diffs, + test_channel_buffers_diffs_postgres, + test_channel_buffers_diffs_sqlite +); + +async fn test_channel_buffers_diffs(db: &Database) { + let a_id = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let b_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let owner_id = db.create_server("production").await.unwrap().0 as u32; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + let connection_id_a = ConnectionId { + owner_id, + id: a_id.0 as u32, + }; + let connection_id_b = ConnectionId { + owner_id, + id: b_id.0 as u32, + }; + + // Zero test: A should not register as changed on an unitialized channel buffer + assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + + let _ = db + .join_channel_buffer(zed_id, a_id, connection_id_a) + .await + .unwrap(); + + // Zero test: A should register as changed on an empty channel buffer + assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + + let mut buffer_a = Buffer::new(0, 0, "".to_string()); + let mut operations = Vec::new(); + operations.push(buffer_a.edit([(0..0, "hello world")])); + assert_eq!(buffer_a.text(), "hello world"); + + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + + db.update_channel_buffer(zed_id, a_id, &operations) + .await + .unwrap(); + + // Smoke test: Does B register as changed, A as unchanged? + assert!(db.has_buffer_changed(b_id, zed_id).await.unwrap()); + assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + + db.leave_channel_buffer(zed_id, connection_id_a) + .await + .unwrap(); + + // Snapshotting from leaving the channel buffer should not have a diff + assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + + let _ = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + + // B has opened the channel buffer, so we shouldn't have any diff + assert!(!db.has_buffer_changed(b_id, zed_id).await.unwrap()); + + db.leave_channel_buffer(zed_id, connection_id_b) + .await + .unwrap(); + + // Since B just opened and closed the buffer without editing, neither should have a diff + assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + assert!(!db.has_buffer_changed(b_id, zed_id).await.unwrap()); +} From 9ba975d6adf45a92bda3e51e32c23454f0329046 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 30 Sep 2023 22:30:36 -0700 Subject: [PATCH 08/67] Channel notifications from the server works --- crates/channel/src/channel_store.rs | 16 +- .../src/channel_store/channel_index.rs | 7 + .../20221109000000_test_schema.sql | 8 +- .../20230925210437_add_observed_notes.sql | 8 +- crates/collab/src/db.rs | 1 + crates/collab/src/db/queries/buffers.rs | 155 +++++++++++------- crates/collab/src/db/queries/channels.rs | 15 +- crates/collab/src/db/tables.rs | 2 +- ...note_edits.rs => observed_buffer_edits.rs} | 18 +- crates/collab/src/db/tests/buffer_tests.rs | 17 +- crates/collab/src/rpc.rs | 27 ++- .../collab/src/tests/channel_buffer_tests.rs | 63 ++++++- crates/collab_ui/src/collab_panel.rs | 19 ++- crates/rpc/proto/zed.proto | 1 + crates/theme/src/theme.rs | 2 +- styles/src/style_tree/collab_panel.ts | 14 +- 16 files changed, 266 insertions(+), 107 deletions(-) rename crates/collab/src/db/tables/{observed_note_edits.rs => observed_buffer_edits.rs} (66%) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a8f6dd67b6..19fc4f35ee 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -43,6 +43,7 @@ pub type ChannelData = (Channel, ChannelPath); pub struct Channel { pub id: ChannelId, pub name: String, + pub has_changed: bool, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] @@ -207,6 +208,13 @@ impl ChannelStore { ) } + pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .get(&channel_id) + .map(|channel| channel.has_changed) + } + pub fn open_channel_chat( &mut self, channel_id: ChannelId, @@ -779,6 +787,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, + has_changed: false, }), ), } @@ -787,7 +796,8 @@ impl ChannelStore { let channels_changed = !payload.channels.is_empty() || !payload.delete_channels.is_empty() || !payload.insert_edge.is_empty() - || !payload.delete_edge.is_empty(); + || !payload.delete_edge.is_empty() + || !payload.notes_changed.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { @@ -814,6 +824,10 @@ impl ChannelStore { index.insert(channel) } + for id_changed in payload.notes_changed { + index.has_changed(id_changed); + } + for edge in payload.insert_edge { index.insert_edge(edge.channel_id, edge.parent_id); } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index d0c49dc298..88c6b85698 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -76,6 +76,12 @@ impl<'a> ChannelPathsInsertGuard<'a> { } } + pub fn has_changed(&mut self, channel_id: ChannelId) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + Arc::make_mut(channel).has_changed = true; + } + } + pub fn insert(&mut self, channel_proto: proto::Channel) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { Arc::make_mut(existing_channel).name = channel_proto.name; @@ -85,6 +91,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { Arc::new(Channel { id: channel_proto.id, name: channel_proto.name, + has_changed: false, }), ); self.insert_root(channel_proto.id); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 982d2b290e..7553392f39 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -291,12 +291,12 @@ CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); -CREATE TABLE "observed_channel_note_edits" ( +CREATE TABLE "observed_buffer_edits" ( "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, - PRIMARY KEY (user_id, channel_id) + PRIMARY KEY (user_id, buffer_id) ); -CREATE UNIQUE INDEX "index_observed_notes_user_and_channel_id" ON "observed_channel_note_edits" ("user_id", "channel_id"); +CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); diff --git a/crates/collab/migrations/20230925210437_add_observed_notes.sql b/crates/collab/migrations/20230925210437_add_observed_notes.sql index ce3547b281..4574fe215b 100644 --- a/crates/collab/migrations/20230925210437_add_observed_notes.sql +++ b/crates/collab/migrations/20230925210437_add_observed_notes.sql @@ -1,9 +1,9 @@ -CREATE TABLE "observed_channel_note_edits" ( +CREATE TABLE "observed_buffer_edits" ( "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, - PRIMARY KEY (user_id, channel_id) + PRIMARY KEY (user_id, buffer_id) ); -CREATE UNIQUE INDEX "index_observed_notes_user_and_channel_id" ON "observed_channel_note_edits" ("user_id", "channel_id"); +CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ab2fbe3945..f0896f8732 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -436,6 +436,7 @@ pub struct Channel { pub struct ChannelsForUser { pub channels: ChannelGraph, pub channel_participants: HashMap>, + pub channels_with_changed_notes: HashSet, pub channels_with_admin_privileges: HashSet, } diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 51848f1e61..f2993c516d 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -80,20 +80,20 @@ impl Database { // Save the last observed operation if let Some(max_operation) = max_operation { - observed_note_edits::Entity::insert(observed_note_edits::ActiveModel { + observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { user_id: ActiveValue::Set(user_id), - channel_id: ActiveValue::Set(channel_id), + buffer_id: ActiveValue::Set(buffer.id), epoch: ActiveValue::Set(max_operation.0), lamport_timestamp: ActiveValue::Set(max_operation.1), }) .on_conflict( OnConflict::columns([ - observed_note_edits::Column::UserId, - observed_note_edits::Column::ChannelId, + observed_buffer_edits::Column::UserId, + observed_buffer_edits::Column::BufferId, ]) .update_columns([ - observed_note_edits::Column::Epoch, - observed_note_edits::Column::LamportTimestamp, + observed_buffer_edits::Column::Epoch, + observed_buffer_edits::Column::LamportTimestamp, ]) .to_owned(), ) @@ -110,20 +110,20 @@ impl Database { .map(|model| (model.epoch, model.lamport_timestamp)); if let Some(buffer_max) = buffer_max { - observed_note_edits::Entity::insert(observed_note_edits::ActiveModel { + observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { user_id: ActiveValue::Set(user_id), - channel_id: ActiveValue::Set(channel_id), + buffer_id: ActiveValue::Set(buffer.id), epoch: ActiveValue::Set(buffer_max.0), lamport_timestamp: ActiveValue::Set(buffer_max.1), }) .on_conflict( OnConflict::columns([ - observed_note_edits::Column::UserId, - observed_note_edits::Column::ChannelId, + observed_buffer_edits::Column::UserId, + observed_buffer_edits::Column::BufferId, ]) .update_columns([ - observed_note_edits::Column::Epoch, - observed_note_edits::Column::LamportTimestamp, + observed_buffer_edits::Column::Epoch, + observed_buffer_edits::Column::LamportTimestamp, ]) .to_owned(), ) @@ -463,7 +463,7 @@ impl Database { channel_id: ChannelId, user: UserId, operations: &[proto::Operation], - ) -> Result> { + ) -> Result<(Vec, Vec)> { self.transaction(move |tx| async move { self.check_user_is_channel_member(channel_id, user, &*tx) .await?; @@ -483,10 +483,23 @@ impl Database { .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) .collect::>(); + let mut channel_members; + if !operations.is_empty() { // get current channel participants and save the max operation above - self.save_max_operation_for_collaborators(operations.as_slice(), channel_id, &*tx) + self.save_max_operation_for_collaborators( + operations.as_slice(), + channel_id, + buffer.id, + &*tx, + ) + .await?; + + channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + let collaborators = self + .get_channel_buffer_collaborators_internal(channel_id, &*tx) .await?; + channel_members.retain(|member| !collaborators.contains(member)); buffer_operation::Entity::insert_many(operations) .on_conflict( @@ -501,6 +514,8 @@ impl Database { ) .exec(&*tx) .await?; + } else { + channel_members = Vec::new(); } let mut connections = Vec::new(); @@ -519,7 +534,7 @@ impl Database { }); } - Ok(connections) + Ok((connections, channel_members)) }) .await } @@ -528,6 +543,7 @@ impl Database { &self, operations: &[buffer_operation::ActiveModel], channel_id: ChannelId, + buffer_id: BufferId, tx: &DatabaseTransaction, ) -> Result<()> { let max_operation = operations @@ -553,22 +569,22 @@ impl Database { .get_channel_buffer_collaborators_internal(channel_id, tx) .await?; - observed_note_edits::Entity::insert_many(users.iter().map(|id| { - observed_note_edits::ActiveModel { + observed_buffer_edits::Entity::insert_many(users.iter().map(|id| { + observed_buffer_edits::ActiveModel { user_id: ActiveValue::Set(*id), - channel_id: ActiveValue::Set(channel_id), + buffer_id: ActiveValue::Set(buffer_id), epoch: max_operation.0.clone(), lamport_timestamp: ActiveValue::Set(*max_operation.1.as_ref()), } })) .on_conflict( OnConflict::columns([ - observed_note_edits::Column::UserId, - observed_note_edits::Column::ChannelId, + observed_buffer_edits::Column::UserId, + observed_buffer_edits::Column::BufferId, ]) .update_columns([ - observed_note_edits::Column::Epoch, - observed_note_edits::Column::LamportTimestamp, + observed_buffer_edits::Column::Epoch, + observed_buffer_edits::Column::LamportTimestamp, ]) .to_owned(), ) @@ -699,54 +715,75 @@ impl Database { Ok(()) } - pub async fn has_buffer_changed(&self, user_id: UserId, channel_id: ChannelId) -> Result { - self.transaction(|tx| async move { - let user_max = observed_note_edits::Entity::find() - .filter(observed_note_edits::Column::UserId.eq(user_id)) - .filter(observed_note_edits::Column::ChannelId.eq(channel_id)) - .one(&*tx) - .await? - .map(|model| (model.epoch, model.lamport_timestamp)); + #[cfg(test)] + pub async fn test_has_note_changed( + &self, + user_id: UserId, + channel_id: ChannelId, + ) -> Result { + self.transaction(|tx| async move { self.has_note_changed(user_id, channel_id, &*tx).await }) + .await + } - let channel_buffer = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(buffer::Entity) + pub async fn has_note_changed( + &self, + user_id: UserId, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result { + let Some(buffer_id) = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await? + .map(|buffer| buffer.id) else { + return Ok(false); + }; + + let user_max = observed_buffer_edits::Entity::find() + .filter(observed_buffer_edits::Column::UserId.eq(user_id)) + .filter(observed_buffer_edits::Column::BufferId.eq(buffer_id)) .one(&*tx) - .await?; + .await? + .map(|model| (model.epoch, model.lamport_timestamp)); - let Some(channel_buffer) = channel_buffer else { - return Ok(false); - }; + let channel_buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await?; - let mut channel_max = buffer_operation::Entity::find() + let Some(channel_buffer) = channel_buffer else { + return Ok(false); + }; + + let mut channel_max = buffer_operation::Entity::find() + .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) + .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch)) + .order_by(buffer_operation::Column::Epoch, Desc) + .order_by(buffer_operation::Column::LamportTimestamp, Desc) + .one(&*tx) + .await? + .map(|model| (model.epoch, model.lamport_timestamp)); + + // If there are no edits in this epoch + if channel_max.is_none() { + // check if this user observed the last edit of the previous epoch + channel_max = buffer_operation::Entity::find() .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) - .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch)) + .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch.saturating_sub(1))) .order_by(buffer_operation::Column::Epoch, Desc) .order_by(buffer_operation::Column::LamportTimestamp, Desc) .one(&*tx) .await? .map(|model| (model.epoch, model.lamport_timestamp)); + } - // If there are no edits in this epoch - if channel_max.is_none() { - // check if this user observed the last edit of the previous epoch - channel_max = buffer_operation::Entity::find() - .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) - .filter( - buffer_operation::Column::Epoch.eq(channel_buffer.epoch.saturating_sub(1)), - ) - .order_by(buffer_operation::Column::Epoch, Desc) - .order_by(buffer_operation::Column::LamportTimestamp, Desc) - .one(&*tx) - .await? - .map(|model| (model.epoch, model.lamport_timestamp)); - } - - Ok(user_max != channel_max) - }) - .await + Ok(user_max != channel_max) } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 16a0891d16..6274550c25 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -391,7 +391,8 @@ impl Database { .all(&*tx) .await?; - self.get_user_channels(channel_memberships, &tx).await + self.get_user_channels(user_id, channel_memberships, &tx) + .await }) .await } @@ -414,13 +415,15 @@ impl Database { .all(&*tx) .await?; - self.get_user_channels(channel_membership, &tx).await + self.get_user_channels(user_id, channel_membership, &tx) + .await }) .await } pub async fn get_user_channels( &self, + user_id: UserId, channel_memberships: Vec, tx: &DatabaseTransaction, ) -> Result { @@ -460,10 +463,18 @@ impl Database { } } + let mut channels_with_changed_notes = HashSet::default(); + for channel in graph.channels.iter() { + if self.has_note_changed(user_id, channel.id, tx).await? { + channels_with_changed_notes.insert(channel.id); + } + } + Ok(ChannelsForUser { channels: graph, channel_participants, channels_with_admin_privileges, + channels_with_changed_notes, }) } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index e8deb1501e..4068606546 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -12,7 +12,7 @@ pub mod contact; pub mod feature_flag; pub mod follower; pub mod language_server; -pub mod observed_note_edits; +pub mod observed_buffer_edits; pub mod project; pub mod project_collaborator; pub mod room; diff --git a/crates/collab/src/db/tables/observed_note_edits.rs b/crates/collab/src/db/tables/observed_buffer_edits.rs similarity index 66% rename from crates/collab/src/db/tables/observed_note_edits.rs rename to crates/collab/src/db/tables/observed_buffer_edits.rs index 01e0e212ef..db027f78b2 100644 --- a/crates/collab/src/db/tables/observed_note_edits.rs +++ b/crates/collab/src/db/tables/observed_buffer_edits.rs @@ -1,12 +1,12 @@ -use crate::db::{ChannelId, UserId}; +use crate::db::{BufferId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "observed_channel_note_edits")] +#[sea_orm(table_name = "observed_buffer_edits")] pub struct Model { #[sea_orm(primary_key)] pub user_id: UserId, - pub channel_id: ChannelId, + pub buffer_id: BufferId, pub epoch: i32, pub lamport_timestamp: i32, } @@ -14,11 +14,11 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::channel::Entity", - from = "Column::ChannelId", - to = "super::channel::Column::Id" + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" )] - Channel, + Buffer, #[sea_orm( belongs_to = "super::user::Entity", from = "Column::UserId", @@ -27,9 +27,9 @@ pub enum Relation { User, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Channel.def() + Relation::Buffer.def() } } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 76edc13136..115a20ffa6 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -220,7 +220,7 @@ async fn test_channel_buffers_diffs(db: &Database) { }; // Zero test: A should not register as changed on an unitialized channel buffer - assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); let _ = db .join_channel_buffer(zed_id, a_id, connection_id_a) @@ -228,7 +228,7 @@ async fn test_channel_buffers_diffs(db: &Database) { .unwrap(); // Zero test: A should register as changed on an empty channel buffer - assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); let mut buffer_a = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); @@ -245,15 +245,16 @@ async fn test_channel_buffers_diffs(db: &Database) { .unwrap(); // Smoke test: Does B register as changed, A as unchanged? - assert!(db.has_buffer_changed(b_id, zed_id).await.unwrap()); - assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + assert!(db.test_has_note_changed(b_id, zed_id).await.unwrap()); + + assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); db.leave_channel_buffer(zed_id, connection_id_a) .await .unwrap(); // Snapshotting from leaving the channel buffer should not have a diff - assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); + assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); let _ = db .join_channel_buffer(zed_id, b_id, connection_id_b) @@ -261,13 +262,13 @@ async fn test_channel_buffers_diffs(db: &Database) { .unwrap(); // B has opened the channel buffer, so we shouldn't have any diff - assert!(!db.has_buffer_changed(b_id, zed_id).await.unwrap()); + assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap()); db.leave_channel_buffer(zed_id, connection_id_b) .await .unwrap(); // Since B just opened and closed the buffer without editing, neither should have a diff - assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap()); - assert!(!db.has_buffer_changed(b_id, zed_id).await.unwrap()); + assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); + assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap()); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9e14c48473..b9dae999cd 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2691,7 +2691,7 @@ async fn update_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let collaborators = db + let (collaborators, non_collaborators) = db .update_channel_buffer(channel_id, session.user_id, &request.operations) .await?; @@ -2704,6 +2704,25 @@ async fn update_channel_buffer( }, &session.peer, ); + + let pool = &*session.connection_pool().await; + + broadcast( + None, + non_collaborators + .iter() + .flat_map(|user_id| pool.user_connection_ids(*user_id)), + |peer_id| { + session.peer.send( + peer_id.into(), + proto::UpdateChannels { + notes_changed: vec![channel_id.to_proto()], + ..Default::default() + }, + ) + }, + ); + Ok(()) } @@ -2986,6 +3005,12 @@ fn build_initial_channels_update( }); } + update.notes_changed = channels + .channels_with_changed_notes + .iter() + .map(|channel_id| channel_id.to_proto()) + .collect(); + update.insert_edge = channels.channels.edges; for (channel_id, participants) in channels.channel_participants { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 05abda5af3..0f87aeb48f 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -410,10 +410,7 @@ async fn test_channel_buffer_disconnect( channel_buffer_a.update(cx_a, |buffer, _| { assert_eq!( buffer.channel().as_ref(), - &Channel { - id: channel_id, - name: "the-channel".to_string() - } + &channel(channel_id, "the-channel") ); assert!(!buffer.is_connected()); }); @@ -438,15 +435,20 @@ async fn test_channel_buffer_disconnect( channel_buffer_b.update(cx_b, |buffer, _| { assert_eq!( buffer.channel().as_ref(), - &Channel { - id: channel_id, - name: "the-channel".to_string() - } + &channel(channel_id, "the-channel") ); assert!(!buffer.is_connected()); }); } +fn channel(id: u64, name: &'static str) -> Channel { + Channel { + id, + name: name.to_string(), + has_changed: false, + } +} + #[gpui::test] async fn test_rejoin_channel_buffer( deterministic: Arc, @@ -627,6 +629,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( 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; cx_a.update(editor::init); @@ -757,6 +760,50 @@ async fn test_following_to_channel_notes_without_a_shared_project( }); } +#[gpui::test] +async fn test_channel_buffer_changes( + 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; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + + assert!(has_buffer_changed); +} + #[track_caller] fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { let mut user_ids = collaborators diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16a9ec563b..4f81d07ea7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1816,12 +1816,19 @@ impl CollabPanel { .left(), ) .with_child( - Label::new(channel.name.clone(), theme.channel_name.text.clone()) - .contained() - .with_style(theme.channel_name.container) - .aligned() - .left() - .flex(1., true), + Label::new( + channel.name.clone(), + theme + .channel_name + .in_state(channel.has_changed) + .text + .clone(), + ) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), ) .with_child( MouseEventHandler::new::(ix, cx, move |_, cx| { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a62a9f06c3..c53db447d3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -955,6 +955,7 @@ message UpdateChannels { repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; repeated ChannelPermission channel_permissions = 8; + repeated uint64 notes_changed = 9; } message ChannelEdge { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2d839c0..6c32bfd129 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -251,7 +251,7 @@ pub struct CollabPanel { pub leave_call: Interactive, pub contact_row: Toggleable>, pub channel_row: Toggleable>, - pub channel_name: ContainedText, + pub channel_name: Toggleable, pub row_height: f32, pub project_row: Toggleable>, pub tree_branch: Toggleable>, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 4d605d118c..33b36e36f4 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -267,10 +267,18 @@ export default function contacts_panel(): any { }), channel_row: item_row, channel_name: { - ...text(layer, "sans", { size: "sm" }), - margin: { - left: CHANNEL_SPACING, + active: { + ...text(layer, "sans", { size: "sm", weight: "bold" }), + margin: { + left: CHANNEL_SPACING, + }, }, + inactive: { + ...text(layer, "sans", { size: "sm" }), + margin: { + left: CHANNEL_SPACING, + }, + } }, list_empty_label_container: { margin: { From e0ff7ba18082373ab15bb46e44ce43167e08635c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 1 Oct 2023 19:53:32 -0700 Subject: [PATCH 09/67] Add channel note indicator and clear changed status --- crates/channel/src/channel_store.rs | 20 ++++-- .../src/channel_store/channel_index.rs | 12 +++- .../collab/src/tests/channel_buffer_tests.rs | 63 ++++++++++++++++++- crates/collab_ui/src/collab_panel.rs | 33 +++++++++- crates/theme/src/theme.rs | 1 + styles/src/style_tree/collab_panel.ts | 1 + 6 files changed, 119 insertions(+), 11 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 19fc4f35ee..417f486b9e 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -43,7 +43,7 @@ pub type ChannelData = (Channel, ChannelPath); pub struct Channel { pub id: ChannelId, pub name: String, - pub has_changed: bool, + pub has_note_changed: bool, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] @@ -200,19 +200,27 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - self.open_channel_resource( + let open_channel_buffer = self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), cx, - ) + ); + cx.spawn(|this, mut cx| async move { + let buffer = open_channel_buffer.await?; + this.update(&mut cx, |this, cx| { + this.channel_index.clear_note_changed(channel_id); + cx.notify(); + }); + Ok(buffer) + }) } pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option { self.channel_index .by_id() .get(&channel_id) - .map(|channel| channel.has_changed) + .map(|channel| channel.has_note_changed) } pub fn open_channel_chat( @@ -787,7 +795,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, - has_changed: false, + has_note_changed: false, }), ), } @@ -825,7 +833,7 @@ impl ChannelStore { } for id_changed in payload.notes_changed { - index.has_changed(id_changed); + index.note_changed(id_changed); } for edge in payload.insert_edge { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 88c6b85698..42474f1d85 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -38,6 +38,12 @@ impl ChannelIndex { channels_by_id: &mut self.channels_by_id, } } + + pub fn clear_note_changed(&mut self, channel_id: ChannelId) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + Arc::make_mut(channel).has_note_changed = false; + } + } } impl Deref for ChannelIndex { @@ -76,9 +82,9 @@ impl<'a> ChannelPathsInsertGuard<'a> { } } - pub fn has_changed(&mut self, channel_id: ChannelId) { + pub fn note_changed(&mut self, channel_id: ChannelId) { if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { - Arc::make_mut(channel).has_changed = true; + Arc::make_mut(channel).has_note_changed = true; } } @@ -91,7 +97,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { Arc::new(Channel { id: channel_proto.id, name: channel_proto.name, - has_changed: false, + has_note_changed: false, }), ); self.insert_root(channel_proto.id); diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 0f87aeb48f..7ca6f0db3f 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -445,7 +445,7 @@ fn channel(id: u64, name: &'static str) -> Channel { Channel { id, name: name.to_string(), - has_changed: false, + has_note_changed: false, } } @@ -786,6 +786,7 @@ async fn test_channel_buffer_changes( .await .unwrap(); + // Client A makes an edit, and client B should see that the note has changed. channel_buffer_a.update(cx_a, |buffer, cx| { buffer.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "1")], None, cx); @@ -802,6 +803,66 @@ async fn test_channel_buffer_changes( }); assert!(has_buffer_changed); + + // Opening the buffer should clear the changed flag. + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + + assert!(!has_buffer_changed); + + // Editing the channel while the buffer is open shuold not show that the buffer has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "2")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + + assert!(!has_buffer_changed); + + // Closing the buffer should re-enable change tracking + cx_b.update(|_| { + drop(channel_buffer_b); + }); + + deterministic.run_until_parked(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "3")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + + assert!(has_buffer_changed); } #[track_caller] diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4f81d07ea7..0fd265d0aa 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1774,6 +1774,7 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; enum ChannelCall {} + enum ChannelNote {} let mut is_dragged_over = false; if cx @@ -1820,7 +1821,7 @@ impl CollabPanel { channel.name.clone(), theme .channel_name - .in_state(channel.has_changed) + .in_state(channel.has_note_changed) .text .clone(), ) @@ -1863,6 +1864,8 @@ impl CollabPanel { .with_color(theme.channel_hash.color) .constrained() .with_width(theme.channel_hash.width) + .contained() + .with_margin_right(theme.channel_hash.container.margin.left) .into_any() } else { Empty::new().into_any() @@ -1872,6 +1875,34 @@ impl CollabPanel { this.join_channel_call(channel_id, cx); }), ) + .with_child( + MouseEventHandler::new::(ix, cx, move |_, cx| { + let participants = + self.channel_store.read(cx).channel_participants(channel_id); + if participants.is_empty() { + if channel.has_note_changed { + Svg::new("icons/terminal.svg") + .with_color(theme.channel_note_active_color) + .constrained() + .with_width(theme.channel_hash.width) + .into_any() + } else if row_hovered { + Svg::new("icons/terminal.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .into_any() + } else { + Empty::new().into_any() + } + } else { + Empty::new().into_any() + } + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + }), + ) .align_children_center() .styleable_component() .disclosable( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6c32bfd129..875fdd892e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -238,6 +238,7 @@ pub struct CollabPanel { pub log_in_button: Interactive, pub channel_editor: ContainerStyle, pub channel_hash: Icon, + pub channel_note_active_color: Color, pub tabbed_modal: TabbedModal, pub contact_finder: ContactFinder, pub channel_modal: ChannelModal, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 33b36e36f4..2a7702842a 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -194,6 +194,7 @@ export default function contacts_panel(): any { }, user_query_editor: filter_input, channel_hash: icon_style, + channel_note_active_color: foreground(layer, "active"), user_query_editor_height: 33, add_contact_button: header_icon_button, add_channel_button: header_icon_button, From 51cf6a5ff34e188685f17d70c242c52b0ff5c20a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 1 Oct 2023 21:31:36 -0700 Subject: [PATCH 10/67] Add database implementation of channel message change tracking --- .../20221109000000_test_schema.sql | 9 ++ .../20230925210437_add_channel_changes.sql | 18 +++ .../20230925210437_add_observed_notes.sql | 9 -- crates/collab/src/db/queries.rs | 10 ++ crates/collab/src/db/queries/buffers.rs | 10 -- crates/collab/src/db/queries/messages.rs | 121 +++++++++++++++ crates/collab/src/db/tables.rs | 1 + .../db/tables/observed_channel_messages.rs | 41 ++++++ crates/collab/src/db/tests/message_tests.rs | 139 ++++++++++++++++++ 9 files changed, 339 insertions(+), 19 deletions(-) create mode 100644 crates/collab/migrations/20230925210437_add_channel_changes.sql delete mode 100644 crates/collab/migrations/20230925210437_add_observed_notes.sql create mode 100644 crates/collab/src/db/tables/observed_channel_messages.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7553392f39..277a78d2d6 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -300,3 +300,12 @@ CREATE TABLE "observed_buffer_edits" ( ); CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); + +CREATE TABLE IF NOT EXISTS "observed_channel_messages" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "channel_message_id" INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id"); diff --git a/crates/collab/migrations/20230925210437_add_channel_changes.sql b/crates/collab/migrations/20230925210437_add_channel_changes.sql new file mode 100644 index 0000000000..7787975c1c --- /dev/null +++ b/crates/collab/migrations/20230925210437_add_channel_changes.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS "observed_buffer_edits" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + PRIMARY KEY (user_id, buffer_id) +); + +CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); + +CREATE TABLE IF NOT EXISTS "observed_channel_messages" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "channel_message_id" INTEGER NOT NULL, + PRIMARY KEY (user_id, channel_id) +); + +CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id"); diff --git a/crates/collab/migrations/20230925210437_add_observed_notes.sql b/crates/collab/migrations/20230925210437_add_observed_notes.sql deleted file mode 100644 index 4574fe215b..0000000000 --- a/crates/collab/migrations/20230925210437_add_observed_notes.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE "observed_buffer_edits" ( - "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, - "epoch" INTEGER NOT NULL, - "lamport_timestamp" INTEGER NOT NULL, - PRIMARY KEY (user_id, buffer_id) -); - -CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id"); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 80bd8704b2..59face1f33 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -9,3 +9,13 @@ pub mod projects; pub mod rooms; pub mod servers; pub mod users; + +fn max_assign(max: &mut Option, val: T) { + if let Some(max_val) = max { + if val > *max_val { + *max = Some(val); + } + } else { + *max = Some(val); + } +} diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index f2993c516d..1e8dd30c6b 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -787,16 +787,6 @@ impl Database { } } -fn max_assign(max: &mut Option, val: T) { - if let Some(max_val) = max { - if val > *max_val { - *max = Some(val); - } - } else { - *max = Some(val); - } -} - fn operation_to_storage( operation: &proto::Operation, buffer: &buffer::Model, diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 0b88df6716..328737dd0a 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -93,9 +93,13 @@ impl Database { .stream(&*tx) .await?; + let mut max_id = None; let mut messages = Vec::new(); while let Some(row) = rows.next().await { let row = row?; + dbg!(&max_id); + max_assign(&mut max_id, row.id); + let nonce = row.nonce.as_u64_pair(); messages.push(proto::ChannelMessage { id: row.id.to_proto(), @@ -108,6 +112,55 @@ impl Database { }), }); } + drop(rows); + dbg!(&max_id); + + if let Some(max_id) = max_id { + let has_older_message = dbg!( + observed_channel_messages::Entity::find() + .filter( + observed_channel_messages::Column::UserId + .eq(user_id) + .and(observed_channel_messages::Column::ChannelId.eq(channel_id)) + .and( + observed_channel_messages::Column::ChannelMessageId.lt(max_id) + ), + ) + .one(&*tx) + .await + )? + .is_some(); + + if has_older_message { + observed_channel_messages::Entity::update( + observed_channel_messages::ActiveModel { + user_id: ActiveValue::Unchanged(user_id), + channel_id: ActiveValue::Unchanged(channel_id), + channel_message_id: ActiveValue::Set(max_id), + }, + ) + .exec(&*tx) + .await?; + } else { + observed_channel_messages::Entity::insert( + observed_channel_messages::ActiveModel { + user_id: ActiveValue::Set(user_id), + channel_id: ActiveValue::Set(channel_id), + channel_message_id: ActiveValue::Set(max_id), + }, + ) + .on_conflict( + OnConflict::columns([ + observed_channel_messages::Column::UserId, + observed_channel_messages::Column::ChannelId, + ]) + .update_columns([observed_channel_messages::Column::ChannelMessageId]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + } Ok(messages) }) @@ -130,11 +183,13 @@ impl Database { let mut is_participant = false; let mut participant_connection_ids = Vec::new(); + let mut participant_user_ids = Vec::new(); while let Some(row) = rows.next().await { let row = row?; if row.user_id == user_id { is_participant = true; } + participant_user_ids.push(row.user_id); participant_connection_ids.push(row.connection()); } drop(rows); @@ -167,11 +222,77 @@ impl Database { ConnectionId, } + // Observe this message for all participants + observed_channel_messages::Entity::insert_many(participant_user_ids.iter().map( + |pariticpant_id| observed_channel_messages::ActiveModel { + user_id: ActiveValue::Set(*pariticpant_id), + channel_id: ActiveValue::Set(channel_id), + channel_message_id: ActiveValue::Set(message.last_insert_id), + }, + )) + .on_conflict( + OnConflict::columns([ + observed_channel_messages::Column::ChannelId, + observed_channel_messages::Column::UserId, + ]) + .update_column(observed_channel_messages::Column::ChannelMessageId) + .to_owned(), + ) + .exec(&*tx) + .await?; + Ok((message.last_insert_id, participant_connection_ids)) }) .await } + #[cfg(test)] + pub async fn has_new_message_tx(&self, channel_id: ChannelId, user_id: UserId) -> Result { + self.transaction(|tx| async move { self.has_new_message(channel_id, user_id, &*tx).await }) + .await + } + + #[cfg(test)] + pub async fn dbg_print_messages(&self) -> Result<()> { + self.transaction(|tx| async move { + dbg!(observed_channel_messages::Entity::find() + .all(&*tx) + .await + .unwrap()); + dbg!(channel_message::Entity::find().all(&*tx).await.unwrap()); + + Ok(()) + }) + .await + } + + pub async fn has_new_message( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result { + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; + + let latest_message_id = channel_message::Entity::find() + .filter(Condition::all().add(channel_message::Column::ChannelId.eq(channel_id))) + .order_by(channel_message::Column::SentAt, sea_query::Order::Desc) + .limit(1 as u64) + .one(&*tx) + .await? + .map(|model| model.id); + + let last_message_read = observed_channel_messages::Entity::find() + .filter(observed_channel_messages::Column::ChannelId.eq(channel_id)) + .filter(observed_channel_messages::Column::UserId.eq(user_id)) + .one(&*tx) + .await? + .map(|model| model.channel_message_id); + + Ok(dbg!(last_message_read) != dbg!(latest_message_id)) + } + pub async fn remove_channel_message( &self, channel_id: ChannelId, diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index 4068606546..e19391da7d 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -13,6 +13,7 @@ pub mod feature_flag; pub mod follower; pub mod language_server; pub mod observed_buffer_edits; +pub mod observed_channel_messages; pub mod project; pub mod project_collaborator; pub mod room; diff --git a/crates/collab/src/db/tables/observed_channel_messages.rs b/crates/collab/src/db/tables/observed_channel_messages.rs new file mode 100644 index 0000000000..18259f8442 --- /dev/null +++ b/crates/collab/src/db/tables/observed_channel_messages.rs @@ -0,0 +1,41 @@ +use crate::db::{ChannelId, MessageId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "observed_channel_messages")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + pub channel_id: ChannelId, + pub channel_message_id: MessageId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index c40e53d355..2c745bb8ae 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -57,3 +57,142 @@ async fn test_channel_message_nonces(db: &Arc) { assert_eq!(msg1_id, msg3_id); assert_eq!(msg2_id, msg4_id); } + +test_both_dbs!( + test_channel_message_new_notification, + test_channel_message_new_notification_postgres, + test_channel_message_new_notification_sqlite +); + +async fn test_channel_message_new_notification(db: &Arc) { + let user_a = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_b = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel = db + .create_channel("channel", None, "room", user_a) + .await + .unwrap(); + + db.invite_channel_member(channel, user_b, user_a, false) + .await + .unwrap(); + + db.respond_to_channel_invite(channel, user_b, true) + .await + .unwrap(); + + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + // Zero case: no messages at all + assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + + let a_connection_id = rpc::ConnectionId { owner_id, id: 0 }; + db.join_channel_chat(channel, a_connection_id, user_a) + .await + .unwrap(); + + let _ = db + .create_channel_message(channel, user_a, "1", OffsetDateTime::now_utc(), 1) + .await + .unwrap(); + + let (second_message, _) = db + .create_channel_message(channel, user_a, "2", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + + let _ = db + .create_channel_message(channel, user_a, "3", OffsetDateTime::now_utc(), 3) + .await + .unwrap(); + + // Smoke test: can we detect a new message? + assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + + let b_connection_id = rpc::ConnectionId { owner_id, id: 1 }; + db.join_channel_chat(channel, b_connection_id, user_b) + .await + .unwrap(); + + // Joining the channel should _not_ update us to the latest message + assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + + // Reading the earlier messages should not change that we have new messages + let _ = db + .get_channel_messages(channel, user_b, 1, Some(second_message)) + .await + .unwrap(); + + assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + + // This constraint is currently inexpressible, creating a message implicitly broadcasts + // it to all participants + // + // Creating new messages when we haven't read the latest one should not change the flag + // let _ = db + // .create_channel_message(channel, user_a, "4", OffsetDateTime::now_utc(), 4) + // .await + // .unwrap(); + // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + + // But reading the latest message should clear the flag + let _ = db + .get_channel_messages(channel, user_b, 4, None) + .await + .unwrap(); + + assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + + // And future messages should not reset the flag + let _ = db + .create_channel_message(channel, user_a, "5", OffsetDateTime::now_utc(), 5) + .await + .unwrap(); + + assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + + let _ = db + .create_channel_message(channel, user_b, "6", OffsetDateTime::now_utc(), 6) + .await + .unwrap(); + + assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + + // And we should start seeing the flag again after we've left the channel + db.leave_channel_chat(channel, b_connection_id, user_b) + .await + .unwrap(); + + assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + + let _ = db + .create_channel_message(channel, user_a, "7", OffsetDateTime::now_utc(), 7) + .await + .unwrap(); + + assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); +} From 1d5b665f13886f1b7c56002d6f09110b9efa7483 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 1 Oct 2023 22:21:27 -0700 Subject: [PATCH 11/67] Implement channel changes for messages --- crates/channel/src/channel_store.rs | 28 ++++- .../src/channel_store/channel_index.rs | 13 +++ crates/collab/src/db.rs | 3 +- crates/collab/src/db/queries/channels.rs | 5 + crates/collab/src/db/queries/messages.rs | 40 +++---- crates/collab/src/db/tests/message_tests.rs | 2 +- crates/collab/src/rpc.rs | 38 ++++++- .../collab/src/tests/channel_buffer_tests.rs | 1 + .../collab/src/tests/channel_message_tests.rs | 105 +++++++++++++++++- crates/collab_ui/src/collab_panel.rs | 2 +- crates/gpui/src/app.rs | 2 +- crates/rpc/proto/zed.proto | 1 + 12 files changed, 212 insertions(+), 28 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 417f486b9e..f0f66f4839 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -44,6 +44,7 @@ pub struct Channel { pub id: ChannelId, pub name: String, pub has_note_changed: bool, + pub has_new_messages: bool, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] @@ -223,6 +224,13 @@ impl ChannelStore { .map(|channel| channel.has_note_changed) } + pub fn has_new_messages(&self, channel_id: ChannelId) -> Option { + self.channel_index + .by_id() + .get(&channel_id) + .map(|channel| channel.has_new_messages) + } + pub fn open_channel_chat( &mut self, channel_id: ChannelId, @@ -230,12 +238,20 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - self.open_channel_resource( + let open_channel_chat = self.open_channel_resource( channel_id, |this| &mut this.opened_chats, |channel, cx| ChannelChat::new(channel, user_store, client, cx), cx, - ) + ); + cx.spawn(|this, mut cx| async move { + let chat = open_channel_chat.await?; + this.update(&mut cx, |this, cx| { + this.channel_index.clear_message_changed(channel_id); + cx.notify(); + }); + Ok(chat) + }) } /// Asynchronously open a given resource associated with a channel. @@ -796,6 +812,7 @@ impl ChannelStore { id: channel.id, name: channel.name, has_note_changed: false, + has_new_messages: false, }), ), } @@ -805,7 +822,8 @@ impl ChannelStore { || !payload.delete_channels.is_empty() || !payload.insert_edge.is_empty() || !payload.delete_edge.is_empty() - || !payload.notes_changed.is_empty(); + || !payload.notes_changed.is_empty() + || !payload.new_messages.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { @@ -836,6 +854,10 @@ impl ChannelStore { index.note_changed(id_changed); } + for id_changed in payload.new_messages { + index.new_messages(id_changed); + } + for edge in payload.insert_edge { index.insert_edge(edge.channel_id, edge.parent_id); } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 42474f1d85..513e20e3a7 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -44,6 +44,12 @@ impl ChannelIndex { Arc::make_mut(channel).has_note_changed = false; } } + + pub fn clear_message_changed(&mut self, channel_id: ChannelId) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + Arc::make_mut(channel).has_new_messages = false; + } + } } impl Deref for ChannelIndex { @@ -88,6 +94,12 @@ impl<'a> ChannelPathsInsertGuard<'a> { } } + pub fn new_messages(&mut self, channel_id: ChannelId) { + if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { + Arc::make_mut(channel).has_new_messages = true; + } + } + pub fn insert(&mut self, channel_proto: proto::Channel) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { Arc::make_mut(existing_channel).name = channel_proto.name; @@ -98,6 +110,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { id: channel_proto.id, name: channel_proto.name, has_note_changed: false, + has_new_messages: false, }), ); self.insert_root(channel_proto.id); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f0896f8732..8f7f9cc975 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -436,8 +436,9 @@ pub struct Channel { pub struct ChannelsForUser { pub channels: ChannelGraph, pub channel_participants: HashMap>, - pub channels_with_changed_notes: HashSet, pub channels_with_admin_privileges: HashSet, + pub channels_with_changed_notes: HashSet, + pub channels_with_new_messages: HashSet, } #[derive(Debug)] diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 6274550c25..ea9f64fe5e 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -464,10 +464,14 @@ impl Database { } let mut channels_with_changed_notes = HashSet::default(); + let mut channels_with_new_messages = HashSet::default(); for channel in graph.channels.iter() { if self.has_note_changed(user_id, channel.id, tx).await? { channels_with_changed_notes.insert(channel.id); } + if self.has_new_message(channel.id, user_id, tx).await? { + channels_with_new_messages.insert(channel.id); + } } Ok(ChannelsForUser { @@ -475,6 +479,7 @@ impl Database { channel_participants, channels_with_admin_privileges, channels_with_changed_notes, + channels_with_new_messages, }) } diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 328737dd0a..8e3c92d916 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -97,7 +97,7 @@ impl Database { let mut messages = Vec::new(); while let Some(row) = rows.next().await { let row = row?; - dbg!(&max_id); + max_assign(&mut max_id, row.id); let nonce = row.nonce.as_u64_pair(); @@ -113,23 +113,18 @@ impl Database { }); } drop(rows); - dbg!(&max_id); if let Some(max_id) = max_id { - let has_older_message = dbg!( - observed_channel_messages::Entity::find() - .filter( - observed_channel_messages::Column::UserId - .eq(user_id) - .and(observed_channel_messages::Column::ChannelId.eq(channel_id)) - .and( - observed_channel_messages::Column::ChannelMessageId.lt(max_id) - ), - ) - .one(&*tx) - .await - )? - .is_some(); + let has_older_message = observed_channel_messages::Entity::find() + .filter( + observed_channel_messages::Column::UserId + .eq(user_id) + .and(observed_channel_messages::Column::ChannelId.eq(channel_id)) + .and(observed_channel_messages::Column::ChannelMessageId.lt(max_id)), + ) + .one(&*tx) + .await? + .is_some(); if has_older_message { observed_channel_messages::Entity::update( @@ -174,7 +169,7 @@ impl Database { body: &str, timestamp: OffsetDateTime, nonce: u128, - ) -> Result<(MessageId, Vec)> { + ) -> Result<(MessageId, Vec, Vec)> { self.transaction(|tx| async move { let mut rows = channel_chat_participant::Entity::find() .filter(channel_chat_participant::Column::ChannelId.eq(channel_id)) @@ -241,7 +236,14 @@ impl Database { .exec(&*tx) .await?; - Ok((message.last_insert_id, participant_connection_ids)) + let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + channel_members.retain(|member| !participant_user_ids.contains(member)); + + Ok(( + message.last_insert_id, + participant_connection_ids, + channel_members, + )) }) .await } @@ -290,7 +292,7 @@ impl Database { .await? .map(|model| model.channel_message_id); - Ok(dbg!(last_message_read) != dbg!(latest_message_id)) + Ok(last_message_read != latest_message_id) } pub async fn remove_channel_message( diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 2c745bb8ae..98b8cc6037 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -120,7 +120,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - let (second_message, _) = db + let (second_message, _, _) = db .create_channel_message(channel, user_a, "2", OffsetDateTime::now_utc(), 2) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b9dae999cd..371d0466c1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2568,6 +2568,16 @@ async fn respond_to_channel_invite( name: channel.name, }), ); + update.notes_changed = result + .channels_with_changed_notes + .iter() + .map(|id| id.to_proto()) + .collect(); + update.new_messages = result + .channels_with_new_messages + .iter() + .map(|id| id.to_proto()) + .collect(); update.insert_edge = result.channels.edges; update .channel_participants @@ -2818,7 +2828,7 @@ async fn send_channel_message( .ok_or_else(|| anyhow!("nonce can't be blank"))?; let channel_id = ChannelId::from_proto(request.channel_id); - let (message_id, connection_ids) = session + let (message_id, connection_ids, non_participants) = session .db() .await .create_channel_message( @@ -2848,6 +2858,26 @@ async fn send_channel_message( response.send(proto::SendChannelMessageResponse { message: Some(message), })?; + + dbg!(&non_participants); + let pool = &*session.connection_pool().await; + + broadcast( + None, + non_participants + .iter() + .flat_map(|user_id| pool.user_connection_ids(*user_id)), + |peer_id| { + session.peer.send( + peer_id.into(), + proto::UpdateChannels { + new_messages: vec![channel_id.to_proto()], + ..Default::default() + }, + ) + }, + ); + Ok(()) } @@ -3011,6 +3041,12 @@ fn build_initial_channels_update( .map(|channel_id| channel_id.to_proto()) .collect(); + update.new_messages = channels + .channels_with_new_messages + .iter() + .map(|channel_id| channel_id.to_proto()) + .collect(); + update.insert_edge = channels.channels.edges; for (channel_id, participants) in channels.channel_participants { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 7ca6f0db3f..68acffacf8 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -446,6 +446,7 @@ fn channel(id: u64, name: &'static str) -> Channel { id, name: name.to_string(), has_note_changed: false, + has_new_messages: false, } } diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 58494c538b..1b3a54dc42 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -1,6 +1,6 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::{ChannelChat, ChannelMessageId}; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext}; use std::sync::Arc; #[gpui::test] @@ -223,3 +223,106 @@ fn assert_messages(chat: &ModelHandle, messages: &[&str], cx: &mut messages ); } + +#[gpui::test] +async fn test_channel_message_changes( + 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; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + // Client A sends a message, client B should see that there is a new message. + let channel_chat_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); + + // Opening the chat should clear the changed flag. + let channel_chat_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + .await + .unwrap(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(!b_has_messages); + + // Sending a message while the chat is open should not change the flag. + channel_chat_a + .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(!b_has_messages); + + // Closing the chat should re-enable change tracking + + cx_b.update(|_| { + drop(channel_chat_b); + }); + + deterministic.run_until_parked(); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) + .await + .unwrap(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0fd265d0aa..53d5140a12 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1821,7 +1821,7 @@ impl CollabPanel { channel.name.clone(), theme .channel_name - .in_state(channel.has_note_changed) + .in_state(channel.has_new_messages) .text .clone(), ) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 6b3cd03e8f..edcc7ad6f6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1252,7 +1252,7 @@ impl AppContext { result }) } else { - panic!("circular model update"); + panic!("circular model update for {}", std::any::type_name::()); } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c53db447d3..d0c9d73fc4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -956,6 +956,7 @@ message UpdateChannels { repeated ChannelParticipants channel_participants = 7; repeated ChannelPermission channel_permissions = 8; repeated uint64 notes_changed = 9; + repeated uint64 new_messages = 10; } message ChannelEdge { From 64a55681e615d71ae5146d16b1d6f28d2237819c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 14:32:13 +0200 Subject: [PATCH 12/67] Summarize the contents of a file using the embedding query --- crates/assistant/src/assistant_panel.rs | 1 - crates/assistant/src/prompts.rs | 480 ++++++++++++---------- crates/language/src/buffer.rs | 12 +- crates/zed/src/languages/rust/summary.scm | 6 - 4 files changed, 264 insertions(+), 235 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 37d0d729fe..816047e325 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 ee58090d04..8699c77cd1 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 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'); - } +fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { + #[derive(Debug)] + struct Match { + collapse: Range, + keep: Vec>, } - Some(text) + 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 + } + }) { + 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 { + break; + } + } + + if offset >= mat.collapse.start { + // Skip collapsed nodes that have already been summarized. + offset = cmp::max(offset, mat.collapse.end); + continue; + } + + 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 { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|END|>"); + } + offset = selected_range.end; + flushed_selection = true; + } + + // If the selection intersects the collapsed node, we won't collapse it. + if selected_range.end >= mat.collapse.start { + continue; + } + } + + 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; + } + + 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 {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} ); - 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 + 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 - "}) + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} ); - 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 + 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 {} + + pub fn b(&self) -> usize {} + } + "} ); - 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 } - } + 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 {} + + pub fn b(&self) -> usize {} + } + <|START|>"} ); - let outline = outline_for_prompt( - &snapshot, - snapshot.anchor_before(Point::new(5, 6))..snapshot.anchor_before(Point::new(12, 0)), - cx, - ); + // 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 + } + self.a + nested() + } + + 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<|START| { + summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), + indoc! {" + <|START|>struct X { + a: usize, + b: usize, + } - fn new() -> Self { - let a = 1; - let b = 2; - Self { a, b } - } - |END|> - fn a - fn b - "}) - ); + impl X { - 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 - } - "}) + 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 38b2842c12..27b01543e1 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 7174eec3c3..0000000000 --- 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 13/67] :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 816047e325..4a4dc08790 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 8699c77cd1..0646534e01 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 14/67] 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 4a4dc08790..0d9f69011e 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 0646534e01..2451369a18 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 d70014cfd065dcc65823ab8c0c465edf5dff5d08 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 15:36:10 +0200 Subject: [PATCH 15/67] 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 0d9f69011e..b69c12a2a3 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 16/67] Sort matches before processing them --- crates/assistant/src/prompts.rs | 65 ++++++++++----------- crates/zed/src/languages/rust/embedding.scm | 3 + 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 2451369a18..bf041dff52 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()) - } 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; - } - } - matches.advance(); - return Some(Match { collapse, keep }); + 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 { - matches.advance(); + 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; + } + } + 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 e4218382a9..c4ed7d2097 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 17/67] 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 e8579a0006..7b4813ffa9 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 18/67] 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 26a531cc31..130a7a64f0 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 d7acae3995..696923e505 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 879b375cd4..d85aca164a 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 84a9b3b6b6..57d6f7b4f6 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 21fa7d4ee6..5e362403f0 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 38773fb8cc..c90b175320 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 19/67] 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 4ddfbc5a34..5eae700404 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 27d784b23e81a8f763587ebb3cb6fa09f4327e5f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 16:27:12 -0600 Subject: [PATCH 20/67] 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 696923e505..657d71afd4 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 5e362403f0..28ccee768b 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 c90b175320..6e62a9bf16 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 21/67] 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 05abda5af3..46005244c1 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 657d71afd4..6d374b7920 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 6e62a9bf16..f7bb409229 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 84c4db13fb52543c1ef22465d885cf33e19f24c6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Oct 2023 15:57:59 -0700 Subject: [PATCH 22/67] Avoid spurious notifies in chat channel select Co-authored-by: Mikayla --- crates/gpui/src/views/select.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index bad65ccfc8..b1ea201fdf 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -53,8 +53,10 @@ impl Select { } pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext) { - self.item_count = count; - cx.notify(); + if count != self.item_count { + self.item_count = count; + cx.notify(); + } } fn toggle(&mut self, cx: &mut ViewContext) { @@ -63,9 +65,11 @@ impl Select { } pub fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { - self.selected_item_ix = ix; - self.is_open = false; - cx.notify(); + if ix != self.selected_item_ix || self.is_open { + self.selected_item_ix = ix; + self.is_open = false; + cx.notify(); + } } pub fn selected_index(&self) -> usize { From d9d997b218712b6efdf673841edbe68a0d03297e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Oct 2023 15:58:34 -0700 Subject: [PATCH 23/67] Avoid N+1 query for channels with notes changes Also, start work on new timing for recording observed notes edits. Co-authored-by: Mikayla --- .../20221109000000_test_schema.sql | 1 + .../20230925210437_add_channel_changes.sql | 1 + crates/collab/src/db.rs | 4 +- crates/collab/src/db/queries/buffers.rs | 269 ++++++++++++------ crates/collab/src/db/queries/channels.rs | 12 +- .../src/db/tables/observed_buffer_edits.rs | 1 + crates/collab/src/db/tests/buffer_tests.rs | 190 +++++++++++++ 7 files changed, 381 insertions(+), 97 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 277a78d2d6..2d963ff15f 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -296,6 +296,7 @@ CREATE TABLE "observed_buffer_edits" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, PRIMARY KEY (user_id, buffer_id) ); diff --git a/crates/collab/migrations/20230925210437_add_channel_changes.sql b/crates/collab/migrations/20230925210437_add_channel_changes.sql index 7787975c1c..250a9ac731 100644 --- a/crates/collab/migrations/20230925210437_add_channel_changes.sql +++ b/crates/collab/migrations/20230925210437_add_channel_changes.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS "observed_buffer_edits" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, PRIMARY KEY (user_id, buffer_id) ); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8f7f9cc975..b0223bbf27 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -119,7 +119,7 @@ impl Database { Ok(new_migrations) } - async fn transaction(&self, f: F) -> Result + pub async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, Fut: Send + Future>, @@ -321,7 +321,7 @@ fn is_serialization_error(error: &Error) -> bool { } } -struct TransactionHandle(Arc>); +pub struct TransactionHandle(Arc>); impl Deref for TransactionHandle { type Target = DatabaseTransaction; diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 1e8dd30c6b..b22bfc80cf 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -79,12 +79,13 @@ impl Database { self.get_buffer_state(&buffer, &tx).await?; // Save the last observed operation - if let Some(max_operation) = max_operation { + if let Some(op) = max_operation { observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { user_id: ActiveValue::Set(user_id), buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(max_operation.0), - lamport_timestamp: ActiveValue::Set(max_operation.1), + epoch: ActiveValue::Set(op.epoch), + lamport_timestamp: ActiveValue::Set(op.lamport_timestamp), + replica_id: ActiveValue::Set(op.replica_id), }) .on_conflict( OnConflict::columns([ @@ -99,37 +100,6 @@ impl Database { ) .exec(&*tx) .await?; - } else { - let buffer_max = buffer_operation::Entity::find() - .filter(buffer_operation::Column::BufferId.eq(buffer.id)) - .filter(buffer_operation::Column::Epoch.eq(buffer.epoch.saturating_sub(1))) - .order_by(buffer_operation::Column::Epoch, Desc) - .order_by(buffer_operation::Column::LamportTimestamp, Desc) - .one(&*tx) - .await? - .map(|model| (model.epoch, model.lamport_timestamp)); - - if let Some(buffer_max) = buffer_max { - observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { - user_id: ActiveValue::Set(user_id), - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(buffer_max.0), - lamport_timestamp: ActiveValue::Set(buffer_max.1), - }) - .on_conflict( - OnConflict::columns([ - observed_buffer_edits::Column::UserId, - observed_buffer_edits::Column::BufferId, - ]) - .update_columns([ - observed_buffer_edits::Column::Epoch, - observed_buffer_edits::Column::LamportTimestamp, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } } Ok(proto::JoinChannelBufferResponse { @@ -487,13 +457,8 @@ impl Database { if !operations.is_empty() { // get current channel participants and save the max operation above - self.save_max_operation_for_collaborators( - operations.as_slice(), - channel_id, - buffer.id, - &*tx, - ) - .await?; + self.save_max_operation(user, buffer.id, buffer.epoch, operations.as_slice(), &*tx) + .await?; channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; let collaborators = self @@ -539,54 +504,55 @@ impl Database { .await } - async fn save_max_operation_for_collaborators( + async fn save_max_operation( &self, - operations: &[buffer_operation::ActiveModel], - channel_id: ChannelId, + user_id: UserId, buffer_id: BufferId, + epoch: i32, + operations: &[buffer_operation::ActiveModel], tx: &DatabaseTransaction, ) -> Result<()> { + use observed_buffer_edits::Column; + let max_operation = operations .iter() - .map(|storage_model| { - ( - storage_model.epoch.clone(), - storage_model.lamport_timestamp.clone(), - ) - }) - .max_by( - |(epoch_a, lamport_timestamp_a), (epoch_b, lamport_timestamp_b)| { - epoch_a.as_ref().cmp(epoch_b.as_ref()).then( - lamport_timestamp_a - .as_ref() - .cmp(lamport_timestamp_b.as_ref()), - ) - }, - ) + .max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref())) .unwrap(); - let users = self - .get_channel_buffer_collaborators_internal(channel_id, tx) - .await?; - - observed_buffer_edits::Entity::insert_many(users.iter().map(|id| { - observed_buffer_edits::ActiveModel { - user_id: ActiveValue::Set(*id), - buffer_id: ActiveValue::Set(buffer_id), - epoch: max_operation.0.clone(), - lamport_timestamp: ActiveValue::Set(*max_operation.1.as_ref()), - } - })) + observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { + user_id: ActiveValue::Set(user_id), + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(epoch), + replica_id: max_operation.replica_id.clone(), + lamport_timestamp: max_operation.lamport_timestamp.clone(), + }) .on_conflict( - OnConflict::columns([ - observed_buffer_edits::Column::UserId, - observed_buffer_edits::Column::BufferId, - ]) - .update_columns([ - observed_buffer_edits::Column::Epoch, - observed_buffer_edits::Column::LamportTimestamp, - ]) - .to_owned(), + OnConflict::columns([Column::UserId, Column::BufferId]) + .update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId]) + .target_cond_where( + Condition::any() + .add(Column::Epoch.lt(*max_operation.epoch.as_ref())) + .add( + Condition::all() + .add(Column::Epoch.eq(*max_operation.epoch.as_ref())) + .add( + Condition::any() + .add( + Column::LamportTimestamp + .lt(*max_operation.lamport_timestamp.as_ref()), + ) + .add( + Column::LamportTimestamp + .eq(*max_operation.lamport_timestamp.as_ref()) + .and( + Column::ReplicaId + .lt(*max_operation.replica_id.as_ref()), + ), + ), + ), + ), + ) + .to_owned(), ) .exec(tx) .await?; @@ -611,7 +577,7 @@ impl Database { .ok_or_else(|| anyhow!("missing buffer snapshot"))?) } - async fn get_channel_buffer( + pub async fn get_channel_buffer( &self, channel_id: ChannelId, tx: &DatabaseTransaction, @@ -630,7 +596,11 @@ impl Database { &self, buffer: &buffer::Model, tx: &DatabaseTransaction, - ) -> Result<(String, Vec, Option<(i32, i32)>)> { + ) -> Result<( + String, + Vec, + Option, + )> { let id = buffer.id; let (base_text, version) = if buffer.epoch > 0 { let snapshot = buffer_snapshot::Entity::find() @@ -655,24 +625,28 @@ impl Database { .eq(id) .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), ) + .order_by_asc(buffer_operation::Column::LamportTimestamp) + .order_by_asc(buffer_operation::Column::ReplicaId) .stream(&*tx) .await?; - let mut operations = Vec::new(); - let mut max_epoch: Option = None; - let mut max_timestamp: Option = None; + let mut operations = Vec::new(); + let mut last_row = None; while let Some(row) = rows.next().await { let row = row?; - - max_assign(&mut max_epoch, row.epoch); - max_assign(&mut max_timestamp, row.lamport_timestamp); - + last_row = Some(buffer_operation::Model { + buffer_id: row.buffer_id, + epoch: row.epoch, + lamport_timestamp: row.lamport_timestamp, + replica_id: row.lamport_timestamp, + value: Default::default(), + }); operations.push(proto::Operation { variant: Some(operation_from_storage(row, version)?), - }) + }); } - Ok((base_text, operations, max_epoch.zip(max_timestamp))) + Ok((base_text, operations, last_row)) } async fn snapshot_channel_buffer( @@ -725,6 +699,119 @@ impl Database { .await } + pub async fn channels_with_changed_notes( + &self, + user_id: UserId, + channel_ids: impl IntoIterator, + tx: &DatabaseTransaction, + ) -> Result> { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryIds { + ChannelId, + Id, + } + + let mut channel_ids_by_buffer_id = HashMap::default(); + let mut rows = buffer::Entity::find() + .filter(buffer::Column::ChannelId.is_in(channel_ids)) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + channel_ids_by_buffer_id.insert(row.id, row.channel_id); + } + drop(rows); + + let mut observed_edits_by_buffer_id = HashMap::default(); + let mut rows = observed_buffer_edits::Entity::find() + .filter(observed_buffer_edits::Column::UserId.eq(user_id)) + .filter( + observed_buffer_edits::Column::BufferId + .is_in(channel_ids_by_buffer_id.keys().copied()), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + observed_edits_by_buffer_id.insert(row.buffer_id, row); + } + drop(rows); + + let last_operations = self + .get_last_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx) + .await?; + + let mut channels_with_new_changes = HashSet::default(); + for last_operation in last_operations { + if let Some(observed_edit) = observed_edits_by_buffer_id.get(&last_operation.buffer_id) + { + if observed_edit.epoch == last_operation.epoch + && observed_edit.lamport_timestamp == last_operation.lamport_timestamp + && observed_edit.replica_id == last_operation.replica_id + { + continue; + } + } + + if let Some(channel_id) = channel_ids_by_buffer_id.get(&last_operation.buffer_id) { + channels_with_new_changes.insert(*channel_id); + } + } + + Ok(channels_with_new_changes) + } + + pub async fn get_last_operations_for_buffers( + &self, + channel_ids: impl IntoIterator, + tx: &DatabaseTransaction, + ) -> Result> { + let mut values = String::new(); + for id in channel_ids { + if !values.is_empty() { + values.push_str(", "); + } + write!(&mut values, "({})", id).unwrap(); + } + + if values.is_empty() { + return Ok(Vec::default()); + } + + let sql = format!( + r#" + SELECT + * + FROM ( + SELECT + buffer_id, + epoch, + lamport_timestamp, + replica_id, + value, + row_number() OVER ( + PARTITION BY buffer_id + ORDER BY + epoch DESC, + lamport_timestamp DESC, + replica_id DESC + ) as row_number + FROM buffer_operations + WHERE + buffer_id in ({values}) + ) AS operations + WHERE + row_number = 1 + "#, + ); + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + let operations = buffer_operation::Model::find_by_statement(stmt) + .all(&*tx) + .await?; + Ok(operations) + } + pub async fn has_note_changed( &self, user_id: UserId, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ea9f64fe5e..6d976b310e 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -463,12 +463,16 @@ impl Database { } } - let mut channels_with_changed_notes = HashSet::default(); + let channels_with_changed_notes = self + .channels_with_changed_notes( + user_id, + graph.channels.iter().map(|channel| channel.id), + &*tx, + ) + .await?; + let mut channels_with_new_messages = HashSet::default(); for channel in graph.channels.iter() { - if self.has_note_changed(user_id, channel.id, tx).await? { - channels_with_changed_notes.insert(channel.id); - } if self.has_new_message(channel.id, user_id, tx).await? { channels_with_new_messages.insert(channel.id); } diff --git a/crates/collab/src/db/tables/observed_buffer_edits.rs b/crates/collab/src/db/tables/observed_buffer_edits.rs index db027f78b2..e8e7aafaa2 100644 --- a/crates/collab/src/db/tables/observed_buffer_edits.rs +++ b/crates/collab/src/db/tables/observed_buffer_edits.rs @@ -9,6 +9,7 @@ pub struct Model { pub buffer_id: BufferId, pub epoch: i32, pub lamport_timestamp: i32, + pub replica_id: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 115a20ffa6..5a5fe6a812 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -272,3 +272,193 @@ async fn test_channel_buffers_diffs(db: &Database) { assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap()); } + +test_both_dbs!( + test_channel_buffers_last_operations, + test_channel_buffers_last_operations_postgres, + test_channel_buffers_last_operations_sqlite +); + +async fn test_channel_buffers_last_operations(db: &Database) { + let user_id = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let owner_id = db.create_server("production").await.unwrap().0 as u32; + let connection_id = ConnectionId { + owner_id, + id: user_id.0 as u32, + }; + + let mut buffers = Vec::new(); + let mut text_buffers = Vec::new(); + for i in 0..3 { + let channel = db + .create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id) + .await + .unwrap(); + + db.join_channel_buffer(channel, user_id, connection_id) + .await + .unwrap(); + + buffers.push( + db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await }) + .await + .unwrap(), + ); + + text_buffers.push(Buffer::new(0, 0, "".to_string())); + } + + let operations = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.get_last_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx) + .await + } + }) + .await + .unwrap(); + + assert!(operations.is_empty()); + + update_buffer( + buffers[0].channel_id, + user_id, + db, + vec![ + text_buffers[0].edit([(0..0, "a")]), + text_buffers[0].edit([(0..0, "b")]), + text_buffers[0].edit([(0..0, "c")]), + ], + ) + .await; + + update_buffer( + buffers[1].channel_id, + user_id, + db, + vec![ + text_buffers[1].edit([(0..0, "d")]), + text_buffers[1].edit([(1..1, "e")]), + text_buffers[1].edit([(2..2, "f")]), + ], + ) + .await; + + // cause buffer 1's epoch to increment. + db.leave_channel_buffer(buffers[1].channel_id, connection_id) + .await + .unwrap(); + db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id) + .await + .unwrap(); + text_buffers[1] = Buffer::new(1, 0, "def".to_string()); + update_buffer( + buffers[1].channel_id, + user_id, + db, + vec![ + text_buffers[1].edit([(0..0, "g")]), + text_buffers[1].edit([(0..0, "h")]), + ], + ) + .await; + + update_buffer( + buffers[2].channel_id, + user_id, + db, + vec![text_buffers[2].edit([(0..0, "i")])], + ) + .await; + + let operations = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.get_last_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx) + .await + } + }) + .await + .unwrap(); + assert_operations( + &operations, + &[ + (buffers[1].id, 1, &text_buffers[1]), + (buffers[2].id, 0, &text_buffers[2]), + ], + ); + + let operations = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.get_last_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx) + .await + } + }) + .await + .unwrap(); + assert_operations( + &operations, + &[ + (buffers[0].id, 0, &text_buffers[0]), + (buffers[1].id, 1, &text_buffers[1]), + ], + ); + + async fn update_buffer( + channel_id: ChannelId, + user_id: UserId, + db: &Database, + operations: Vec, + ) { + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + db.update_channel_buffer(channel_id, user_id, &operations) + .await + .unwrap(); + } + + fn assert_operations( + operations: &[buffer_operation::Model], + expected: &[(BufferId, i32, &text::Buffer)], + ) { + let actual = operations + .iter() + .map(|op| buffer_operation::Model { + buffer_id: op.buffer_id, + epoch: op.epoch, + lamport_timestamp: op.lamport_timestamp, + replica_id: op.replica_id, + value: vec![], + }) + .collect::>(); + let expected = expected + .iter() + .map(|(buffer_id, epoch, buffer)| buffer_operation::Model { + buffer_id: *buffer_id, + epoch: *epoch, + lamport_timestamp: buffer.lamport_clock.value as i32 - 1, + replica_id: buffer.replica_id() as i32, + value: vec![], + }) + .collect::>(); + assert_eq!(actual, expected, "unexpected operations") + } +} From 0db4b294524c718fce6c1b04bffe0bfe1484375d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Oct 2023 16:22:28 -0700 Subject: [PATCH 24/67] Avoid N+1 query for channels with new messages Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 6 +- crates/collab/src/db/queries/channels.rs | 16 +-- crates/collab/src/db/queries/messages.rs | 113 ++++++++++++-------- crates/collab/src/db/tests/buffer_tests.rs | 2 + crates/collab/src/db/tests/message_tests.rs | 20 ++-- 5 files changed, 88 insertions(+), 69 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index b22bfc80cf..6a74ae4d44 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -529,7 +529,7 @@ impl Database { .on_conflict( OnConflict::columns([Column::UserId, Column::BufferId]) .update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId]) - .target_cond_where( + .action_cond_where( Condition::any() .add(Column::Epoch.lt(*max_operation.epoch.as_ref())) .add( @@ -702,7 +702,7 @@ impl Database { pub async fn channels_with_changed_notes( &self, user_id: UserId, - channel_ids: impl IntoIterator, + channel_ids: &[ChannelId], tx: &DatabaseTransaction, ) -> Result> { #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] @@ -713,7 +713,7 @@ impl Database { let mut channel_ids_by_buffer_id = HashMap::default(); let mut rows = buffer::Entity::find() - .filter(buffer::Column::ChannelId.is_in(channel_ids)) + .filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied())) .stream(&*tx) .await?; while let Some(row) = rows.next().await { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 6d976b310e..8292e9dbcb 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -463,20 +463,14 @@ impl Database { } } + let channel_ids = graph.channels.iter().map(|c| c.id).collect::>(); let channels_with_changed_notes = self - .channels_with_changed_notes( - user_id, - graph.channels.iter().map(|channel| channel.id), - &*tx, - ) + .channels_with_changed_notes(user_id, &channel_ids, &*tx) .await?; - let mut channels_with_new_messages = HashSet::default(); - for channel in graph.channels.iter() { - if self.has_new_message(channel.id, user_id, tx).await? { - channels_with_new_messages.insert(channel.id); - } - } + let channels_with_new_messages = self + .channels_with_new_messages(user_id, &channel_ids, &*tx) + .await?; Ok(ChannelsForUser { channels: graph, diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 8e3c92d916..484509f685 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -217,14 +217,12 @@ impl Database { ConnectionId, } - // Observe this message for all participants - observed_channel_messages::Entity::insert_many(participant_user_ids.iter().map( - |pariticpant_id| observed_channel_messages::ActiveModel { - user_id: ActiveValue::Set(*pariticpant_id), - channel_id: ActiveValue::Set(channel_id), - channel_message_id: ActiveValue::Set(message.last_insert_id), - }, - )) + // Observe this message for the sender + observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel { + user_id: ActiveValue::Set(user_id), + channel_id: ActiveValue::Set(channel_id), + channel_message_id: ActiveValue::Set(message.last_insert_id), + }) .on_conflict( OnConflict::columns([ observed_channel_messages::Column::ChannelId, @@ -248,51 +246,74 @@ impl Database { .await } - #[cfg(test)] - pub async fn has_new_message_tx(&self, channel_id: ChannelId, user_id: UserId) -> Result { - self.transaction(|tx| async move { self.has_new_message(channel_id, user_id, &*tx).await }) - .await - } - - #[cfg(test)] - pub async fn dbg_print_messages(&self) -> Result<()> { - self.transaction(|tx| async move { - dbg!(observed_channel_messages::Entity::find() - .all(&*tx) - .await - .unwrap()); - dbg!(channel_message::Entity::find().all(&*tx).await.unwrap()); - - Ok(()) - }) - .await - } - - pub async fn has_new_message( + pub async fn channels_with_new_messages( &self, - channel_id: ChannelId, user_id: UserId, + channel_ids: &[ChannelId], tx: &DatabaseTransaction, - ) -> Result { - self.check_user_is_channel_member(channel_id, user_id, &*tx) + ) -> Result> { + let mut observed_messages_by_channel_id = HashMap::default(); + let mut rows = observed_channel_messages::Entity::find() + .filter(observed_channel_messages::Column::UserId.eq(user_id)) + .filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied())) + .stream(&*tx) .await?; - let latest_message_id = channel_message::Entity::find() - .filter(Condition::all().add(channel_message::Column::ChannelId.eq(channel_id))) - .order_by(channel_message::Column::SentAt, sea_query::Order::Desc) - .limit(1 as u64) - .one(&*tx) - .await? - .map(|model| model.id); + while let Some(row) = rows.next().await { + let row = row?; + observed_messages_by_channel_id.insert(row.channel_id, row); + } + drop(rows); + let mut values = String::new(); + for id in channel_ids { + if !values.is_empty() { + values.push_str(", "); + } + write!(&mut values, "({})", id).unwrap(); + } - let last_message_read = observed_channel_messages::Entity::find() - .filter(observed_channel_messages::Column::ChannelId.eq(channel_id)) - .filter(observed_channel_messages::Column::UserId.eq(user_id)) - .one(&*tx) - .await? - .map(|model| model.channel_message_id); + if values.is_empty() { + return Ok(Default::default()); + } - Ok(last_message_read != latest_message_id) + let sql = format!( + r#" + SELECT + * + FROM ( + SELECT + *, + row_number() OVER ( + PARTITION BY channel_id + ORDER BY id DESC + ) as row_number + FROM channel_messages + WHERE + channel_id in ({values}) + ) AS messages + WHERE + row_number = 1 + "#, + ); + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + let last_messages = channel_message::Model::find_by_statement(stmt) + .all(&*tx) + .await?; + + let mut channels_with_new_changes = HashSet::default(); + for last_message in last_messages { + if let Some(observed_message) = + observed_messages_by_channel_id.get(&last_message.channel_id) + { + if observed_message.channel_message_id == last_message.id { + continue; + } + } + channels_with_new_changes.insert(last_message.channel_id); + } + + Ok(channels_with_new_changes) } pub async fn remove_channel_message( diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 5a5fe6a812..d8edef963a 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -171,6 +171,8 @@ test_both_dbs!( ); async fn test_channel_buffers_diffs(db: &Database) { + panic!("Rewriting the way this works"); + let a_id = db .create_user( "user_a@example.com", diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index 98b8cc6037..e212c36466 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -65,6 +65,8 @@ test_both_dbs!( ); async fn test_channel_message_new_notification(db: &Arc) { + panic!("Rewriting the way this works"); + let user_a = db .create_user( "user_a@example.com", @@ -108,7 +110,7 @@ async fn test_channel_message_new_notification(db: &Arc) { let owner_id = db.create_server("test").await.unwrap().0 as u32; // Zero case: no messages at all - assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); let a_connection_id = rpc::ConnectionId { owner_id, id: 0 }; db.join_channel_chat(channel, a_connection_id, user_a) @@ -131,7 +133,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap(); // Smoke test: can we detect a new message? - assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); let b_connection_id = rpc::ConnectionId { owner_id, id: 1 }; db.join_channel_chat(channel, b_connection_id, user_b) @@ -139,7 +141,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap(); // Joining the channel should _not_ update us to the latest message - assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); // Reading the earlier messages should not change that we have new messages let _ = db @@ -147,7 +149,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); // This constraint is currently inexpressible, creating a message implicitly broadcasts // it to all participants @@ -165,7 +167,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); // And future messages should not reset the flag let _ = db @@ -173,26 +175,26 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); let _ = db .create_channel_message(channel, user_b, "6", OffsetDateTime::now_utc(), 6) .await .unwrap(); - assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); // And we should start seeing the flag again after we've left the channel db.leave_channel_chat(channel, b_connection_id, user_b) .await .unwrap(); - assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); let _ = db .create_channel_message(channel, user_a, "7", OffsetDateTime::now_utc(), 7) .await .unwrap(); - assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); } From 892350fa2d6894290edd68ab2520da1d9e21636f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 2 Oct 2023 19:35:31 -0400 Subject: [PATCH 25/67] 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 76de671620..3b714455ef 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 f09d44e8da..801435ee2a 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 e3038e5bcc..9e371ec8bd 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 1596e6d850..8d51a3d1fe 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 07b6ad790c..651d32ba91 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 7991cabde2..d6f3be2b46 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 32b4b4d24d29ee988764651d817d9ee254abdde7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 2 Oct 2023 17:10:03 -0700 Subject: [PATCH 26/67] Add message and operation ACK messages to protos --- crates/rpc/proto/zed.proto | 16 +++++++++++++++- crates/rpc/src/proto.rs | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0c9d73fc4..0f289edcf8 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -155,6 +155,7 @@ message Envelope { UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; RejoinChannelBuffers rejoin_channel_buffers = 129; RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; + AckBufferOperation ack_buffer_operation = 143; JoinChannelChat join_channel_chat = 131; JoinChannelChatResponse join_channel_chat_response = 132; @@ -165,10 +166,11 @@ message Envelope { GetChannelMessages get_channel_messages = 137; GetChannelMessagesResponse get_channel_messages_response = 138; RemoveChannelMessage remove_channel_message = 139; + AckChannelMessage ack_channel_message = 144; LinkChannel link_channel = 140; UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; + MoveChannel move_channel = 142; // current max: 144 } } @@ -1062,6 +1064,11 @@ message RemoveChannelMessage { uint64 message_id = 2; } +message AckChannelMessage { + uint64 channel_id = 1; + uint64 message_id = 2; +} + message SendChannelMessageResponse { ChannelMessage message = 1; } @@ -1117,6 +1124,13 @@ message RejoinChannelBuffersResponse { repeated RejoinedChannelBuffer buffers = 1; } +message AckBufferOperation { + uint64 buffer_id = 1; + uint64 epoch = 2; + uint64 lamport_timestamp = 3; + uint64 replica_id = 4; +} + message JoinChannelBufferResponse { uint64 buffer_id = 1; uint32 replica_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 6d0a0f85d1..f0d7937f6f 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -271,6 +271,8 @@ messages!( (LeaveChannelBuffer, Background), (UpdateChannelBuffer, Foreground), (UpdateChannelBufferCollaborators, Foreground), + (AckBufferOperation, Background), + (AckChannelMessage, Background), ); request_messages!( @@ -406,7 +408,7 @@ entity_messages!( ChannelMessageSent, UpdateChannelBuffer, RemoveChannelMessage, - UpdateChannelBufferCollaborators + UpdateChannelBufferCollaborators, ); const KIB: usize = 1024; From d7867cd1e283080788117e61b5246478ccd8469d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 19:38:45 -0600 Subject: [PATCH 27/67] 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 16a9ec563b..22ab573974 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 28/67] 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 ca0d06beb6..d86ed1be37 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 130a7a64f0..f24a8e9a9c 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 22ab573974..eeae37bbe5 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 29/67] 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 f2cae8a557..182010ca83 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 c4ed7d2097..286b1d1357 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 30/67] 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 8d51a3d1fe..38a4115ddd 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 31/67] 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 eeae37bbe5..08c5dd70ad 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 32/67] 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 08c5dd70ad..a7080ad051 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 32c413875891b249f11fb005f10d59dc8bc4276a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 3 Oct 2023 11:39:59 -0700 Subject: [PATCH 33/67] Added db message and edit operation observation Co-authored-by: Max --- crates/collab/src/db/queries/buffers.rs | 146 +++------ crates/collab/src/db/queries/messages.rs | 61 +++- crates/collab/src/db/tests/buffer_tests.rs | 314 ++++++++++---------- crates/collab/src/db/tests/message_tests.rs | 151 ++++++---- 4 files changed, 345 insertions(+), 327 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 6a74ae4d44..78ccd9e54a 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,6 +1,5 @@ use super::*; use prost::Message; -use sea_query::Order::Desc; use text::{EditOperation, UndoOperation}; pub struct LeftChannelBuffer { @@ -456,9 +455,21 @@ impl Database { let mut channel_members; if !operations.is_empty() { + let max_operation = operations + .iter() + .max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref())) + .unwrap(); + // get current channel participants and save the max operation above - self.save_max_operation(user, buffer.id, buffer.epoch, operations.as_slice(), &*tx) - .await?; + self.save_max_operation( + user, + buffer.id, + buffer.epoch, + *max_operation.replica_id.as_ref(), + *max_operation.lamport_timestamp.as_ref(), + &*tx, + ) + .await?; channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; let collaborators = self @@ -509,52 +520,38 @@ impl Database { user_id: UserId, buffer_id: BufferId, epoch: i32, - operations: &[buffer_operation::ActiveModel], + replica_id: i32, + lamport_timestamp: i32, tx: &DatabaseTransaction, ) -> Result<()> { use observed_buffer_edits::Column; - let max_operation = operations - .iter() - .max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref())) - .unwrap(); - observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel { user_id: ActiveValue::Set(user_id), buffer_id: ActiveValue::Set(buffer_id), epoch: ActiveValue::Set(epoch), - replica_id: max_operation.replica_id.clone(), - lamport_timestamp: max_operation.lamport_timestamp.clone(), + replica_id: ActiveValue::Set(replica_id), + lamport_timestamp: ActiveValue::Set(lamport_timestamp), }) .on_conflict( OnConflict::columns([Column::UserId, Column::BufferId]) .update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId]) .action_cond_where( - Condition::any() - .add(Column::Epoch.lt(*max_operation.epoch.as_ref())) - .add( - Condition::all() - .add(Column::Epoch.eq(*max_operation.epoch.as_ref())) + Condition::any().add(Column::Epoch.lt(epoch)).add( + Condition::all().add(Column::Epoch.eq(epoch)).add( + Condition::any() + .add(Column::LamportTimestamp.lt(lamport_timestamp)) .add( - Condition::any() - .add( - Column::LamportTimestamp - .lt(*max_operation.lamport_timestamp.as_ref()), - ) - .add( - Column::LamportTimestamp - .eq(*max_operation.lamport_timestamp.as_ref()) - .and( - Column::ReplicaId - .lt(*max_operation.replica_id.as_ref()), - ), - ), + Column::LamportTimestamp + .eq(lamport_timestamp) + .and(Column::ReplicaId.lt(replica_id)), ), ), + ), ) .to_owned(), ) - .exec(tx) + .exec_without_returning(tx) .await?; Ok(()) @@ -689,14 +686,30 @@ impl Database { Ok(()) } - #[cfg(test)] - pub async fn test_has_note_changed( + pub async fn observe_buffer_version( &self, + buffer_id: BufferId, user_id: UserId, - channel_id: ChannelId, - ) -> Result { - self.transaction(|tx| async move { self.has_note_changed(user_id, channel_id, &*tx).await }) - .await + epoch: i32, + version: &[proto::VectorClockEntry], + ) -> Result<()> { + self.transaction(|tx| async move { + // For now, combine concurrent operations. + let Some(component) = version.iter().max_by_key(|version| version.timestamp) else { + return Ok(()); + }; + self.save_max_operation( + user_id, + buffer_id, + epoch, + component.replica_id as i32, + component.timestamp as i32, + &*tx, + ) + .await?; + Ok(()) + }) + .await } pub async fn channels_with_changed_notes( @@ -811,67 +824,6 @@ impl Database { .await?; Ok(operations) } - - pub async fn has_note_changed( - &self, - user_id: UserId, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result { - let Some(buffer_id) = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(buffer::Entity) - .one(&*tx) - .await? - .map(|buffer| buffer.id) else { - return Ok(false); - }; - - let user_max = observed_buffer_edits::Entity::find() - .filter(observed_buffer_edits::Column::UserId.eq(user_id)) - .filter(observed_buffer_edits::Column::BufferId.eq(buffer_id)) - .one(&*tx) - .await? - .map(|model| (model.epoch, model.lamport_timestamp)); - - let channel_buffer = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(buffer::Entity) - .one(&*tx) - .await?; - - let Some(channel_buffer) = channel_buffer else { - return Ok(false); - }; - - let mut channel_max = buffer_operation::Entity::find() - .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) - .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch)) - .order_by(buffer_operation::Column::Epoch, Desc) - .order_by(buffer_operation::Column::LamportTimestamp, Desc) - .one(&*tx) - .await? - .map(|model| (model.epoch, model.lamport_timestamp)); - - // If there are no edits in this epoch - if channel_max.is_none() { - // check if this user observed the last edit of the previous epoch - channel_max = buffer_operation::Entity::find() - .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id)) - .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch.saturating_sub(1))) - .order_by(buffer_operation::Column::Epoch, Desc) - .order_by(buffer_operation::Column::LamportTimestamp, Desc) - .one(&*tx) - .await? - .map(|model| (model.epoch, model.lamport_timestamp)); - } - - Ok(user_max != channel_max) - } } fn operation_to_storage( diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 484509f685..893c1726da 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -218,20 +218,12 @@ impl Database { } // Observe this message for the sender - observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel { - user_id: ActiveValue::Set(user_id), - channel_id: ActiveValue::Set(channel_id), - channel_message_id: ActiveValue::Set(message.last_insert_id), - }) - .on_conflict( - OnConflict::columns([ - observed_channel_messages::Column::ChannelId, - observed_channel_messages::Column::UserId, - ]) - .update_column(observed_channel_messages::Column::ChannelMessageId) - .to_owned(), + self.observe_channel_message_internal( + channel_id, + user_id, + message.last_insert_id, + &*tx, ) - .exec(&*tx) .await?; let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; @@ -246,12 +238,53 @@ impl Database { .await } + pub async fn observe_channel_message( + &self, + channel_id: ChannelId, + user_id: UserId, + message_id: MessageId, + ) -> Result<()> { + self.transaction(|tx| async move { + self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx) + .await?; + Ok(()) + }) + .await + } + + async fn observe_channel_message_internal( + &self, + channel_id: ChannelId, + user_id: UserId, + message_id: MessageId, + tx: &DatabaseTransaction, + ) -> Result<()> { + observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel { + user_id: ActiveValue::Set(user_id), + channel_id: ActiveValue::Set(channel_id), + channel_message_id: ActiveValue::Set(message_id), + }) + .on_conflict( + OnConflict::columns([ + observed_channel_messages::Column::ChannelId, + observed_channel_messages::Column::UserId, + ]) + .update_column(observed_channel_messages::Column::ChannelMessageId) + .action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id)) + .to_owned(), + ) + // TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug + .exec_without_returning(&*tx) + .await?; + Ok(()) + } + pub async fn channels_with_new_messages( &self, user_id: UserId, channel_ids: &[ChannelId], tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result> { let mut observed_messages_by_channel_id = HashMap::default(); let mut rows = observed_channel_messages::Entity::find() .filter(observed_channel_messages::Column::UserId.eq(user_id)) diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index d8edef963a..407cc22108 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -1,6 +1,6 @@ use super::*; use crate::test_both_dbs; -use language::proto; +use language::proto::{self, serialize_version}; use text::Buffer; test_both_dbs!( @@ -164,117 +164,6 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(buffer_response_b.operations, &[]); } -test_both_dbs!( - test_channel_buffers_diffs, - test_channel_buffers_diffs_postgres, - test_channel_buffers_diffs_sqlite -); - -async fn test_channel_buffers_diffs(db: &Database) { - panic!("Rewriting the way this works"); - - let a_id = db - .create_user( - "user_a@example.com", - false, - NewUserParams { - github_login: "user_a".into(), - github_user_id: 101, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let b_id = db - .create_user( - "user_b@example.com", - false, - NewUserParams { - github_login: "user_b".into(), - github_user_id: 102, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let owner_id = db.create_server("production").await.unwrap().0 as u32; - - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); - - db.invite_channel_member(zed_id, b_id, a_id, false) - .await - .unwrap(); - - db.respond_to_channel_invite(zed_id, b_id, true) - .await - .unwrap(); - - let connection_id_a = ConnectionId { - owner_id, - id: a_id.0 as u32, - }; - let connection_id_b = ConnectionId { - owner_id, - id: b_id.0 as u32, - }; - - // Zero test: A should not register as changed on an unitialized channel buffer - assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); - - let _ = db - .join_channel_buffer(zed_id, a_id, connection_id_a) - .await - .unwrap(); - - // Zero test: A should register as changed on an empty channel buffer - assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); - - let mut buffer_a = Buffer::new(0, 0, "".to_string()); - let mut operations = Vec::new(); - operations.push(buffer_a.edit([(0..0, "hello world")])); - assert_eq!(buffer_a.text(), "hello world"); - - let operations = operations - .into_iter() - .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) - .collect::>(); - - db.update_channel_buffer(zed_id, a_id, &operations) - .await - .unwrap(); - - // Smoke test: Does B register as changed, A as unchanged? - assert!(db.test_has_note_changed(b_id, zed_id).await.unwrap()); - - assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); - - db.leave_channel_buffer(zed_id, connection_id_a) - .await - .unwrap(); - - // Snapshotting from leaving the channel buffer should not have a diff - assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); - - let _ = db - .join_channel_buffer(zed_id, b_id, connection_id_b) - .await - .unwrap(); - - // B has opened the channel buffer, so we shouldn't have any diff - assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap()); - - db.leave_channel_buffer(zed_id, connection_id_b) - .await - .unwrap(); - - // Since B just opened and closed the buffer without editing, neither should have a diff - assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap()); - assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap()); -} - test_both_dbs!( test_channel_buffers_last_operations, test_channel_buffers_last_operations_postgres, @@ -295,6 +184,19 @@ async fn test_channel_buffers_last_operations(db: &Database) { .await .unwrap() .user_id; + let observer_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; let owner_id = db.create_server("production").await.unwrap().0 as u32; let connection_id = ConnectionId { owner_id, @@ -309,6 +211,13 @@ async fn test_channel_buffers_last_operations(db: &Database) { .await .unwrap(); + db.invite_channel_member(channel, observer_id, user_id, false) + .await + .unwrap(); + db.respond_to_channel_invite(channel, observer_id, true) + .await + .unwrap(); + db.join_channel_buffer(channel, user_id, connection_id) .await .unwrap(); @@ -422,45 +331,146 @@ async fn test_channel_buffers_last_operations(db: &Database) { ], ); - async fn update_buffer( - channel_id: ChannelId, - user_id: UserId, - db: &Database, - operations: Vec, - ) { - let operations = operations - .into_iter() - .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) - .collect::>(); - db.update_channel_buffer(channel_id, user_id, &operations) - .await - .unwrap(); - } + let changed_channels = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.channels_with_changed_notes( + observer_id, + &[ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ], + &*tx, + ) + .await + } + }) + .await + .unwrap(); + assert_eq!( + changed_channels, + [ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ] + .into_iter() + .collect::>() + ); - fn assert_operations( - operations: &[buffer_operation::Model], - expected: &[(BufferId, i32, &text::Buffer)], - ) { - let actual = operations - .iter() - .map(|op| buffer_operation::Model { - buffer_id: op.buffer_id, - epoch: op.epoch, - lamport_timestamp: op.lamport_timestamp, - replica_id: op.replica_id, - value: vec![], - }) - .collect::>(); - let expected = expected - .iter() - .map(|(buffer_id, epoch, buffer)| buffer_operation::Model { - buffer_id: *buffer_id, - epoch: *epoch, - lamport_timestamp: buffer.lamport_clock.value as i32 - 1, - replica_id: buffer.replica_id() as i32, - value: vec![], - }) - .collect::>(); - assert_eq!(actual, expected, "unexpected operations") - } + db.observe_buffer_version( + buffers[1].id, + observer_id, + 1, + &serialize_version(&text_buffers[1].version()), + ) + .await + .unwrap(); + + let changed_channels = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.channels_with_changed_notes( + observer_id, + &[ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ], + &*tx, + ) + .await + } + }) + .await + .unwrap(); + assert_eq!( + changed_channels, + [buffers[0].channel_id, buffers[2].channel_id,] + .into_iter() + .collect::>() + ); + + // Observe an earlier version of the buffer. + db.observe_buffer_version( + buffers[1].id, + observer_id, + 1, + &[rpc::proto::VectorClockEntry { + replica_id: 0, + timestamp: 0, + }], + ) + .await + .unwrap(); + + let changed_channels = db + .transaction(|tx| { + let buffers = &buffers; + async move { + db.channels_with_changed_notes( + observer_id, + &[ + buffers[0].channel_id, + buffers[1].channel_id, + buffers[2].channel_id, + ], + &*tx, + ) + .await + } + }) + .await + .unwrap(); + assert_eq!( + changed_channels, + [buffers[0].channel_id, buffers[2].channel_id,] + .into_iter() + .collect::>() + ); +} + +async fn update_buffer( + channel_id: ChannelId, + user_id: UserId, + db: &Database, + operations: Vec, +) { + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + db.update_channel_buffer(channel_id, user_id, &operations) + .await + .unwrap(); +} + +fn assert_operations( + operations: &[buffer_operation::Model], + expected: &[(BufferId, i32, &text::Buffer)], +) { + let actual = operations + .iter() + .map(|op| buffer_operation::Model { + buffer_id: op.buffer_id, + epoch: op.epoch, + lamport_timestamp: op.lamport_timestamp, + replica_id: op.replica_id, + value: vec![], + }) + .collect::>(); + let expected = expected + .iter() + .map(|(buffer_id, epoch, buffer)| buffer_operation::Model { + buffer_id: *buffer_id, + epoch: *epoch, + lamport_timestamp: buffer.lamport_clock.value as i32 - 1, + replica_id: buffer.replica_id() as i32, + value: vec![], + }) + .collect::>(); + assert_eq!(actual, expected, "unexpected operations") } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index e212c36466..f3d385e4a0 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -65,9 +65,7 @@ test_both_dbs!( ); async fn test_channel_message_new_notification(db: &Arc) { - panic!("Rewriting the way this works"); - - let user_a = db + let user = db .create_user( "user_a@example.com", false, @@ -80,7 +78,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap() .user_id; - let user_b = db + let observer = db .create_user( "user_b@example.com", false, @@ -94,107 +92,132 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap() .user_id; - let channel = db - .create_channel("channel", None, "room", user_a) + let channel_1 = db + .create_channel("channel", None, "room", user) .await .unwrap(); - db.invite_channel_member(channel, user_b, user_a, false) + let channel_2 = db + .create_channel("channel-2", None, "room", user) .await .unwrap(); - db.respond_to_channel_invite(channel, user_b, true) + db.invite_channel_member(channel_1, observer, user, false) + .await + .unwrap(); + + db.respond_to_channel_invite(channel_1, observer, true) + .await + .unwrap(); + + db.invite_channel_member(channel_2, observer, user, false) + .await + .unwrap(); + + db.respond_to_channel_invite(channel_2, observer, true) .await .unwrap(); let owner_id = db.create_server("test").await.unwrap().0 as u32; + let user_connection_id = rpc::ConnectionId { owner_id, id: 0 }; - // Zero case: no messages at all - // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); - - let a_connection_id = rpc::ConnectionId { owner_id, id: 0 }; - db.join_channel_chat(channel, a_connection_id, user_a) + db.join_channel_chat(channel_1, user_connection_id, user) .await .unwrap(); let _ = db - .create_channel_message(channel, user_a, "1", OffsetDateTime::now_utc(), 1) + .create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1) .await .unwrap(); let (second_message, _, _) = db - .create_channel_message(channel, user_a, "2", OffsetDateTime::now_utc(), 2) + .create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2) + .await + .unwrap(); + + let (third_message, _, _) = db + .create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3) + .await + .unwrap(); + + db.join_channel_chat(channel_2, user_connection_id, user) .await .unwrap(); let _ = db - .create_channel_message(channel, user_a, "3", OffsetDateTime::now_utc(), 3) + .create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4) .await .unwrap(); - // Smoke test: can we detect a new message? - // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); - - let b_connection_id = rpc::ConnectionId { owner_id, id: 1 }; - db.join_channel_chat(channel, b_connection_id, user_b) + // Check that observer has new messages + let channels_with_new_messages = db + .transaction(|tx| async move { + db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) .await .unwrap(); - // Joining the channel should _not_ update us to the latest message - // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + assert_eq!( + channels_with_new_messages, + [channel_1, channel_2] + .into_iter() + .collect::>() + ); - // Reading the earlier messages should not change that we have new messages - let _ = db - .get_channel_messages(channel, user_b, 1, Some(second_message)) + // Observe the second message + db.observe_channel_message(channel_1, observer, second_message) .await .unwrap(); - // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + // Make sure the observer still has a new message + let channels_with_new_messages = db + .transaction(|tx| async move { + db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) + .await + .unwrap(); + assert_eq!( + channels_with_new_messages, + [channel_1, channel_2] + .into_iter() + .collect::>() + ); - // This constraint is currently inexpressible, creating a message implicitly broadcasts - // it to all participants - // - // Creating new messages when we haven't read the latest one should not change the flag - // let _ = db - // .create_channel_message(channel, user_a, "4", OffsetDateTime::now_utc(), 4) - // .await - // .unwrap(); - // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); - - // But reading the latest message should clear the flag - let _ = db - .get_channel_messages(channel, user_b, 4, None) + // Observe the third message, + db.observe_channel_message(channel_1, observer, third_message) .await .unwrap(); - // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); + // Make sure the observer does not have a new method + let channels_with_new_messages = db + .transaction(|tx| async move { + db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) + .await + .unwrap(); + assert_eq!( + channels_with_new_messages, + [channel_2].into_iter().collect::>() + ); - // And future messages should not reset the flag - let _ = db - .create_channel_message(channel, user_a, "5", OffsetDateTime::now_utc(), 5) + // Observe the second message again, should not regress our observed state + db.observe_channel_message(channel_1, observer, second_message) .await .unwrap(); - // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); - - let _ = db - .create_channel_message(channel, user_b, "6", OffsetDateTime::now_utc(), 6) + // Make sure the observer does not have a new method + let channels_with_new_messages = db + .transaction(|tx| async move { + db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + .await + }) .await .unwrap(); - - // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); - - // And we should start seeing the flag again after we've left the channel - db.leave_channel_chat(channel, b_connection_id, user_b) - .await - .unwrap(); - - // assert!(!db.has_new_message_tx(channel, user_b).await.unwrap()); - - let _ = db - .create_channel_message(channel, user_a, "7", OffsetDateTime::now_utc(), 7) - .await - .unwrap(); - - // assert!(db.has_new_message_tx(channel, user_b).await.unwrap()); + assert_eq!( + channels_with_new_messages, + [channel_2].into_iter().collect::>() + ); } From d696b394c45daa5212ece6e4914ba684aa925441 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 12:54:39 -0600 Subject: [PATCH 34/67] 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 a7080ad051..ce18a7b92d 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 6007c8705c38371374c57d94929fca1bb48ae275 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 3 Oct 2023 12:16:53 -0700 Subject: [PATCH 35/67] Upgrade SeaORM to latest version, also upgrade sqlite bindings, rustqlite, and remove SeaQuery co-authored-by: Max --- Cargo.lock | 654 +++++++++++++++-------- Cargo.toml | 1 + crates/ai/Cargo.toml | 2 +- crates/collab/Cargo.toml | 10 +- crates/collab/src/db.rs | 10 +- crates/collab/src/db/ids.rs | 54 +- crates/collab/src/db/queries/channels.rs | 5 +- crates/collab/src/db/queries/contacts.rs | 4 +- crates/collab/src/db/queries/users.rs | 2 +- crates/collab/src/db/tests.rs | 4 +- crates/semantic_index/Cargo.toml | 2 +- crates/sqlez/Cargo.toml | 2 +- 12 files changed, 460 insertions(+), 290 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76de671620..ffdc736830 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "activity_indicator" version = "0.1.0" @@ -73,6 +67,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if 1.0.0", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -99,7 +94,7 @@ dependencies = [ "lazy_static", "log", "matrixmultiply", - "ordered-float", + "ordered-float 2.10.0", "parking_lot 0.11.2", "parse_duration", "postage", @@ -314,7 +309,7 @@ dependencies = [ "language", "log", "menu", - "ordered-float", + "ordered-float 2.10.0", "parking_lot 0.11.2", "project", "rand 0.8.5", @@ -587,7 +582,7 @@ dependencies = [ "futures-core", "futures-io", "rustls 0.19.1", - "webpki 0.21.4", + "webpki", "webpki-roots 0.21.1", ] @@ -618,9 +613,9 @@ dependencies = [ [[package]] name = "atoi" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] @@ -778,19 +773,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "bae" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "base64" version = "0.13.1" @@ -809,6 +791,17 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint 0.4.4", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -1517,7 +1510,6 @@ dependencies = [ "rpc", "scrypt", "sea-orm", - "sea-query", "serde", "serde_derive", "serde_json", @@ -1658,6 +1650,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "context_menu" version = "0.1.0" @@ -2150,6 +2148,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.8" @@ -2159,6 +2168,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -2244,6 +2264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -2375,7 +2396,7 @@ dependencies = [ "lazy_static", "log", "lsp", - "ordered-float", + "ordered-float 2.10.0", "parking_lot 0.11.2", "postage", "project", @@ -2407,6 +2428,9 @@ name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -2509,6 +2533,17 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if 1.0.0", + "home", + "windows-sys", +] + [[package]] name = "euclid" version = "0.22.9" @@ -2680,13 +2715,12 @@ checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "pin-project", "spin 0.9.8", ] @@ -2914,13 +2948,13 @@ dependencies = [ [[package]] name = "futures-intrusive" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.11.2", + "parking_lot 0.12.1", ] [[package]] @@ -3174,7 +3208,7 @@ dependencies = [ "metal", "num_cpus", "objc", - "ordered-float", + "ordered-float 2.10.0", "parking", "parking_lot 0.11.2", "pathfinder_color", @@ -3306,15 +3340,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "hashlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" -dependencies = [ - "hashbrown 0.11.2", -] - [[package]] name = "hashlink" version = "0.8.4" @@ -3636,6 +3661,17 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "inherent" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "install_cli" version = "0.1.0" @@ -4087,9 +4123,9 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" -version = "0.24.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "pkg-config", @@ -4769,6 +4805,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -5027,6 +5080,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +dependencies = [ + "num-traits", +] + [[package]] name = "os_str_bytes" version = "6.5.1" @@ -5035,25 +5097,26 @@ checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "ouroboros" -version = "0.15.6" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" dependencies = [ "aliasable", "ouroboros_macro", + "static_assertions", ] [[package]] name = "ouroboros_macro" -version = "0.15.6" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" dependencies = [ - "Inflector", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] @@ -5064,7 +5127,7 @@ dependencies = [ "fuzzy", "gpui", "language", - "ordered-float", + "ordered-float 2.10.0", "picker", "postage", "settings", @@ -5230,6 +5293,15 @@ dependencies = [ "regex", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -5307,6 +5379,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -5598,7 +5691,7 @@ dependencies = [ "gpui", "language", "lsp", - "ordered-float", + "ordered-float 2.10.0", "picker", "postage", "project", @@ -5949,7 +6042,7 @@ dependencies = [ "fuzzy", "gpui", "language", - "ordered-float", + "ordered-float 2.10.0", "picker", "postage", "settings", @@ -6262,7 +6355,7 @@ dependencies = [ "prost 0.8.0", "prost-build", "rand 0.8.5", - "rsa", + "rsa 0.4.0", "serde", "serde_derive", "smol", @@ -6275,14 +6368,14 @@ dependencies = [ [[package]] name = "rsa" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0aeddcca1082112a6eeb43bf25fd7820b066aaf6eaef776e19d0a1febe38fe" +checksum = "68ef841a26fc5d040ced0417c6c6a64ee851f42489df11cdf0218e545b6f8d28" dependencies = [ "byteorder", "digest 0.9.0", "lazy_static", - "num-bigint-dig", + "num-bigint-dig 0.7.1", "num-integer", "num-iter", "num-traits", @@ -6294,17 +6387,38 @@ dependencies = [ ] [[package]] -name = "rusqlite" -version = "0.27.0" +name = "rsa" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ - "bitflags 1.3.2", + "byteorder", + "const-oid", + "digest 0.10.7", + "num-bigint-dig 0.8.4", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.7.0", + "hashlink", "libsqlite3-sys", - "memchr", "smallvec", ] @@ -6433,19 +6547,18 @@ dependencies = [ "log", "ring", "sct 0.6.1", - "webpki 0.21.4", + "webpki", ] [[package]] name = "rustls" -version = "0.20.9" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ - "log", "ring", + "rustls-webpki", "sct 0.7.0", - "webpki 0.22.1", ] [[package]] @@ -6457,6 +6570,16 @@ dependencies = [ "base64 0.21.4", ] +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -6597,26 +6720,40 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sea-bae" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "sea-orm" -version = "0.10.5" -source = "git+https://github.com/zed-industries/sea-orm?rev=18f4c691085712ad014a51792af75a9044bacee6#18f4c691085712ad014a51792af75a9044bacee6" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5b2d70c255bc5cbe1d49f69c3c8eadae0fbbaeb18ee978edbf2f75775cb94d" dependencies = [ "async-stream", "async-trait", + "bigdecimal", "chrono", "futures 0.3.28", - "futures-util", "log", "ouroboros", "rust_decimal", "sea-orm-macros", "sea-query", "sea-query-binder", - "sea-strum", "serde", "serde_json", "sqlx", + "strum", "thiserror", "time", "tracing", @@ -6626,25 +6763,30 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "0.10.5" -source = "git+https://github.com/zed-industries/sea-orm?rev=18f4c691085712ad014a51792af75a9044bacee6#18f4c691085712ad014a51792af75a9044bacee6" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c8d455fad40194fb9774fdc4810c0f2700ff0dc0e93bd5ce9d641cc3f5dd75" dependencies = [ - "bae", - "heck 0.3.3", + "heck 0.4.1", "proc-macro2", "quote", - "syn 1.0.109", + "sea-bae", + "syn 2.0.37", + "unicode-ident", ] [[package]] name = "sea-query" -version = "0.27.2" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0fc4d8e44e1d51c739a68d336252a18bc59553778075d5e32649be6ec92ed" +checksum = "fb3e6bba153bb198646c8762c48414942a38db27d142e44735a133cabddcc820" dependencies = [ + "bigdecimal", "chrono", + "derivative", + "inherent", + "ordered-float 3.9.1", "rust_decimal", - "sea-query-derive", "serde_json", "time", "uuid 1.4.1", @@ -6652,10 +6794,11 @@ dependencies = [ [[package]] name = "sea-query-binder" -version = "0.2.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2585b89c985cfacfe0ec9fc9e7bb055b776c1a2581c4e3c6185af2b8bf8865" +checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" dependencies = [ + "bigdecimal", "chrono", "rust_decimal", "sea-query", @@ -6665,41 +6808,6 @@ dependencies = [ "uuid 1.4.1", ] -[[package]] -name = "sea-query-derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cdc022b4f606353fe5dc85b09713a04e433323b70163e81513b141c6ae6eb5" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn 1.0.109", - "thiserror", -] - -[[package]] -name = "sea-strum" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b" -dependencies = [ - "sea-strum_macros", -] - -[[package]] -name = "sea-strum_macros" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - [[package]] name = "seahash" version = "4.1.0" @@ -6779,7 +6887,7 @@ dependencies = [ "log", "ndarray", "node_runtime", - "ordered-float", + "ordered-float 2.10.0", "parking_lot 0.11.2", "picker", "postage", @@ -7077,6 +7185,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -7238,6 +7356,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "spsc-buffer" version = "0.1.1" @@ -7284,104 +7412,219 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.6.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" dependencies = [ "sqlx-core", "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] name = "sqlx-core" -version = "0.6.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", "atoi", - "base64 0.13.1", - "bitflags 1.3.2", + "bigdecimal", "byteorder", "bytes 1.5.0", "chrono", "crc", "crossbeam-queue", - "dirs 4.0.0", "dotenvy", "either", "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "rustls 0.21.7", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.10.7", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid 1.4.1", + "webpki-roots 0.24.0", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.7", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +dependencies = [ + "atoi", + "base64 0.21.4", + "bigdecimal", + "bitflags 2.4.0", + "byteorder", + "bytes 1.5.0", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa 0.9.2", + "rust_decimal", + "serde", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid 1.4.1", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64 0.21.4", + "bigdecimal", + "bitflags 2.4.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint 0.4.4", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha1", + "sha2 0.10.7", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid 1.4.1", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "chrono", "flume", "futures-channel", "futures-core", "futures-executor", "futures-intrusive", "futures-util", - "hashlink 0.8.4", - "hex", - "hkdf", - "hmac 0.12.1", - "indexmap 1.9.3", - "itoa", - "libc", "libsqlite3-sys", "log", - "md-5", - "memchr", - "num-bigint 0.4.4", - "once_cell", - "paste", "percent-encoding", - "rand 0.8.5", - "rust_decimal", - "rustls 0.20.9", - "rustls-pemfile", "serde", - "serde_json", - "sha1", - "sha2 0.10.7", - "smallvec", - "sqlformat", - "sqlx-rt", - "stringprep", - "thiserror", + "sqlx-core", "time", - "tokio-stream", + "tracing", "url", "uuid 1.4.1", - "webpki-roots 0.22.6", - "whoami", -] - -[[package]] -name = "sqlx-macros" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" -dependencies = [ - "dotenvy", - "either", - "heck 0.4.1", - "once_cell", - "proc-macro2", - "quote", - "serde_json", - "sha2 0.10.7", - "sqlx-core", - "sqlx-rt", - "syn 1.0.109", - "url", -] - -[[package]] -name = "sqlx-rt" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" -dependencies = [ - "once_cell", - "tokio", - "tokio-rustls", ] [[package]] @@ -7710,7 +7953,7 @@ dependencies = [ "lazy_static", "libc", "mio-extras", - "ordered-float", + "ordered-float 2.10.0", "procinfo", "rand 0.8.5", "schemars", @@ -7742,7 +7985,7 @@ dependencies = [ "lazy_static", "libc", "mio-extras", - "ordered-float", + "ordered-float 2.10.0", "procinfo", "project", "rand 0.8.5", @@ -8035,17 +8278,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki 0.22.1", -] - [[package]] name = "tokio-stream" version = "0.1.14" @@ -9363,32 +9595,22 @@ dependencies = [ "untrusted", ] -[[package]] -name = "webpki" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki 0.21.4", + "webpki", ] [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" dependencies = [ - "webpki 0.22.1", + "rustls-webpki", ] [[package]] @@ -9438,10 +9660,6 @@ name = "whoami" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] [[package]] name = "wiggle" @@ -9907,7 +10125,7 @@ dependencies = [ "recent_projects", "regex", "rpc", - "rsa", + "rsa 0.4.0", "rust-embed", "schemars", "search", @@ -9976,9 +10194,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index f09d44e8da..3732225312 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } rust-embed = { version = "8.0", features = ["include-exclude"] } +rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index a2c70ce8c6..542d7f422f 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -27,7 +27,7 @@ log.workspace = true parse_duration = "2.1.1" tiktoken-rs = "0.5.0" matrixmultiply = "0.3.7" -rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } +rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } bincode = "1.3.3" [dev-dependencies] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6cd29b6e54..6af23223c2 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -42,14 +42,12 @@ rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" smallvec.workspace = true -# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released. -sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] } -sea-query = "0.27" +sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } serde.workspace = true serde_derive.workspace = true serde_json.workspace = true sha-1 = "0.9" -sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } time.workspace = true tokio = { version = "1", features = ["full"] } tokio-tungstenite = "0.17" @@ -87,9 +85,9 @@ env_logger.workspace = true indoc.workspace = true util = { path = "../util" } lazy_static.workspace = true -sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] } +sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] } serde_json.workspace = true -sqlx = { version = "0.6", features = ["sqlite"] } +sqlx = { version = "0.7", features = ["sqlite"] } unindent.workspace = true [features] diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b0223bbf27..13bb3c06e8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -19,11 +19,12 @@ use rpc::{ ConnectionId, }; use sea_orm::{ - entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, - DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, - QueryOrder, QuerySelect, Statement, TransactionTrait, + entity::prelude::*, + sea_query::{Alias, Expr, OnConflict, Query}, + ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, + FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement, + TransactionTrait, }; -use sea_query::{Alias, Expr, OnConflict, Query}; use serde::{Deserialize, Serialize}; use sqlx::{ migrate::{Migrate, Migration, MigrationSource}, @@ -62,6 +63,7 @@ pub struct Database { // separate files in the `queries` folder. impl Database { pub async fn new(options: ConnectOptions, executor: Executor) -> Result { + sqlx::any::install_default_drivers(); Ok(Self { options: options.clone(), pool: sea_orm::Database::connect(options).await?, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 865a39fd71..23bb9e53bf 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -1,6 +1,5 @@ use crate::Result; -use sea_orm::DbErr; -use sea_query::{Value, ValueTypeErr}; +use sea_orm::{entity::prelude::*, DbErr}; use serde::{Deserialize, Serialize}; macro_rules! id_type { @@ -17,6 +16,7 @@ macro_rules! id_type { Hash, Serialize, Deserialize, + DeriveValueType, )] #[serde(transparent)] pub struct $name(pub i32); @@ -42,40 +42,6 @@ macro_rules! id_type { } } - impl From<$name> for sea_query::Value { - fn from(value: $name) -> Self { - sea_query::Value::Int(Some(value.0)) - } - } - - impl sea_orm::TryGetable for $name { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> Result { - Ok(Self(i32::try_get(res, pre, col)?)) - } - } - - impl sea_query::ValueType for $name { - fn try_from(v: Value) -> Result { - Ok(Self(value_to_integer(v)?)) - } - - fn type_name() -> String { - stringify!($name).into() - } - - fn array_type() -> sea_query::ArrayType { - sea_query::ArrayType::Int - } - - fn column_type() -> sea_query::ColumnType { - sea_query::ColumnType::Integer(None) - } - } - impl sea_orm::TryFromU64 for $name { fn try_from_u64(n: u64) -> Result { Ok(Self(n.try_into().map_err(|_| { @@ -88,7 +54,7 @@ macro_rules! id_type { } } - impl sea_query::Nullable for $name { + impl sea_orm::sea_query::Nullable for $name { fn null() -> Value { Value::Int(None) } @@ -96,20 +62,6 @@ macro_rules! id_type { }; } -fn value_to_integer(v: Value) -> Result { - match v { - Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), - _ => Err(ValueTypeErr), - } -} - id_type!(BufferId); id_type!(AccessTokenId); id_type!(ChannelChatParticipantId); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 8292e9dbcb..207cca7657 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,8 +1,7 @@ +use super::*; use rpc::proto::ChannelEdge; use smallvec::SmallVec; -use super::*; - type ChannelDescendants = HashMap>; impl Database { @@ -659,7 +658,7 @@ impl Database { ) -> Result> { let paths = channel_path::Entity::find() .filter(channel_path::Column::ChannelId.eq(channel_id)) - .order_by(channel_path::Column::IdPath, sea_query::Order::Desc) + .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) .all(tx) .await?; let mut channel_ids = Vec::new(); diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index a18958f035..2171f1a6bf 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -18,12 +18,12 @@ impl Database { let user_b_participant = Alias::new("user_b_participant"); let mut db_contacts = contact::Entity::find() .column_as( - Expr::tbl(user_a_participant.clone(), room_participant::Column::Id) + Expr::col((user_a_participant.clone(), room_participant::Column::Id)) .is_not_null(), "user_a_busy", ) .column_as( - Expr::tbl(user_b_participant.clone(), room_participant::Column::Id) + Expr::col((user_b_participant.clone(), room_participant::Column::Id)) .is_not_null(), "user_b_busy", ) diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index db968ba895..27e64e2598 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -184,7 +184,7 @@ impl Database { Ok(user::Entity::find() .from_raw_sql(Statement::from_sql_and_values( self.pool.get_database_backend(), - query.into(), + query, vec![like_string.into(), name_query.into(), limit.into()], )) .all(&*tx) diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index cf12be9b8d..75584ff90b 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -39,7 +39,7 @@ impl TestDb { db.pool .execute(sea_orm::Statement::from_string( db.pool.get_database_backend(), - sql.into(), + sql, )) .await .unwrap(); @@ -134,7 +134,7 @@ impl Drop for TestDb { db.pool .execute(sea_orm::Statement::from_string( db.pool.get_database_backend(), - query.into(), + query, )) .await .log_err(); diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index efda311633..34850f7035 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -26,7 +26,7 @@ postage.workspace = true futures.workspace = true ordered-float.workspace = true smol.workspace = true -rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } +rusqlite.workspace = true log.workspace = true tree-sitter.workspace = true lazy_static.workspace = true diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 01d17d4812..6811ee054c 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] anyhow.workspace = true indoc.workspace = true -libsqlite3-sys = { version = "0.24", features = ["bundled"] } +libsqlite3-sys = { version = "0.26", features = ["bundled"] } smol.workspace = true thread_local = "1.1.4" lazy_static.workspace = true From 044fb9e2f5ac99afdd2cd6b10f7d5adea3ef258c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 13:41:24 -0600 Subject: [PATCH 36/67] 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 ce18a7b92d..ab6261c568 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); } From af09861f5ccd59956e66d21452d23629fbf9c075 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2023 17:39:24 -0700 Subject: [PATCH 37/67] Specify uuid crate in the root Cargo.toml Co-authored-by: Mikayla --- Cargo.toml | 1 + crates/assistant/Cargo.toml | 2 +- crates/channel/Cargo.toml | 3 ++- crates/client/Cargo.toml | 2 +- crates/collab/Cargo.toml | 1 + crates/gpui/Cargo.toml | 2 +- crates/sqlez/Cargo.toml | 2 +- crates/workspace/Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 +- 9 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3732225312..a634b7f67b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ tree-sitter = "0.20" unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" git2 = { version = "0.15", default-features = false} +uuid = { version = "1.1.2", features = ["v4"] } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-c = "0.20.1" diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 5d141b32d5..f1daf47bab 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -21,8 +21,8 @@ search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } -uuid = { version = "1.1.2", features = ["v4"] } workspace = { path = "../workspace" } +uuid.workspace = true anyhow.workspace = true chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 16a1d418d5..6bd177bed5 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -23,6 +23,7 @@ language = { path = "../language" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +clock = { path = "../clock" } anyhow.workspace = true futures.workspace = true @@ -38,7 +39,7 @@ smol.workspace = true thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true url = "2.2" serde.workspace = true serde_derive.workspace = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e3038e5bcc..d322917013 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -37,7 +37,7 @@ smol.workspace = true thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true url = "2.2" serde.workspace = true serde_derive.workspace = true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 6af23223c2..ecc4b57b12 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -57,6 +57,7 @@ toml.workspace = true tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } +uuid.workspace = true [dev-dependencies] audio = { path = "../audio" } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 95b7ccb559..cdd5a0a5c8 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -53,7 +53,7 @@ thiserror.workspace = true time.workspace = true tiny-skia = "0.5" usvg = { version = "0.14", features = [] } -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true waker-fn = "1.1.0" [build-dependencies] diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 6811ee054c..1d1d052e93 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -13,4 +13,4 @@ thread_local = "1.1.4" lazy_static.workspace = true parking_lot.workspace = true futures.workspace = true -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index e2dae07b8c..41c86e538d 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -51,7 +51,7 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true smallvec.workspace = true -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fde598a735..bd6a85e3aa 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -138,7 +138,7 @@ tree-sitter-nu.workspace = true url = "2.2" urlencoding = "2.1.2" -uuid = { version = "1.1.2", features = ["v4"] } +uuid.workspace = true [dev-dependencies] call = { path = "../call", features = ["test-support"] } From 61e0289014732bef59e306c5f0e027c47c1d967a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2023 17:40:10 -0700 Subject: [PATCH 38/67] Acknowledge channel notes and chat changes when views are active Co-authored-by: Mikayla --- Cargo.lock | 2 + crates/channel/src/channel_buffer.rs | 57 +++++++++--- crates/channel/src/channel_chat.rs | 27 +++++- crates/channel/src/channel_store.rs | 93 ++++++++++++------- .../src/channel_store/channel_index.rs | 62 ++++++++++--- crates/client/src/client.rs | 28 +++--- crates/collab/src/db.rs | 4 +- crates/collab/src/db/queries/buffers.rs | 75 +++++++++------ crates/collab/src/db/queries/channels.rs | 12 +-- crates/collab/src/db/queries/messages.rs | 13 ++- crates/collab/src/db/tests/buffer_tests.rs | 79 +++++++++++----- crates/collab/src/db/tests/message_tests.rs | 65 ++++++++----- crates/collab/src/rpc.rs | 58 ++++++------ .../collab/src/tests/channel_buffer_tests.rs | 4 +- crates/collab/src/tests/test_server.rs | 4 +- crates/collab_ui/src/channel_view.rs | 52 ++++++++++- crates/collab_ui/src/chat_panel.rs | 30 ++++-- crates/collab_ui/src/collab_panel.rs | 4 +- crates/rpc/proto/zed.proto | 18 +++- 19 files changed, 478 insertions(+), 209 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffdc736830..18950cdb82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1217,6 +1217,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "clock", "collections", "db", "feature_flags", @@ -1530,6 +1531,7 @@ dependencies = [ "tracing-subscriber", "unindent", "util", + "uuid 1.4.1", "workspace", ] diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a03eb1f1b5..a097cc5467 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -2,14 +2,17 @@ use crate::Channel; use anyhow::Result; use client::{Client, Collaborator, UserStore}; use collections::HashMap; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use language::proto::serialize_version; use rpc::{ proto::{self, PeerId}, TypedEnvelope, }; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use util::ResultExt; +const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250); + pub(crate) fn init(client: &Arc) { client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators); @@ -24,11 +27,13 @@ pub struct ChannelBuffer { buffer_epoch: u64, client: Arc, subscription: Option, + acknowledge_task: Option>>, } pub enum ChannelBufferEvent { CollaboratorsChanged, Disconnected, + BufferEdited, } impl Entity for ChannelBuffer { @@ -36,6 +41,9 @@ impl Entity for ChannelBuffer { fn release(&mut self, _: &mut AppContext) { if self.connected { + if let Some(task) = self.acknowledge_task.take() { + task.detach(); + } self.client .send(proto::LeaveChannelBuffer { channel_id: self.channel.id, @@ -81,6 +89,7 @@ impl ChannelBuffer { client, connected: true, collaborators: Default::default(), + acknowledge_task: None, channel, subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), user_store, @@ -159,19 +168,45 @@ impl ChannelBuffer { &mut self, _: ModelHandle, event: &language::Event, - _: &mut ModelContext, + cx: &mut ModelContext, ) { - if let language::Event::Operation(operation) = event { - let operation = language::proto::serialize_operation(operation); - self.client - .send(proto::UpdateChannelBuffer { - channel_id: self.channel.id, - operations: vec![operation], - }) - .log_err(); + match event { + language::Event::Operation(operation) => { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + channel_id: self.channel.id, + operations: vec![operation], + }) + .log_err(); + } + language::Event::Edited => { + cx.emit(ChannelBufferEvent::BufferEdited); + } + _ => {} } } + pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) { + let buffer = self.buffer.read(cx); + let version = buffer.version(); + let buffer_id = buffer.remote_id(); + let client = self.client.clone(); + let epoch = self.epoch(); + + self.acknowledge_task = Some(cx.spawn_weak(|_, cx| async move { + cx.background().timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL).await; + client + .send(proto::AckBufferOperation { + buffer_id, + epoch, + version: serialize_version(&version), + }) + .ok(); + Ok(()) + })); + } + pub fn epoch(&self) -> u64 { self.buffer_epoch } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 8e03a3b6fd..5b32d67f63 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -1,4 +1,4 @@ -use crate::Channel; +use crate::{Channel, ChannelStore}; use anyhow::{anyhow, Result}; use client::{ proto, @@ -16,7 +16,9 @@ use util::{post_inc, ResultExt as _, TryFutureExt}; pub struct ChannelChat { channel: Arc, messages: SumTree, + channel_store: ModelHandle, loaded_all_messages: bool, + last_acknowledged_id: Option, next_pending_message_id: usize, user_store: ModelHandle, rpc: Arc, @@ -77,6 +79,7 @@ impl Entity for ChannelChat { impl ChannelChat { pub async fn new( channel: Arc, + channel_store: ModelHandle, user_store: ModelHandle, client: Arc, mut cx: AsyncAppContext, @@ -94,11 +97,13 @@ impl ChannelChat { let mut this = Self { channel, user_store, + channel_store, rpc: client, outgoing_messages_lock: Default::default(), messages: Default::default(), loaded_all_messages, next_pending_message_id: 0, + last_acknowledged_id: None, rng: StdRng::from_entropy(), _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), }; @@ -219,6 +224,26 @@ impl ChannelChat { false } + pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext) { + if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id { + if self + .last_acknowledged_id + .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id) + { + self.rpc + .send(proto::AckChannelMessage { + channel_id: self.channel.id, + message_id: latest_message_id, + }) + .ok(); + self.last_acknowledged_id = Some(latest_message_id); + self.channel_store.update(cx, |store, cx| { + store.acknowledge_message_id(self.channel.id, latest_message_id, cx); + }); + } + } + } + pub fn rejoin(&mut self, cx: &mut ModelContext) { let user_store = self.user_store.clone(); let rpc = self.rpc.clone(); diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index f0f66f4839..db6d5f42c5 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -43,8 +43,8 @@ pub type ChannelData = (Channel, ChannelPath); pub struct Channel { pub id: ChannelId, pub name: String, - pub has_note_changed: bool, - pub has_new_messages: bool, + pub unseen_note_version: Option<(u64, clock::Global)>, + pub unseen_message_id: Option, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] @@ -201,34 +201,60 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - let open_channel_buffer = self.open_channel_resource( + self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), cx, - ); - cx.spawn(|this, mut cx| async move { - let buffer = open_channel_buffer.await?; - this.update(&mut cx, |this, cx| { - this.channel_index.clear_note_changed(channel_id); - cx.notify(); - }); - Ok(buffer) - }) + ) } pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option { self.channel_index .by_id() .get(&channel_id) - .map(|channel| channel.has_note_changed) + .map(|channel| channel.unseen_note_version.is_some()) } pub fn has_new_messages(&self, channel_id: ChannelId) -> Option { self.channel_index .by_id() .get(&channel_id) - .map(|channel| channel.has_new_messages) + .map(|channel| channel.unseen_message_id.is_some()) + } + + pub fn notes_changed( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + cx: &mut ModelContext, + ) { + self.channel_index.note_changed(channel_id, epoch, version); + cx.notify(); + } + + pub fn acknowledge_message_id( + &mut self, + channel_id: ChannelId, + message_id: u64, + cx: &mut ModelContext, + ) { + self.channel_index + .acknowledge_message_id(channel_id, message_id); + cx.notify(); + } + + pub fn acknowledge_notes_version( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + cx: &mut ModelContext, + ) { + self.channel_index + .acknowledge_note_version(channel_id, epoch, version); + cx.notify(); } pub fn open_channel_chat( @@ -238,20 +264,13 @@ impl ChannelStore { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - let open_channel_chat = self.open_channel_resource( + let this = cx.handle(); + self.open_channel_resource( channel_id, |this| &mut this.opened_chats, - |channel, cx| ChannelChat::new(channel, user_store, client, cx), + |channel, cx| ChannelChat::new(channel, this, user_store, client, cx), cx, - ); - cx.spawn(|this, mut cx| async move { - let chat = open_channel_chat.await?; - this.update(&mut cx, |this, cx| { - this.channel_index.clear_message_changed(channel_id); - cx.notify(); - }); - Ok(chat) - }) + ) } /// Asynchronously open a given resource associated with a channel. @@ -811,8 +830,8 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, - has_note_changed: false, - has_new_messages: false, + unseen_note_version: None, + unseen_message_id: None, }), ), } @@ -822,8 +841,8 @@ impl ChannelStore { || !payload.delete_channels.is_empty() || !payload.insert_edge.is_empty() || !payload.delete_edge.is_empty() - || !payload.notes_changed.is_empty() - || !payload.new_messages.is_empty(); + || !payload.unseen_channel_messages.is_empty() + || !payload.unseen_channel_buffer_changes.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { @@ -850,12 +869,20 @@ impl ChannelStore { index.insert(channel) } - for id_changed in payload.notes_changed { - index.note_changed(id_changed); + for unseen_buffer_change in payload.unseen_channel_buffer_changes { + let version = language::proto::deserialize_version(&unseen_buffer_change.version); + index.note_changed( + unseen_buffer_change.channel_id, + unseen_buffer_change.epoch, + &version, + ); } - for id_changed in payload.new_messages { - index.new_messages(id_changed); + for unseen_channel_message in payload.unseen_channel_messages { + index.new_messages( + unseen_channel_message.channel_id, + unseen_channel_message.message_id, + ); } for edge in payload.insert_edge { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 513e20e3a7..2a93ca6573 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -39,17 +39,38 @@ impl ChannelIndex { } } - pub fn clear_note_changed(&mut self, channel_id: ChannelId) { + pub fn acknowledge_note_version( + &mut self, + channel_id: ChannelId, + epoch: u64, + version: &clock::Global, + ) { if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { - Arc::make_mut(channel).has_note_changed = false; + let channel = Arc::make_mut(channel); + if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version { + if epoch > *unseen_epoch + || epoch == *unseen_epoch && version.observed_all(unseen_version) + { + channel.unseen_note_version = None; + } + } } } - pub fn clear_message_changed(&mut self, channel_id: ChannelId) { + pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) { if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { - Arc::make_mut(channel).has_new_messages = false; + let channel = Arc::make_mut(channel); + if let Some(unseen_message_id) = channel.unseen_message_id { + if message_id >= unseen_message_id { + channel.unseen_message_id = None; + } + } } } + + pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { + insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version); + } } impl Deref for ChannelIndex { @@ -88,15 +109,14 @@ impl<'a> ChannelPathsInsertGuard<'a> { } } - pub fn note_changed(&mut self, channel_id: ChannelId) { - if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { - Arc::make_mut(channel).has_note_changed = true; - } + pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { + insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version); } - pub fn new_messages(&mut self, channel_id: ChannelId) { + pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) { if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { - Arc::make_mut(channel).has_new_messages = true; + let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0); + *unseen_message_id = message_id.max(*unseen_message_id); } } @@ -109,8 +129,8 @@ impl<'a> ChannelPathsInsertGuard<'a> { Arc::new(Channel { id: channel_proto.id, name: channel_proto.name, - has_note_changed: false, - has_new_messages: false, + unseen_note_version: None, + unseen_message_id: None, }), ); self.insert_root(channel_proto.id); @@ -186,3 +206,21 @@ fn channel_path_sorting_key<'a>( path.iter() .map(|id| Some(channels_by_id.get(id)?.name.as_str())) } + +fn insert_note_changed( + channels_by_id: &mut BTreeMap>, + channel_id: u64, + epoch: u64, + version: &clock::Global, +) { + if let Some(channel) = channels_by_id.get_mut(&channel_id) { + let unseen_version = Arc::make_mut(channel) + .unseen_note_version + .get_or_insert((0, clock::Global::new())); + if epoch > unseen_version.0 { + *unseen_version = (epoch, version.clone()); + } else { + unseen_version.1.join(&version); + } + } +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5eae700404..5767ac54b7 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -34,7 +34,7 @@ use std::{ future::Future, marker::PhantomData, path::PathBuf, - sync::{Arc, Weak}, + sync::{atomic::AtomicU64, Arc, Weak}, time::{Duration, Instant}, }; use telemetry::Telemetry; @@ -105,7 +105,7 @@ pub fn init(client: &Arc, cx: &mut AppContext) { } pub struct Client { - id: usize, + id: AtomicU64, peer: Arc, http: Arc, telemetry: Arc, @@ -374,7 +374,7 @@ impl settings::Setting for TelemetrySettings { impl Client { pub fn new(http: Arc, cx: &AppContext) -> Arc { Arc::new(Self { - id: 0, + id: AtomicU64::new(0), peer: Peer::new(0), telemetry: Telemetry::new(http.clone(), cx), http, @@ -387,17 +387,16 @@ impl Client { }) } - pub fn id(&self) -> usize { - self.id + pub fn id(&self) -> u64 { + self.id.load(std::sync::atomic::Ordering::SeqCst) } pub fn http_client(&self) -> Arc { self.http.clone() } - #[cfg(any(test, feature = "test-support"))] - pub fn set_id(&mut self, id: usize) -> &Self { - self.id = id; + pub fn set_id(&self, id: u64) -> &Self { + self.id.store(id, std::sync::atomic::Ordering::SeqCst); self } @@ -454,7 +453,7 @@ impl Client { } fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { - log::info!("set status on client {}: {:?}", self.id, status); + log::info!("set status on client {}: {:?}", self.id(), status); let mut state = self.state.write(); *state.status.0.borrow_mut() = status; @@ -805,6 +804,7 @@ impl Client { } } let credentials = credentials.unwrap(); + self.set_id(credentials.user_id); if was_disconnected { self.set_status(Status::Connecting, cx); @@ -1221,7 +1221,7 @@ impl Client { } pub fn send(&self, message: T) -> Result<()> { - log::debug!("rpc send. client_id:{}, name:{}", self.id, T::NAME); + log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME); self.peer.send(self.connection_id()?, message) } @@ -1237,7 +1237,7 @@ impl Client { &self, request: T, ) -> impl Future>> { - let client_id = self.id; + let client_id = self.id(); log::debug!( "rpc request start. client_id:{}. name:{}", client_id, @@ -1258,7 +1258,7 @@ impl Client { } fn respond(&self, receipt: Receipt, response: T::Response) -> Result<()> { - log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME); + log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME); self.peer.respond(receipt, response) } @@ -1267,7 +1267,7 @@ impl Client { receipt: Receipt, error: proto::Error, ) -> Result<()> { - log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME); + log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME); self.peer.respond_with_error(receipt, error) } @@ -1336,7 +1336,7 @@ impl Client { if let Some(handler) = handler { let future = handler(subscriber, message, &self, cx.clone()); - let client_id = self.id; + let client_id = self.id(); log::debug!( "rpc message received. client_id:{}, sender_id:{:?}, type:{}", client_id, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 13bb3c06e8..e60b7cc33d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -439,8 +439,8 @@ pub struct ChannelsForUser { pub channels: ChannelGraph, pub channel_participants: HashMap>, pub channels_with_admin_privileges: HashSet, - pub channels_with_changed_notes: HashSet, - pub channels_with_new_messages: HashSet, + pub unseen_buffer_changes: Vec, + pub channel_messages: Vec, } #[derive(Debug)] diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 78ccd9e54a..c85432f2bb 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -432,7 +432,12 @@ impl Database { channel_id: ChannelId, user: UserId, operations: &[proto::Operation], - ) -> Result<(Vec, Vec)> { + ) -> Result<( + Vec, + Vec, + i32, + Vec, + )> { self.transaction(move |tx| async move { self.check_user_is_channel_member(channel_id, user, &*tx) .await?; @@ -453,6 +458,7 @@ impl Database { .collect::>(); let mut channel_members; + let max_version; if !operations.is_empty() { let max_operation = operations @@ -460,6 +466,11 @@ impl Database { .max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref())) .unwrap(); + max_version = vec![proto::VectorClockEntry { + replica_id: *max_operation.replica_id.as_ref() as u32, + timestamp: *max_operation.lamport_timestamp.as_ref() as u32, + }]; + // get current channel participants and save the max operation above self.save_max_operation( user, @@ -492,6 +503,7 @@ impl Database { .await?; } else { channel_members = Vec::new(); + max_version = Vec::new(); } let mut connections = Vec::new(); @@ -510,7 +522,7 @@ impl Database { }); } - Ok((connections, channel_members)) + Ok((connections, channel_members, buffer.epoch, max_version)) }) .await } @@ -712,12 +724,12 @@ impl Database { .await } - pub async fn channels_with_changed_notes( + pub async fn unseen_channel_buffer_changes( &self, user_id: UserId, channel_ids: &[ChannelId], tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result> { #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] enum QueryIds { ChannelId, @@ -750,37 +762,45 @@ impl Database { } drop(rows); - let last_operations = self - .get_last_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx) + let latest_operations = self + .get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx) .await?; - let mut channels_with_new_changes = HashSet::default(); - for last_operation in last_operations { - if let Some(observed_edit) = observed_edits_by_buffer_id.get(&last_operation.buffer_id) - { - if observed_edit.epoch == last_operation.epoch - && observed_edit.lamport_timestamp == last_operation.lamport_timestamp - && observed_edit.replica_id == last_operation.replica_id + let mut changes = Vec::default(); + for latest in latest_operations { + if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) { + if ( + observed.epoch, + observed.lamport_timestamp, + observed.replica_id, + ) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id) { continue; } } - if let Some(channel_id) = channel_ids_by_buffer_id.get(&last_operation.buffer_id) { - channels_with_new_changes.insert(*channel_id); + if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) { + changes.push(proto::UnseenChannelBufferChange { + channel_id: channel_id.to_proto(), + epoch: latest.epoch as u64, + version: vec![proto::VectorClockEntry { + replica_id: latest.replica_id as u32, + timestamp: latest.lamport_timestamp as u32, + }], + }); } } - Ok(channels_with_new_changes) + Ok(changes) } - pub async fn get_last_operations_for_buffers( + pub async fn get_latest_operations_for_buffers( &self, - channel_ids: impl IntoIterator, + buffer_ids: impl IntoIterator, tx: &DatabaseTransaction, ) -> Result> { let mut values = String::new(); - for id in channel_ids { + for id in buffer_ids { if !values.is_empty() { values.push_str(", "); } @@ -795,13 +815,10 @@ impl Database { r#" SELECT * - FROM ( + FROM + ( SELECT - buffer_id, - epoch, - lamport_timestamp, - replica_id, - value, + *, row_number() OVER ( PARTITION BY buffer_id ORDER BY @@ -812,17 +829,17 @@ impl Database { FROM buffer_operations WHERE buffer_id in ({values}) - ) AS operations + ) AS last_operations WHERE row_number = 1 "#, ); let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - let operations = buffer_operation::Model::find_by_statement(stmt) + Ok(buffer_operation::Entity::find() + .from_raw_sql(stmt) .all(&*tx) - .await?; - Ok(operations) + .await?) } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 207cca7657..ab31f59541 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -463,20 +463,20 @@ impl Database { } let channel_ids = graph.channels.iter().map(|c| c.id).collect::>(); - let channels_with_changed_notes = self - .channels_with_changed_notes(user_id, &channel_ids, &*tx) + let channel_buffer_changes = self + .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx) .await?; - let channels_with_new_messages = self - .channels_with_new_messages(user_id, &channel_ids, &*tx) + let unseen_messages = self + .unseen_channel_messages(user_id, &channel_ids, &*tx) .await?; Ok(ChannelsForUser { channels: graph, channel_participants, channels_with_admin_privileges, - channels_with_changed_notes, - channels_with_new_messages, + unseen_buffer_changes: channel_buffer_changes, + channel_messages: unseen_messages, }) } diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 893c1726da..db1252230e 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -279,12 +279,12 @@ impl Database { Ok(()) } - pub async fn channels_with_new_messages( + pub async fn unseen_channel_messages( &self, user_id: UserId, channel_ids: &[ChannelId], tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result> { let mut observed_messages_by_channel_id = HashMap::default(); let mut rows = observed_channel_messages::Entity::find() .filter(observed_channel_messages::Column::UserId.eq(user_id)) @@ -334,7 +334,7 @@ impl Database { .all(&*tx) .await?; - let mut channels_with_new_changes = HashSet::default(); + let mut changes = Vec::new(); for last_message in last_messages { if let Some(observed_message) = observed_messages_by_channel_id.get(&last_message.channel_id) @@ -343,10 +343,13 @@ impl Database { continue; } } - channels_with_new_changes.insert(last_message.channel_id); + changes.push(proto::UnseenChannelMessage { + channel_id: last_message.channel_id.to_proto(), + message_id: last_message.id.to_proto(), + }); } - Ok(channels_with_new_changes) + Ok(changes) } pub async fn remove_channel_message( diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 407cc22108..e5d8ab8cf9 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -235,7 +235,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .transaction(|tx| { let buffers = &buffers; async move { - db.get_last_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx) + db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx) .await } }) @@ -299,7 +299,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .transaction(|tx| { let buffers = &buffers; async move { - db.get_last_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx) + db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx) .await } }) @@ -317,7 +317,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .transaction(|tx| { let buffers = &buffers; async move { - db.get_last_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx) + db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx) .await } }) @@ -331,11 +331,11 @@ async fn test_channel_buffers_last_operations(db: &Database) { ], ); - let changed_channels = db + let buffer_changes = db .transaction(|tx| { let buffers = &buffers; async move { - db.channels_with_changed_notes( + db.unseen_channel_buffer_changes( observer_id, &[ buffers[0].channel_id, @@ -349,31 +349,42 @@ async fn test_channel_buffers_last_operations(db: &Database) { }) .await .unwrap(); + assert_eq!( - changed_channels, + buffer_changes, [ - buffers[0].channel_id, - buffers[1].channel_id, - buffers[2].channel_id, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[0].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[0].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[1].channel_id.to_proto(), + epoch: 1, + version: serialize_version(&text_buffers[1].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[2].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[2].version()), + }, ] - .into_iter() - .collect::>() ); db.observe_buffer_version( buffers[1].id, observer_id, 1, - &serialize_version(&text_buffers[1].version()), + serialize_version(&text_buffers[1].version()).as_slice(), ) .await .unwrap(); - let changed_channels = db + let buffer_changes = db .transaction(|tx| { let buffers = &buffers; async move { - db.channels_with_changed_notes( + db.unseen_channel_buffer_changes( observer_id, &[ buffers[0].channel_id, @@ -387,11 +398,21 @@ async fn test_channel_buffers_last_operations(db: &Database) { }) .await .unwrap(); + assert_eq!( - changed_channels, - [buffers[0].channel_id, buffers[2].channel_id,] - .into_iter() - .collect::>() + buffer_changes, + [ + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[0].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[0].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[2].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[2].version()), + }, + ] ); // Observe an earlier version of the buffer. @@ -407,11 +428,11 @@ async fn test_channel_buffers_last_operations(db: &Database) { .await .unwrap(); - let changed_channels = db + let buffer_changes = db .transaction(|tx| { let buffers = &buffers; async move { - db.channels_with_changed_notes( + db.unseen_channel_buffer_changes( observer_id, &[ buffers[0].channel_id, @@ -425,11 +446,21 @@ async fn test_channel_buffers_last_operations(db: &Database) { }) .await .unwrap(); + assert_eq!( - changed_channels, - [buffers[0].channel_id, buffers[2].channel_id,] - .into_iter() - .collect::>() + buffer_changes, + [ + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[0].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[0].version()), + }, + rpc::proto::UnseenChannelBufferChange { + channel_id: buffers[2].channel_id.to_proto(), + epoch: 0, + version: serialize_version(&text_buffers[2].version()), + }, + ] ); } diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index f3d385e4a0..4966ef1bda 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -144,25 +144,32 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - let _ = db + let (fourth_message, _, _) = db .create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4) .await .unwrap(); // Check that observer has new messages - let channels_with_new_messages = db + let unseen_messages = db .transaction(|tx| async move { - db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) .await }) .await .unwrap(); assert_eq!( - channels_with_new_messages, - [channel_1, channel_2] - .into_iter() - .collect::>() + unseen_messages, + [ + rpc::proto::UnseenChannelMessage { + channel_id: channel_1.to_proto(), + message_id: third_message.to_proto(), + }, + rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }, + ] ); // Observe the second message @@ -171,18 +178,25 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap(); // Make sure the observer still has a new message - let channels_with_new_messages = db + let unseen_messages = db .transaction(|tx| async move { - db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) .await }) .await .unwrap(); assert_eq!( - channels_with_new_messages, - [channel_1, channel_2] - .into_iter() - .collect::>() + unseen_messages, + [ + rpc::proto::UnseenChannelMessage { + channel_id: channel_1.to_proto(), + message_id: third_message.to_proto(), + }, + rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }, + ] ); // Observe the third message, @@ -191,16 +205,20 @@ async fn test_channel_message_new_notification(db: &Arc) { .unwrap(); // Make sure the observer does not have a new method - let channels_with_new_messages = db + let unseen_messages = db .transaction(|tx| async move { - db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) .await }) .await .unwrap(); + assert_eq!( - channels_with_new_messages, - [channel_2].into_iter().collect::>() + unseen_messages, + [rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }] ); // Observe the second message again, should not regress our observed state @@ -208,16 +226,19 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - // Make sure the observer does not have a new method - let channels_with_new_messages = db + // Make sure the observer does not have a new message + let unseen_messages = db .transaction(|tx| async move { - db.channels_with_new_messages(observer, &[channel_1, channel_2], &*tx) + db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) .await }) .await .unwrap(); assert_eq!( - channels_with_new_messages, - [channel_2].into_iter().collect::>() + unseen_messages, + [rpc::proto::UnseenChannelMessage { + channel_id: channel_2.to_proto(), + message_id: fourth_message.to_proto(), + }] ); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 371d0466c1..1f0ecce2bf 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -274,7 +274,8 @@ impl Server { .add_message_handler(unfollow) .add_message_handler(update_followers) .add_message_handler(update_diff_base) - .add_request_handler(get_private_user_info); + .add_request_handler(get_private_user_info) + .add_message_handler(acknowledge_channel_message); Arc::new(server) } @@ -2568,16 +2569,8 @@ async fn respond_to_channel_invite( name: channel.name, }), ); - update.notes_changed = result - .channels_with_changed_notes - .iter() - .map(|id| id.to_proto()) - .collect(); - update.new_messages = result - .channels_with_new_messages - .iter() - .map(|id| id.to_proto()) - .collect(); + update.unseen_channel_messages = result.channel_messages; + update.unseen_channel_buffer_changes = result.unseen_buffer_changes; update.insert_edge = result.channels.edges; update .channel_participants @@ -2701,7 +2694,7 @@ async fn update_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let (collaborators, non_collaborators) = db + let (collaborators, non_collaborators, epoch, version) = db .update_channel_buffer(channel_id, session.user_id, &request.operations) .await?; @@ -2726,7 +2719,11 @@ async fn update_channel_buffer( session.peer.send( peer_id.into(), proto::UpdateChannels { - notes_changed: vec![channel_id.to_proto()], + unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange { + channel_id: channel_id.to_proto(), + epoch: epoch as u64, + version: version.clone(), + }], ..Default::default() }, ) @@ -2859,9 +2856,7 @@ async fn send_channel_message( message: Some(message), })?; - dbg!(&non_participants); let pool = &*session.connection_pool().await; - broadcast( None, non_participants @@ -2871,7 +2866,10 @@ async fn send_channel_message( session.peer.send( peer_id.into(), proto::UpdateChannels { - new_messages: vec![channel_id.to_proto()], + unseen_channel_messages: vec![proto::UnseenChannelMessage { + channel_id: channel_id.to_proto(), + message_id: message_id.to_proto(), + }], ..Default::default() }, ) @@ -2900,6 +2898,20 @@ async fn remove_channel_message( Ok(()) } +async fn acknowledge_channel_message( + request: proto::AckChannelMessage, + session: Session, +) -> Result<()> { + let channel_id = ChannelId::from_proto(request.channel_id); + let message_id = MessageId::from_proto(request.message_id); + session + .db() + .await + .observe_channel_message(channel_id, session.user_id, message_id) + .await?; + Ok(()) +} + async fn join_channel_chat( request: proto::JoinChannelChat, response: Response, @@ -3035,18 +3047,8 @@ fn build_initial_channels_update( }); } - update.notes_changed = channels - .channels_with_changed_notes - .iter() - .map(|channel_id| channel_id.to_proto()) - .collect(); - - update.new_messages = channels - .channels_with_new_messages - .iter() - .map(|channel_id| channel_id.to_proto()) - .collect(); - + update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; + update.unseen_channel_messages = channels.channel_messages; update.insert_edge = channels.channels.edges; for (channel_id, participants) in channels.channel_participants { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 68acffacf8..d9d12d485b 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -445,8 +445,8 @@ fn channel(id: u64, name: &'static str) -> Channel { Channel { id, name: name.to_string(), - has_note_changed: false, - has_new_messages: false, + unseen_note_version: None, + unseen_message_id: None, } } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index a56df311bd..cf5b58703c 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -151,12 +151,12 @@ impl TestServer { Arc::get_mut(&mut client) .unwrap() - .set_id(user_id.0 as usize) + .set_id(user_id.to_proto()) .override_authenticate(move |cx| { cx.spawn(|_| async move { let access_token = "the-token".to_string(); Ok(Credentials { - user_id: user_id.0 as u64, + user_id: user_id.to_proto(), access_token, }) }) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 1d9e409748..a955768050 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use call::report_call_event_for_channel; -use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; use client::{ proto::{self, PeerId}, Collaborator, ParticipantIndex, @@ -36,6 +36,7 @@ pub fn init(cx: &mut AppContext) { pub struct ChannelView { pub editor: ViewHandle, project: ModelHandle, + channel_store: ModelHandle, channel_buffer: ModelHandle, remote_id: Option, _editor_event_subscription: Subscription, @@ -94,7 +95,13 @@ impl ChannelView { pane.update(&mut cx, |pane, cx| { pane.items_of_type::() .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) - .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx))) + .unwrap_or_else(|| { + cx.add_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }) + }) }) .ok_or_else(|| anyhow!("pane was dropped")) }) @@ -102,6 +109,7 @@ impl ChannelView { pub fn new( project: ModelHandle, + channel_store: ModelHandle, channel_buffer: ModelHandle, cx: &mut ViewContext, ) -> Self { @@ -121,6 +129,7 @@ impl ChannelView { Self { editor, project, + channel_store, channel_buffer, remote_id: None, _editor_event_subscription, @@ -137,13 +146,44 @@ impl ChannelView { event: &ChannelBufferEvent, cx: &mut ViewContext, ) { - if let ChannelBufferEvent::Disconnected = event { - self.editor.update(cx, |editor, cx| { + match event { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { editor.set_read_only(true); cx.notify(); - }) + }), + ChannelBufferEvent::BufferEdited => { + if cx.is_self_focused() || self.editor.is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel().id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + _ => {} } } + + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel().id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); + }); + } } impl Entity for ChannelView { @@ -161,6 +201,7 @@ impl View for ChannelView { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { + self.acknowledge_buffer_version(cx); cx.focus(self.editor.as_any()) } } @@ -200,6 +241,7 @@ impl Item for ChannelView { fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { Some(Self::new( self.project.clone(), + self.channel_store.clone(), self.channel_buffer.clone(), cx, )) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 81a421e8d9..626b3004d7 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -42,6 +42,7 @@ pub struct ChatPanel { local_timezone: UtcOffset, fs: Arc, width: Option, + active: bool, pending_serialization: Task>, subscriptions: Vec, workspace: WeakViewHandle, @@ -138,6 +139,7 @@ impl ChatPanel { has_focus: false, subscriptions: Vec::new(), workspace: workspace_handle, + active: false, width: None, }; @@ -154,9 +156,9 @@ impl ChatPanel { }), ); - this.init_active_channel(cx); + this.update_channel_count(cx); cx.observe(&this.channel_store, |this, _, cx| { - this.init_active_channel(cx); + this.update_channel_count(cx) }) .detach(); @@ -225,10 +227,8 @@ impl ChatPanel { ); } - fn init_active_channel(&mut self, cx: &mut ViewContext) { + fn update_channel_count(&mut self, cx: &mut ViewContext) { let channel_count = self.channel_store.read(cx).channel_count(); - self.message_list.reset(0); - self.active_chat = None; self.channel_select.update(cx, |select, cx| { select.set_item_count(channel_count, cx); }); @@ -247,6 +247,7 @@ impl ChatPanel { } let subscription = cx.subscribe(&chat, Self::channel_did_change); self.active_chat = Some((chat, subscription)); + self.acknowledge_last_message(cx); self.channel_select.update(cx, |select, cx| { if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) { select.set_selected_index(ix, cx); @@ -268,11 +269,22 @@ impl ChatPanel { new_count, } => { self.message_list.splice(old_range.clone(), *new_count); + self.acknowledge_last_message(cx); } } cx.notify(); } + fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) { + if self.active { + if let Some((chat, _)) = &self.active_chat { + chat.update(cx, |chat, cx| { + chat.acknowledge_last_message(cx); + }); + } + } + } + fn render_channel(&self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx); Flex::column() @@ -627,8 +639,12 @@ impl Panel for ChatPanel { } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - if active && !is_chat_feature_enabled(cx) { - cx.emit(Event::Dismissed); + self.active = active; + if active { + self.acknowledge_last_message(cx); + if !is_chat_feature_enabled(cx) { + cx.emit(Event::Dismissed); + } } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 53d5140a12..5e90d438fc 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1821,7 +1821,7 @@ impl CollabPanel { channel.name.clone(), theme .channel_name - .in_state(channel.has_new_messages) + .in_state(channel.unseen_message_id.is_some()) .text .clone(), ) @@ -1880,7 +1880,7 @@ impl CollabPanel { let participants = self.channel_store.read(cx).channel_participants(channel_id); if participants.is_empty() { - if channel.has_note_changed { + if channel.unseen_note_version.is_some() { Svg::new("icons/terminal.svg") .with_color(theme.channel_note_active_color) .constrained() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 0f289edcf8..3501e70e6a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -957,8 +957,19 @@ message UpdateChannels { repeated uint64 remove_channel_invitations = 6; repeated ChannelParticipants channel_participants = 7; repeated ChannelPermission channel_permissions = 8; - repeated uint64 notes_changed = 9; - repeated uint64 new_messages = 10; + repeated UnseenChannelMessage unseen_channel_messages = 9; + repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; +} + +message UnseenChannelMessage { + uint64 channel_id = 1; + uint64 message_id = 2; +} + +message UnseenChannelBufferChange { + uint64 channel_id = 1; + uint64 epoch = 2; + repeated VectorClockEntry version = 3; } message ChannelEdge { @@ -1127,8 +1138,7 @@ message RejoinChannelBuffersResponse { message AckBufferOperation { uint64 buffer_id = 1; uint64 epoch = 2; - uint64 lamport_timestamp = 3; - uint64 replica_id = 4; + repeated VectorClockEntry version = 3; } message JoinChannelBufferResponse { From 23ee8211c7f44dcab15c2f3b39a5260547fcdea9 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 3 Oct 2023 19:30:05 -0700 Subject: [PATCH 39/67] Lower frequency of popup warning when leaving a call co-authored-by: conrad --- crates/call/src/room.rs | 4 ++++ crates/collab_ui/src/collab_panel.rs | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index f24a8e9a9c..ce01099576 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -104,6 +104,10 @@ impl Room { self.channel_id } + pub fn is_sharing_project(&self) -> bool { + !self.shared_projects.is_empty() + } + #[cfg(any(test, feature = "test-support"))] pub fn is_connected(&self) -> bool { if let Some(live_kit) = self.live_kit.as_ref() { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4a0b37b262..7a81b49868 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3165,10 +3165,17 @@ impl CollabPanel { let window = cx.window(); let active_call = ActiveCall::global(cx); cx.spawn(|_, mut cx| async move { - if active_call.read_with(&mut cx, |active_call, _| active_call.room().is_some()) { + if active_call.read_with(&mut cx, |active_call, cx| { + if let Some(room) = active_call.room() { + let room = room.read(cx); + room.is_sharing_project() && room.remote_participants().len() > 0 + } else { + false + } + }) { let answer = window.prompt( PromptLevel::Warning, - "Do you want to leave the current call?", + "Leaving this call will unshare your current project.\nDo you want to switch channels?", &["Yes, Join Channel", "Cancel"], &mut cx, ); From 4ff80a707407716cea9e9f664a3421403f304f56 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 3 Oct 2023 19:31:55 -0700 Subject: [PATCH 40/67] Fix a few mouse event id bugs and move facepile to the left co-authored-by: conrad --- crates/collab_ui/src/collab_panel.rs | 185 +++++++++++++++------------ 1 file changed, 102 insertions(+), 83 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 7a81b49868..5b2b7f0018 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -592,6 +592,7 @@ impl CollabPanel { *channel_id, &theme.collab_panel, is_selected, + ix, cx, ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( @@ -1918,7 +1919,8 @@ impl CollabPanel { enum ChannelCall {} enum ChannelNote {} - enum IconTooltip {} + enum NotesTooltip {} + enum ChatTooltip {} enum ChannelTooltip {} let mut is_dragged_over = false; @@ -1965,72 +1967,111 @@ impl CollabPanel { let style = collab_theme .channel_name .in_state(channel.unseen_note_version.is_some()); - Label::new(channel.name.clone(), style.text.clone()) - .contained() - .with_style(style.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( - MouseEventHandler::new::(ix, cx, move |_, cx| { - let participants = - self.channel_store.read(cx).channel_participants(channel_id); - if !participants.is_empty() { - let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - - FacePile::new(collab_theme.face_overlap) - .with_children( - participants - .iter() - .filter_map(|user| { - Some( - Image::from_data(user.avatar.clone()?) - .with_style(collab_theme.channel_avatar), - ) - }) - .take(FACEPILE_LIMIT), - ) - .with_children((extra_count > 0).then(|| { - Label::new( - format!("+{}", extra_count), - collab_theme.extra_participant_label.text.clone(), - ) - .contained() - .with_style(collab_theme.extra_participant_label.container) - })) - .with_tooltip::( - channel_id as usize, + Flex::row() + .with_child( + Label::new(channel.name.clone(), style.text.clone()) + .contained() + .with_style(style.container) + .aligned() + .left() + .with_tooltip::( + ix, if is_active { - "Open Channel Notes" + "Open channel notes" } else { "Join channel" }, None, theme.tooltip.clone(), cx, - ) + ), + ) + .with_children({ + let participants = + self.channel_store.read(cx).channel_participants(channel_id); + + if !participants.is_empty() { + let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + let result = FacePile::new(collab_theme.face_overlap) + .with_children( + participants + .iter() + .filter_map(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(collab_theme.channel_avatar), + ) + }) + .take(FACEPILE_LIMIT), + ) + .with_children((extra_count > 0).then(|| { + Label::new( + format!("+{}", extra_count), + collab_theme.extra_participant_label.text.clone(), + ) + .contained() + .with_style(collab_theme.extra_participant_label.container) + })); + + Some(result) + } else { + None + } + }) + .with_spacing(8.) + .align_children_center() + .flex(1., true) + }) + .with_child( + MouseEventHandler::new::(ix, cx, move |_, _| { + if channel.unseen_message_id.is_some() { + Svg::new("icons/conversations.svg") + .with_color(collab_theme.channel_note_active_color) + .constrained() + .with_width(collab_theme.channel_hash.width) .into_any() } else if row_hovered { - Svg::new("icons/file.svg") + Svg::new("icons/conversations.svg") .with_color(collab_theme.channel_hash.color) .constrained() .with_width(collab_theme.channel_hash.width) + .into_any() + } else { + Empty::new() + .constrained() + .with_width(collab_theme.channel_hash.width) + .into_any() + } + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + }) + .with_tooltip::( + ix, + "Open channel chat", + None, + theme.tooltip.clone(), + cx, + ) + .contained() + .with_margin_right(4.), + ) + .with_child( + MouseEventHandler::new::(ix, cx, move |_, cx| { + if row_hovered || channel.unseen_note_version.is_some() { + Svg::new("icons/file.svg") + .with_color(if channel.unseen_note_version.is_some() { + collab_theme.channel_note_active_color + } else { + collab_theme.channel_hash.color + }) + .constrained() + .with_width(collab_theme.channel_hash.width) .contained() .with_margin_right(collab_theme.channel_hash.container.margin.left) - .with_tooltip::( - channel_id as usize, + .with_tooltip::( + ix as usize, "Open channel notes", None, theme.tooltip.clone(), @@ -2038,7 +2079,12 @@ impl CollabPanel { ) .into_any() } else { - Empty::new().into_any() + Empty::new() + .constrained() + .with_width(collab_theme.channel_hash.width) + .contained() + .with_margin_right(collab_theme.channel_hash.container.margin.left) + .into_any() } }) .on_click(MouseButton::Left, move |_, this, cx| { @@ -2051,34 +2097,6 @@ impl CollabPanel { }; }), ) - .with_child( - MouseEventHandler::new::(ix, cx, move |_, cx| { - let participants = - self.channel_store.read(cx).channel_participants(channel_id); - if participants.is_empty() { - if channel.unseen_message_id.is_some() { - Svg::new("icons/conversations.svg") - .with_color(collab_theme.channel_note_active_color) - .constrained() - .with_width(collab_theme.channel_hash.width) - .into_any() - } else if row_hovered { - Svg::new("icons/conversations.svg") - .with_color(collab_theme.channel_hash.color) - .constrained() - .with_width(collab_theme.channel_hash.width) - .into_any() - } else { - Empty::new().into_any() - } - } else { - Empty::new().into_any() - } - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - }), - ) .align_children_center() .styleable_component() .disclosable( @@ -2223,6 +2241,7 @@ impl CollabPanel { channel_id: ChannelId, theme: &theme::CollabPanel, is_selected: bool, + ix: usize, cx: &mut ViewContext, ) -> AnyElement { enum ChannelNotes {} @@ -2232,7 +2251,7 @@ impl CollabPanel { .or(theme.contact_avatar.height) .unwrap_or(0.); - MouseEventHandler::new::(channel_id as usize, cx, |state, cx| { + MouseEventHandler::new::(ix as usize, cx, |state, cx| { let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); let row = theme.project_row.in_state(is_selected).style_for(state); From 3bc7024f8b2a568ec219c413399ea1faba280844 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 3 Oct 2023 20:01:10 -0700 Subject: [PATCH 41/67] Fix unit test co-authored-by: Conrad --- crates/collab/src/tests/channel_buffer_tests.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 41662f10c4..82bd7e6afe 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -804,11 +804,13 @@ async fn test_channel_buffer_changes( assert!(has_buffer_changed); // Opening the buffer should clear the changed flag. - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + let project_b = client_b.build_empty_local_project(cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) .await .unwrap(); + deterministic.run_until_parked(); let has_buffer_changed = cx_b.read(|cx| { @@ -840,8 +842,12 @@ async fn test_channel_buffer_changes( assert!(!has_buffer_changed); // Closing the buffer should re-enable change tracking - cx_b.update(|_| { - drop(channel_buffer_b); + cx_b.update(|cx| { + workspace_b.update(cx, |workspace, cx| { + workspace.close_all_items_and_panes(&Default::default(), cx) + }); + + drop(channel_view_b) }); deterministic.run_until_parked(); From db8096ccdc4d34321df95aa70e53e90b0fe4fe61 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 3 Oct 2023 20:47:54 -0700 Subject: [PATCH 42/67] Fix most tests for new chat changes --- crates/collab/src/db/tests/buffer_tests.rs | 8 +++-- .../collab/src/tests/channel_message_tests.rs | 32 +++++++++++++++---- crates/collab_ui/src/chat_panel.rs | 4 +++ crates/collab_ui/src/collab_panel.rs | 4 +-- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index e5d8ab8cf9..f6e91b91f0 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -350,7 +350,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .await .unwrap(); - assert_eq!( + pretty_assertions::assert_eq!( buffer_changes, [ rpc::proto::UnseenChannelBufferChange { @@ -361,7 +361,11 @@ async fn test_channel_buffers_last_operations(db: &Database) { rpc::proto::UnseenChannelBufferChange { channel_id: buffers[1].channel_id.to_proto(), epoch: 1, - version: serialize_version(&text_buffers[1].version()), + version: serialize_version(&text_buffers[1].version()) + .into_iter() + .filter(|vector| vector.replica_id + == buffer_changes[1].version.first().unwrap().replica_id) + .collect::>(), }, rpc::proto::UnseenChannelBufferChange { channel_id: buffers[2].channel_id.to_proto(), diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 1b3a54dc42..0ee17cec59 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -1,7 +1,9 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::{ChannelChat, ChannelMessageId}; +use collab_ui::chat_panel::ChatPanel; use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext}; use std::sync::Arc; +use workspace::dock::Panel; #[gpui::test] async fn test_basic_channel_messages( @@ -244,6 +246,15 @@ async fn test_channel_message_changes( ) .await; + let other_channel_id = server + .make_channel( + "other-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + // Client A sends a message, client B should see that there is a new message. let channel_chat_a = client_a .channel_store() @@ -269,12 +280,22 @@ async fn test_channel_message_changes( assert!(b_has_messages); // Opening the chat should clear the changed flag. - let channel_chat_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx)) + cx_b.update(|cx| { + collab_ui::init(&client_b.app_state, cx); + }); + let project_b = client_b.build_empty_local_project(cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx)); + chat_panel_b + .update(cx_b, |chat_panel, cx| { + chat_panel.set_active(true, cx); + chat_panel.select_channel(channel_id, cx) + }) .await .unwrap(); + deterministic.run_until_parked(); + let b_has_messages = cx_b.read_with(|cx| { client_b .channel_store() @@ -304,10 +325,7 @@ async fn test_channel_message_changes( assert!(!b_has_messages); // Closing the chat should re-enable change tracking - - cx_b.update(|_| { - drop(channel_chat_b); - }); + todo!(); deterministic.run_until_parked(); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 626b3004d7..2050ee2f69 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -181,6 +181,10 @@ impl ChatPanel { }) } + pub fn active_chat(&self) -> Option> { + self.active_chat.as_ref().map(|(chat, _)| chat.clone()) + } + pub fn load( workspace: WeakViewHandle, cx: AsyncAppContext, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 5b2b7f0018..6214eecac2 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1964,9 +1964,7 @@ impl CollabPanel { .left(), ) .with_child({ - let style = collab_theme - .channel_name - .in_state(channel.unseen_note_version.is_some()); + let style = collab_theme.channel_name.inactive_state(); Flex::row() .with_child( Label::new(channel.name.clone(), style.text.clone()) From e548572f121e373e5368bb071b3580fd22f60c67 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 10:13:02 -0700 Subject: [PATCH 43/67] Fix channel messages test --- crates/channel/src/channel_chat.rs | 14 ++++++- crates/channel/src/channel_store.rs | 10 +++++ .../src/channel_store/channel_index.rs | 20 ++++++++-- .../collab/src/tests/channel_message_tests.rs | 40 +++++++++++++------ crates/collab_ui/src/chat_panel.rs | 14 ++++++- 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 5b32d67f63..29a260ea7e 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -1,4 +1,4 @@ -use crate::{Channel, ChannelStore}; +use crate::{Channel, ChannelId, ChannelStore}; use anyhow::{anyhow, Result}; use client::{ proto, @@ -57,6 +57,10 @@ pub enum ChannelChatEvent { old_range: Range, new_count: usize, }, + NewMessage { + channel_id: ChannelId, + message_id: u64, + }, } pub fn init(client: &Arc) { @@ -338,10 +342,15 @@ impl ChannelChat { .payload .message .ok_or_else(|| anyhow!("empty message"))?; + let message_id = message.id; let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; this.update(&mut cx, |this, cx| { - this.insert_messages(SumTree::from_item(message, &()), cx) + this.insert_messages(SumTree::from_item(message, &()), cx); + cx.emit(ChannelChatEvent::NewMessage { + channel_id: this.channel.id, + message_id, + }) }); Ok(()) @@ -413,6 +422,7 @@ impl ChannelChat { old_range: start_ix..end_ix, new_count, }); + cx.notify(); } } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index db6d5f42c5..bd72c92c7d 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -234,6 +234,16 @@ impl ChannelStore { cx.notify(); } + pub fn new_message( + &mut self, + channel_id: ChannelId, + message_id: u64, + cx: &mut ModelContext, + ) { + self.channel_index.new_message(channel_id, message_id); + cx.notify(); + } + pub fn acknowledge_message_id( &mut self, channel_id: ChannelId, diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 2a93ca6573..bf0de1b644 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -71,6 +71,10 @@ impl ChannelIndex { pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) { insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version); } + + pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) { + insert_new_message(&mut self.channels_by_id, channel_id, message_id) + } } impl Deref for ChannelIndex { @@ -114,10 +118,7 @@ impl<'a> ChannelPathsInsertGuard<'a> { } pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) { - if let Some(channel) = self.channels_by_id.get_mut(&channel_id) { - let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0); - *unseen_message_id = message_id.max(*unseen_message_id); - } + insert_new_message(&mut self.channels_by_id, channel_id, message_id) } pub fn insert(&mut self, channel_proto: proto::Channel) { @@ -224,3 +225,14 @@ fn insert_note_changed( } } } + +fn insert_new_message( + channels_by_id: &mut BTreeMap>, + channel_id: u64, + message_id: u64, +) { + if let Some(channel) = channels_by_id.get_mut(&channel_id) { + let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0); + *unseen_message_id = message_id.max(*unseen_message_id); + } +} diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index 0ee17cec59..0fc3b085ed 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -246,15 +246,6 @@ async fn test_channel_message_changes( ) .await; - let other_channel_id = server - .make_channel( - "other-channel", - None, - (&client_a, cx_a), - &mut [(&client_b, cx_b)], - ) - .await; - // Client A sends a message, client B should see that there is a new message. let channel_chat_a = client_a .channel_store() @@ -324,16 +315,39 @@ async fn test_channel_message_changes( assert!(!b_has_messages); - // Closing the chat should re-enable change tracking - todo!(); - - deterministic.run_until_parked(); + // Sending a message while the chat is closed should change the flag. + chat_panel_b.update(cx_b, |chat_panel, cx| { + chat_panel.set_active(false, cx); + }); + // Sending a message while the chat is open should not change the flag. channel_chat_a .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap()) .await .unwrap(); + deterministic.run_until_parked(); + + let b_has_messages = cx_b.read_with(|cx| { + client_b + .channel_store() + .read(cx) + .has_new_messages(channel_id) + .unwrap() + }); + + assert!(b_has_messages); + + // Closing the chat should re-enable change tracking + cx_b.update(|_| drop(chat_panel_b)); + + channel_chat_a + .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap()) + .await + .unwrap(); + + deterministic.run_until_parked(); + let b_has_messages = cx_b.read_with(|cx| { client_b .channel_store() diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 2050ee2f69..f0874f544e 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -273,7 +273,19 @@ impl ChatPanel { new_count, } => { self.message_list.splice(old_range.clone(), *new_count); - self.acknowledge_last_message(cx); + if self.active { + self.acknowledge_last_message(cx); + } + } + ChannelChatEvent::NewMessage { + channel_id, + message_id, + } => { + if !self.active { + self.channel_store.update(cx, |store, cx| { + store.new_message(*channel_id, *message_id, cx) + }) + } } } cx.notify(); From dd0edcd20336ce83bb01728b13e324a958bd07a1 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 11:46:08 -0700 Subject: [PATCH 44/67] Changed the on-click behavior of joining a channel to not open the chat, and only open 1 project instead of all projects Co-authored-by: conrad Co-authored-by: max --- crates/call/src/room.rs | 12 +++++------- crates/collab_ui/src/collab_panel.rs | 25 +++++++------------------ 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ce01099576..72db174d72 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -598,9 +598,8 @@ 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)> { + /// Returns the most 'active' projects, defined as most people in the project + pub fn most_active_project(&self) -> Option<(u64, u64)> { let mut projects = HashMap::default(); let mut hosts = HashMap::default(); for participant in self.remote_participants.values() { @@ -617,12 +616,11 @@ impl Room { } let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect(); - pairs.sort_by_key(|(_, count)| 0 - *count as i32); + pairs.sort_by_key(|(_, count)| *count as i32); pairs - .into_iter() - .map(|(project_id, _)| (project_id, hosts[&project_id])) - .collect() + .first() + .map(|(project_id, _)| (*project_id, hosts[&project_id])) } async fn handle_room_updated( diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 6214eecac2..8933067109 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3208,27 +3208,16 @@ impl CollabPanel { .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 { - 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() + let task = room.update(&mut cx, |room, cx| { + let workspace = workspace.upgrade(cx)?; + let (project, host) = room.most_active_project()?; + let app_state = workspace.read(cx).app_state().clone(); + Some(workspace::join_remote_project(project, host, app_state, cx)) }); - for task in tasks { + if let Some(task) = task { task.await?; } + anyhow::Ok(()) }) .detach_and_log_err(cx); From 4d61d01943f9c1e406d86359dbdf2aca3f0ebe86 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 11:46:41 -0700 Subject: [PATCH 45/67] Add an RPC handler for channel buffer acks co-authored-by: max --- crates/channel/src/channel.rs | 2 +- crates/channel/src/channel_buffer.rs | 2 +- crates/collab/src/rpc.rs | 25 ++++++++++++++++--- .../collab/src/tests/channel_buffer_tests.rs | 21 +++++++++++----- crates/collab/src/tests/test_server.rs | 16 +++++++++++- crates/collab_ui/src/collab_panel.rs | 2 -- 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 724ff75d60..160b8441ff 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -2,7 +2,7 @@ mod channel_buffer; mod channel_chat; mod channel_store; -pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent}; +pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId}; pub use channel_store::{ Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore, diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a097cc5467..7de8b956f1 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -11,7 +11,7 @@ use rpc::{ use std::{sync::Arc, time::Duration}; use util::ResultExt; -const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250); +pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250); pub(crate) fn init(client: &Arc) { client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1f0ecce2bf..6171803341 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3,8 +3,8 @@ mod connection_pool; use crate::{ auth, db::{ - self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User, - UserId, + self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, + ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -275,7 +275,8 @@ impl Server { .add_message_handler(update_followers) .add_message_handler(update_diff_base) .add_request_handler(get_private_user_info) - .add_message_handler(acknowledge_channel_message); + .add_message_handler(acknowledge_channel_message) + .add_message_handler(acknowledge_buffer_version); Arc::new(server) } @@ -2912,6 +2913,24 @@ async fn acknowledge_channel_message( Ok(()) } +async fn acknowledge_buffer_version( + request: proto::AckBufferOperation, + session: Session, +) -> Result<()> { + let buffer_id = BufferId::from_proto(request.buffer_id); + session + .db() + .await + .observe_buffer_version( + buffer_id, + session.user_id, + request.epoch as i32, + &request.version, + ) + .await?; + Ok(()) +} + async fn join_channel_chat( request: proto::JoinChannelChat, response: Response, diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 82bd7e6afe..a0b9b52484 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -3,7 +3,7 @@ use crate::{ tests::TestServer, }; use call::ActiveCall; -use channel::Channel; +use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL}; use client::ParticipantIndex; use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; @@ -800,7 +800,6 @@ async fn test_channel_buffer_changes( .has_channel_buffer_changed(channel_id) .unwrap() }); - assert!(has_buffer_changed); // Opening the buffer should clear the changed flag. @@ -810,7 +809,6 @@ async fn test_channel_buffer_changes( .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) .await .unwrap(); - deterministic.run_until_parked(); let has_buffer_changed = cx_b.read(|cx| { @@ -820,10 +818,9 @@ async fn test_channel_buffer_changes( .has_channel_buffer_changed(channel_id) .unwrap() }); - assert!(!has_buffer_changed); - // Editing the channel while the buffer is open shuold not show that the buffer has changed. + // Editing the channel while the buffer is open should not show that the buffer has changed. channel_buffer_a.update(cx_a, |buffer, cx| { buffer.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "2")], None, cx); @@ -838,7 +835,20 @@ async fn test_channel_buffer_changes( .has_channel_buffer_changed(channel_id) .unwrap() }); + assert!(!has_buffer_changed); + deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + + // Test that the server is tracking things correctly, and we retain our 'not changed' + // state across a disconnect + server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); assert!(!has_buffer_changed); // Closing the buffer should re-enable change tracking @@ -866,7 +876,6 @@ async fn test_channel_buffer_changes( .has_channel_buffer_changed(channel_id) .unwrap() }); - assert!(has_buffer_changed); } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index cf5b58703c..e10ded7d95 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -1,7 +1,7 @@ use crate::{ db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, - rpc::{Server, CLEANUP_TIMEOUT}, + rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, AppState, }; use anyhow::anyhow; @@ -17,6 +17,7 @@ use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHan use language::LanguageRegistry; use parking_lot::Mutex; use project::{Project, WorktreeId}; +use rpc::RECEIVE_TIMEOUT; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -255,6 +256,19 @@ impl TestServer { .store(true, SeqCst); } + pub fn simulate_long_connection_interruption( + &self, + peer_id: PeerId, + deterministic: &Arc, + ) { + self.forbid_connections(); + self.disconnect_client(peer_id); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + self.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + deterministic.run_until_parked(); + } + pub fn forbid_connections(&self) { self.forbid_connections.store(true, SeqCst); } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8933067109..39543c8def 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2760,11 +2760,9 @@ impl CollabPanel { .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 { From 6db47478cfa20587ffc471076df932e7eaac8e47 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 4 Oct 2023 15:00:53 -0400 Subject: [PATCH 46/67] v0.108.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35f3285ebd..7c49477db8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10063,7 +10063,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.107.0" +version = "0.108.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bd6a85e3aa..d4ac972a5d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.107.0" +version = "0.108.0" publish = false [lib] From 6cb674a0aac9c45d879ae0cee1c19a3b4e98fc30 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 4 Oct 2023 15:01:38 -0400 Subject: [PATCH 47/67] collab 0.23.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c49477db8..96f7aa99e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,7 +1467,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.22.1" +version = "0.23.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index ecc4b57b12..a2731320bc 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.22.1" +version = "0.23.0" publish = false [[bin]] From ff1722d3070fbd828b4ae7d0de0d9ccc5080601d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Oct 2023 14:29:13 -0600 Subject: [PATCH 48/67] Fix tracking newly saved buffers Co-Authored-By: Mikayla Maki --- crates/language/src/buffer.rs | 2 +- crates/project/src/project.rs | 28 +++++++++- crates/project_panel/src/project_panel.rs | 68 ++++++++++++++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 27b01543e1..207c41e7cd 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -660,12 +660,12 @@ impl Buffer { file_changed = true; }; + self.file = Some(new_file); if file_changed { self.file_update_count += 1; cx.emit(Event::FileHandleChanged); cx.notify(); } - self.file = Some(new_file); task } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 96037e9b20..a50e02a631 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1841,6 +1841,7 @@ impl Project { Worktree::Remote(_) => panic!("cannot remote buffers as new files"), }) .await?; + this.update(&mut cx, |this, cx| { this.detect_language_for_buffer(&buffer, cx); this.register_buffer_with_language_servers(&buffer, cx); @@ -2368,7 +2369,30 @@ impl Project { } } } + BufferEvent::FileHandleChanged => { + let Some(file) = File::from_dyn(buffer.read(cx).file()) else { + return None; + }; + match self.local_buffer_ids_by_entry_id.get(&file.entry_id) { + Some(_) => { + return None; + } + None => { + let remote_id = buffer.read(cx).remote_id(); + self.local_buffer_ids_by_entry_id + .insert(file.entry_id, remote_id); + + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); + } + } + } _ => {} } @@ -5906,7 +5930,9 @@ impl Project { Some(&buffer_id) => buffer_id, None => match self.local_buffer_ids_by_path.get(&project_path) { Some(&buffer_id) => buffer_id, - None => continue, + None => { + continue; + } }, }; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3263649562..d66de1ad2e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1737,7 +1737,7 @@ mod tests { use settings::SettingsStore; use std::{ collections::HashSet, - path::Path, + path::{Path, PathBuf}, sync::atomic::{self, AtomicUsize}, }; use workspace::{pane, AppState}; @@ -2759,6 +2759,71 @@ mod tests { ); } + #[gpui::test] + async fn test_new_file_move(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.as_fake().insert_tree("/root", json!({})).await; + let project = Project::test(fs, ["/root".as_ref()], cx).await; + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + + // Make a new buffer with no backing file + workspace.update(cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }); + + // "Save as"" the buffer, creating a new backing file for it + let task = workspace.update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }); + + cx.foreground().run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); + task.await.unwrap(); + + // Rename the file + select_path(&panel, "root/new", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " new <== selected"] + ); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("newer", cx)); + }); + panel + .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) + .unwrap() + .await + .unwrap(); + + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " newer <== selected"] + ); + + workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }) + .await + .unwrap(); + + cx.foreground().run_until_parked(); + // assert that saving the file doesn't restore "new" + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " newer <== selected"] + ); + } + fn toggle_expand_dir( panel: &ViewHandle, path: impl AsRef, @@ -2862,6 +2927,7 @@ mod tests { editor::init_settings(cx); crate::init((), cx); workspace::init_settings(cx); + client::init_settings(cx); Project::init_settings(cx); }); } From 5074bccae4fbdcb25c02920a2c43cbfc4c447a96 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 14:04:02 -0700 Subject: [PATCH 49/67] Add image avatars to channel messages --- crates/collab_ui/src/chat_panel.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index f0874f544e..8959fb6932 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -2,7 +2,7 @@ use crate::{channel_view::ChannelView, ChatPanelSettings}; use anyhow::Result; use call::ActiveCall; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; -use client::Client; +use client::{Client, UserStore}; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; @@ -35,6 +35,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel"; pub struct ChatPanel { client: Arc, channel_store: ModelHandle, + user_store: ModelHandle, active_chat: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, @@ -78,6 +79,7 @@ impl ChatPanel { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = workspace.app_state().channel_store.clone(); + let user_store = workspace.app_state().user_store.clone(); let input_editor = cx.add_view(|cx| { let mut editor = Editor::auto_height( @@ -130,6 +132,7 @@ impl ChatPanel { fs, client, channel_store, + user_store, active_chat: Default::default(), pending_serialization: Task::ready(None), message_list, @@ -352,6 +355,27 @@ impl ChatPanel { Flex::column() .with_child( Flex::row() + .with_child( + message + .sender + .avatar + .clone() + .map(|avatar| { + Image::from_data(avatar) + .with_style(theme.collab_panel.channel_avatar) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width( + theme.collab_panel.channel_avatar.width.unwrap_or(12.), + ) + .into_any() + }) + .contained() + .with_margin_right(4.), + ) .with_child( Label::new( message.sender.github_login.clone(), @@ -386,7 +410,8 @@ impl ChatPanel { this.remove_message(id, cx); }) .flex_float() - })), + })) + .align_children_center(), ) .with_child(Text::new(body, style.body.clone())) .contained() From a4e77af571f720ca4cc1e1f17cd66eb1f69f1470 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Oct 2023 15:13:01 -0600 Subject: [PATCH 50/67] Fix panic in increment --- crates/vim/src/normal/increment.rs | 14 ++++++++++---- crates/vim/test_data/test_increment_radix.json | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index d7f3d1f904..9d62f8ab7b 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -78,10 +78,14 @@ fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) { 2 => format!("{:b}", result), _ => unreachable!(), }; - if selection.is_empty() { - new_anchors.push((false, snapshot.anchor_after(range.end))) - } - edits.push((range, replace)); + edits.push((range.clone(), replace)); + } + if selection.is_empty() { + new_anchors.push((false, snapshot.anchor_after(range.end))) + } + } else { + if selection.is_empty() { + new_anchors.push((true, snapshot.anchor_after(start))) } } } @@ -226,6 +230,8 @@ mod test { cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)") .await; cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await; + cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana") + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_increment_radix.json b/crates/vim/test_data/test_increment_radix.json index f9379a7195..0f41c01599 100644 --- a/crates/vim/test_data/test_increment_radix.json +++ b/crates/vim/test_data/test_increment_radix.json @@ -13,3 +13,6 @@ {"Put":{"state":"ˇ-1"}} {"Key":"ctrl-a"} {"Get":{"state":"ˇ0","mode":"Normal"}} +{"Put":{"state":"banˇana"}} +{"Key":"ctrl-a"} +{"Get":{"state":"banˇana","mode":"Normal"}} From f7cd0e84f9c8dbf2373b13d2e751caa92656c0cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Oct 2023 15:18:26 -0600 Subject: [PATCH 51/67] Remove old code from notes icon click handler --- crates/collab_ui/src/collab_panel.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 39543c8def..c21435e093 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2086,13 +2086,7 @@ impl CollabPanel { } }) .on_click(MouseButton::Left, move |_, this, cx| { - let participants = - this.channel_store.read(cx).channel_participants(channel_id); - if is_active || participants.is_empty() { - this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - } else { - this.join_channel(channel_id, cx); - }; + this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); }), ) .align_children_center() From 73e78a2257c46c5f9c636824f8b193d7743e3bf0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 14:29:08 -0700 Subject: [PATCH 52/67] Adjust channel rendering to group related messages --- crates/collab_ui/src/chat_panel.rs | 178 +++++++++++++++++----------- crates/theme/src/theme.rs | 2 + styles/src/style_tree/chat_panel.ts | 14 ++- 3 files changed, 126 insertions(+), 68 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 8959fb6932..41bc5fbd08 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -2,7 +2,7 @@ use crate::{channel_view::ChannelView, ChatPanelSettings}; use anyhow::Result; use call::ActiveCall; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; -use client::{Client, UserStore}; +use client::Client; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; @@ -35,7 +35,6 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel"; pub struct ChatPanel { client: Arc, channel_store: ModelHandle, - user_store: ModelHandle, active_chat: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, @@ -79,7 +78,6 @@ impl ChatPanel { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = workspace.app_state().channel_store.clone(); - let user_store = workspace.app_state().user_store.clone(); let input_editor = cx.add_view(|cx| { let mut editor = Editor::auto_height( @@ -132,7 +130,7 @@ impl ChatPanel { fs, client, channel_store, - user_store, + active_chat: Default::default(), pending_serialization: Task::ready(None), message_list, @@ -331,12 +329,26 @@ impl ChatPanel { } fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix); + let (message, is_continuation, is_last) = { + let active_chat = self.active_chat.as_ref().unwrap().0.read(cx); + let last_message = active_chat.message(ix.saturating_sub(1)); + let this_message = active_chat.message(ix); + let is_continuation = last_message.id != this_message.id + && this_message.sender.id == last_message.sender.id; + + ( + active_chat.message(ix), + is_continuation, + active_chat.message_count() == ix + 1, + ) + }; let now = OffsetDateTime::now_utc(); let theme = theme::current(cx); let style = if message.is_pending() { &theme.chat_panel.pending_message + } else if is_continuation { + &theme.chat_panel.continuation_message } else { &theme.chat_panel.message }; @@ -352,71 +364,103 @@ impl ChatPanel { enum DeleteMessage {} let body = message.body.clone(); - Flex::column() - .with_child( - Flex::row() - .with_child( - message - .sender - .avatar - .clone() - .map(|avatar| { - Image::from_data(avatar) - .with_style(theme.collab_panel.channel_avatar) - .into_any() - }) - .unwrap_or_else(|| { - Empty::new() - .constrained() - .with_width( - theme.collab_panel.channel_avatar.width.unwrap_or(12.), - ) - .into_any() - }) + if is_continuation { + Flex::row() + .with_child(Text::new(body, style.body.clone())) + .with_children(message_id_to_remove.map(|id| { + MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { + let button_style = theme.chat_panel.icon_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .into_any() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_message(id, cx); + }) + .flex_float() + })) + .contained() + .with_style(style.container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. + }) + .into_any() + } else { + Flex::column() + .with_child( + Flex::row() + .with_child( + message + .sender + .avatar + .clone() + .map(|avatar| { + Image::from_data(avatar) + .with_style(theme.collab_panel.channel_avatar) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width( + theme.collab_panel.channel_avatar.width.unwrap_or(12.), + ) + .into_any() + }) + .contained() + .with_margin_right(4.), + ) + .with_child( + Label::new( + message.sender.github_login.clone(), + style.sender.text.clone(), + ) .contained() - .with_margin_right(4.), - ) - .with_child( - Label::new( - message.sender.github_login.clone(), - style.sender.text.clone(), + .with_style(style.sender.container), ) - .contained() - .with_style(style.sender.container), - ) - .with_child( - Label::new( - format_timestamp(message.timestamp, now, self.local_timezone), - style.timestamp.text.clone(), + .with_child( + Label::new( + format_timestamp(message.timestamp, now, self.local_timezone), + style.timestamp.text.clone(), + ) + .contained() + .with_style(style.timestamp.container), ) - .contained() - .with_style(style.timestamp.container), - ) - .with_children(message_id_to_remove.map(|id| { - MouseEventHandler::new::( - id as usize, - cx, - |mouse_state, _| { - let button_style = - theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() - }, - ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - })) - .align_children_center(), - ) - .with_child(Text::new(body, style.body.clone())) - .contained() - .with_style(style.container) - .into_any() + .with_children(message_id_to_remove.map(|id| { + MouseEventHandler::new::( + id as usize, + cx, + |mouse_state, _| { + let button_style = + theme.chat_panel.icon_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .into_any() + }, + ) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_message(id, cx); + }) + .flex_float() + })) + .align_children_center(), + ) + .with_child(Text::new(body, style.body.clone())) + .contained() + .with_style(style.container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. + }) + .into_any() + } } fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9b7c0309c6..63241668c4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -635,6 +635,8 @@ pub struct ChatPanel { pub channel_select: ChannelSelect, pub input_editor: FieldEditor, pub message: ChatMessage, + pub continuation_message: ChatMessage, + pub last_message_bottom_spacing: f32, pub pending_message: ChatMessage, pub sign_in_prompt: Interactive, pub icon_button: Interactive, diff --git a/styles/src/style_tree/chat_panel.ts b/styles/src/style_tree/chat_panel.ts index 9efa084456..466d25f43d 100644 --- a/styles/src/style_tree/chat_panel.ts +++ b/styles/src/style_tree/chat_panel.ts @@ -87,7 +87,19 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", { weight: "bold" }), }, timestamp: text(layer, "sans", "base", "disabled"), - margin: { bottom: SPACING } + margin: { top: SPACING } + }, + last_message_bottom_spacing: SPACING, + continuation_message: { + body: text(layer, "sans", "base"), + sender: { + margin: { + right: 8, + }, + ...text(layer, "sans", "base", { weight: "bold" }), + }, + timestamp: text(layer, "sans", "base", "disabled"), + }, pending_message: { body: text(layer, "sans", "base"), From d09767a90b0903585a99fece6b3d9d115908ee11 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Oct 2023 14:38:59 -0700 Subject: [PATCH 53/67] Ensure chat messages are retrieved in order of id --- crates/collab/src/db/queries.rs | 10 ----- crates/collab/src/db/queries/messages.rs | 49 +----------------------- 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 59face1f33..80bd8704b2 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -9,13 +9,3 @@ pub mod projects; pub mod rooms; pub mod servers; pub mod users; - -fn max_assign(max: &mut Option, val: T) { - if let Some(max_val) = max { - if val > *max_val { - *max = Some(val); - } - } else { - *max = Some(val); - } -} diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index db1252230e..83b5382cf5 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -89,17 +89,14 @@ impl Database { let mut rows = channel_message::Entity::find() .filter(condition) + .order_by_asc(channel_message::Column::Id) .limit(count as u64) .stream(&*tx) .await?; - let mut max_id = None; let mut messages = Vec::new(); while let Some(row) = rows.next().await { let row = row?; - - max_assign(&mut max_id, row.id); - let nonce = row.nonce.as_u64_pair(); messages.push(proto::ChannelMessage { id: row.id.to_proto(), @@ -113,50 +110,6 @@ impl Database { }); } drop(rows); - - if let Some(max_id) = max_id { - let has_older_message = observed_channel_messages::Entity::find() - .filter( - observed_channel_messages::Column::UserId - .eq(user_id) - .and(observed_channel_messages::Column::ChannelId.eq(channel_id)) - .and(observed_channel_messages::Column::ChannelMessageId.lt(max_id)), - ) - .one(&*tx) - .await? - .is_some(); - - if has_older_message { - observed_channel_messages::Entity::update( - observed_channel_messages::ActiveModel { - user_id: ActiveValue::Unchanged(user_id), - channel_id: ActiveValue::Unchanged(channel_id), - channel_message_id: ActiveValue::Set(max_id), - }, - ) - .exec(&*tx) - .await?; - } else { - observed_channel_messages::Entity::insert( - observed_channel_messages::ActiveModel { - user_id: ActiveValue::Set(user_id), - channel_id: ActiveValue::Set(channel_id), - channel_message_id: ActiveValue::Set(max_id), - }, - ) - .on_conflict( - OnConflict::columns([ - observed_channel_messages::Column::UserId, - observed_channel_messages::Column::ChannelId, - ]) - .update_columns([observed_channel_messages::Column::ChannelMessageId]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - } - Ok(messages) }) .await From 2f3c3d510fbcae4413dad5611db68ea63a3f2aa5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 14:43:58 -0700 Subject: [PATCH 54/67] Fix hit boxes and hover styles for new buttons co-authored-by: conrad --- crates/collab_ui/src/collab_panel.rs | 35 ++++++++++++++++++++----- styles/src/style_tree/component_test.ts | 1 + 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 39543c8def..7bcaa5be66 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1937,6 +1937,8 @@ impl CollabPanel { is_dragged_over = true; } + let has_messages_notification = channel.unseen_message_id.is_some(); + MouseEventHandler::new::(ix, cx, |state, cx| { let row_hovered = state.hovered(); @@ -2022,24 +2024,33 @@ impl CollabPanel { .flex(1., true) }) .with_child( - MouseEventHandler::new::(ix, cx, move |_, _| { + MouseEventHandler::new::(ix, cx, move |mouse_state, _| { + let container_style = collab_theme + .disclosure + .button + .style_for(mouse_state) + .container; + if channel.unseen_message_id.is_some() { Svg::new("icons/conversations.svg") .with_color(collab_theme.channel_note_active_color) .constrained() .with_width(collab_theme.channel_hash.width) + .contained() + .with_style(container_style) + .with_uniform_padding(4.) .into_any() } else if row_hovered { Svg::new("icons/conversations.svg") .with_color(collab_theme.channel_hash.color) .constrained() .with_width(collab_theme.channel_hash.width) + .contained() + .with_style(container_style) + .with_uniform_padding(4.) .into_any() } else { - Empty::new() - .constrained() - .with_width(collab_theme.channel_hash.width) - .into_any() + Empty::new().into_any() } }) .on_click(MouseButton::Left, move |_, this, cx| { @@ -2056,7 +2067,12 @@ impl CollabPanel { .with_margin_right(4.), ) .with_child( - MouseEventHandler::new::(ix, cx, move |_, cx| { + MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { + let container_style = collab_theme + .disclosure + .button + .style_for(mouse_state) + .container; if row_hovered || channel.unseen_note_version.is_some() { Svg::new("icons/file.svg") .with_color(if channel.unseen_note_version.is_some() { @@ -2067,6 +2083,8 @@ impl CollabPanel { .constrained() .with_width(collab_theme.channel_hash.width) .contained() + .with_style(container_style) + .with_uniform_padding(4.) .with_margin_right(collab_theme.channel_hash.container.margin.left) .with_tooltip::( ix as usize, @@ -2076,13 +2094,16 @@ impl CollabPanel { cx, ) .into_any() - } else { + } else if has_messages_notification { Empty::new() .constrained() .with_width(collab_theme.channel_hash.width) .contained() + .with_uniform_padding(4.) .with_margin_right(collab_theme.channel_hash.container.margin.left) .into_any() + } else { + Empty::new().into_any() } }) .on_click(MouseButton::Left, move |_, this, cx| { diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index 71057c67ea..8dc22eec31 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -21,6 +21,7 @@ export default function contacts_panel(): any { ...text(theme.lowest, "sans", "base"), button: icon_button({ variant: "ghost" }), spacing: 4, + padding: 4, }, } } From df2fa87e6b601933be79c72f1b58194d8dd44807 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Oct 2023 15:12:17 -0700 Subject: [PATCH 55/67] collab 0.23.1 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96f7aa99e7..b5c1dee79d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,7 +1467,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.23.0" +version = "0.23.1" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a2731320bc..0ede6483bc 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.23.0" +version = "0.23.1" publish = false [[bin]] From d298afba01583668c769f78f14421a15e109537b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 4 Oct 2023 17:47:30 -0700 Subject: [PATCH 56/67] Create markdown text element and add to channel chat --- Cargo.lock | 19 + Cargo.toml | 1 + crates/channel/src/channel_chat.rs | 2 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/chat_panel.rs | 53 ++- crates/markdown_element/Cargo.toml | 30 ++ .../markdown_element/src/markdown_element.rs | 339 ++++++++++++++++++ 7 files changed, 439 insertions(+), 6 deletions(-) create mode 100644 crates/markdown_element/Cargo.toml create mode 100644 crates/markdown_element/src/markdown_element.rs diff --git a/Cargo.lock b/Cargo.lock index b5c1dee79d..3eebf5e3d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,6 +1558,7 @@ dependencies = [ "gpui", "language", "log", + "markdown_element", "menu", "picker", "postage", @@ -4323,6 +4324,24 @@ dependencies = [ "libc", ] +[[package]] +name = "markdown_element" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui", + "language", + "lazy_static", + "pulldown-cmark", + "smallvec", + "smol", + "sum_tree", + "theme", + "util", +] + [[package]] name = "matchers" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 05a013a4e0..bc04baa17c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "crates/lsp", "crates/media", "crates/menu", + "crates/markdown_element", "crates/node_runtime", "crates/outline", "crates/picker", diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 29a260ea7e..734182886b 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -36,7 +36,7 @@ pub struct ChannelMessage { pub nonce: u128, } -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ChannelMessageId { Saved(u64), Pending(usize), diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index b6e45471f1..613ad4c7f5 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } +markdown_element = { path = "../markdown_element" } picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 41bc5fbd08..0dbcde31ed 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -3,6 +3,7 @@ use anyhow::Result; use call::ActiveCall; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; use client::Client; +use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; @@ -15,7 +16,8 @@ use gpui::{ AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::language_settings::SoftWrap; +use language::{language_settings::SoftWrap, LanguageRegistry}; +use markdown_element::{MarkdownData, MarkdownElement}; use menu::Confirm; use project::Fs; use serde::{Deserialize, Serialize}; @@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel"; pub struct ChatPanel { client: Arc, channel_store: ModelHandle, + languages: Arc, active_chat: Option<(ModelHandle, Subscription)>, message_list: ListState, input_editor: ViewHandle, @@ -47,6 +50,7 @@ pub struct ChatPanel { subscriptions: Vec, workspace: WeakViewHandle, has_focus: bool, + markdown_data: HashMap>, } #[derive(Serialize, Deserialize)] @@ -78,6 +82,7 @@ impl ChatPanel { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let channel_store = workspace.app_state().channel_store.clone(); + let languages = workspace.app_state().languages.clone(); let input_editor = cx.add_view(|cx| { let mut editor = Editor::auto_height( @@ -130,6 +135,7 @@ impl ChatPanel { fs, client, channel_store, + languages, active_chat: Default::default(), pending_serialization: Task::ready(None), @@ -142,6 +148,7 @@ impl ChatPanel { workspace: workspace_handle, active: false, width: None, + markdown_data: Default::default(), }; let mut old_dock_position = this.position(cx); @@ -178,6 +185,25 @@ impl ChatPanel { }) .detach(); + let markdown = this.languages.language_for_name("Markdown"); + cx.spawn(|this, mut cx| async move { + let markdown = markdown.await?; + + this.update(&mut cx, |this, cx| { + this.input_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multi_buffer, cx| { + multi_buffer + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx)) + }) + }) + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + this }) } @@ -328,7 +354,7 @@ impl ChatPanel { messages.flex(1., true).into_any() } - fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { let (message, is_continuation, is_last) = { let active_chat = self.active_chat.as_ref().unwrap().0.read(cx); let last_message = active_chat.message(ix.saturating_sub(1)); @@ -343,6 +369,13 @@ impl ChatPanel { ) }; + let markdown = self.markdown_data.entry(message.id).or_insert_with(|| { + Arc::new(markdown_element::render_markdown( + message.body.clone(), + &self.languages, + )) + }); + let now = OffsetDateTime::now_utc(); let theme = theme::current(cx); let style = if message.is_pending() { @@ -363,10 +396,14 @@ impl ChatPanel { enum DeleteMessage {} - let body = message.body.clone(); if is_continuation { Flex::row() - .with_child(Text::new(body, style.body.clone())) + .with_child(MarkdownElement::new( + markdown.clone(), + style.body.clone(), + theme.editor.syntax.clone(), + theme.editor.document_highlight_read_background, + )) .with_children(message_id_to_remove.map(|id| { MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { let button_style = theme.chat_panel.icon_button.style_for(mouse_state); @@ -451,7 +488,12 @@ impl ChatPanel { })) .align_children_center(), ) - .with_child(Text::new(body, style.body.clone())) + .with_child(MarkdownElement::new( + markdown.clone(), + style.body.clone(), + theme.editor.syntax.clone(), + theme.editor.document_highlight_read_background, + )) .contained() .with_style(style.container) .with_margin_bottom(if is_last { @@ -634,6 +676,7 @@ impl ChatPanel { cx.spawn(|this, mut cx| async move { let chat = open_chat.await?; this.update(&mut cx, |this, cx| { + this.markdown_data = Default::default(); this.set_active_chat(chat, cx); }) }) diff --git a/crates/markdown_element/Cargo.toml b/crates/markdown_element/Cargo.toml new file mode 100644 index 0000000000..920a3c3005 --- /dev/null +++ b/crates/markdown_element/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "markdown_element" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/markdown_element.rs" +doctest = false + +[features] +test-support = [ + "gpui/test-support", + "util/test-support", +] + + +[dependencies] +collections = { path = "../collections" } +gpui = { path = "../gpui" } +sum_tree = { path = "../sum_tree" } +theme = { path = "../theme" } +language = { path = "../language" } +util = { path = "../util" } +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } +smallvec.workspace = true +smol.workspace = true diff --git a/crates/markdown_element/src/markdown_element.rs b/crates/markdown_element/src/markdown_element.rs new file mode 100644 index 0000000000..66011d2a25 --- /dev/null +++ b/crates/markdown_element/src/markdown_element.rs @@ -0,0 +1,339 @@ +use std::{ops::Range, sync::Arc}; + +use futures::FutureExt; +use gpui::{ + color::Color, + elements::Text, + fonts::{HighlightStyle, TextStyle, Underline, Weight}, + platform::{CursorStyle, MouseButton}, + AnyElement, CursorRegion, Element, MouseRegion, +}; +use language::{HighlightId, Language, LanguageRegistry}; +use theme::SyntaxTheme; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Highlight { + Id(HighlightId), + Highlight(HighlightStyle), +} + +#[derive(Debug, Clone)] +pub struct MarkdownData { + text: String, + highlights: Vec<(Range, Highlight)>, + region_ranges: Vec>, + regions: Vec, +} + +#[derive(Debug, Clone)] +struct RenderedRegion { + code: bool, + link_url: Option, +} + +pub struct MarkdownElement { + data: Arc, + syntax: Arc, + style: TextStyle, + code_span_background_color: Color, +} + +impl MarkdownElement { + pub fn new( + data: Arc, + style: TextStyle, + syntax: Arc, + code_span_background_color: Color, + ) -> Self { + Self { + data, + style, + syntax, + code_span_background_color, + } + } +} + +impl Element for MarkdownElement { + type LayoutState = AnyElement; + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + view: &mut V, + cx: &mut gpui::ViewContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + let mut region_id = 0; + let view_id = cx.view_id(); + + let code_span_background_color = self.code_span_background_color; + let data = self.data.clone(); + let mut element = Text::new(self.data.text.clone(), self.style.clone()) + .with_highlights( + self.data + .highlights + .iter() + .filter_map(|(range, highlight)| { + let style = match highlight { + Highlight::Id(id) => id.style(&self.syntax)?, + Highlight::Highlight(style) => style.clone(), + }; + Some((range.clone(), style)) + }) + .collect::>(), + ) + .with_custom_runs(self.data.region_ranges.clone(), move |ix, bounds, cx| { + region_id += 1; + let region = data.regions[ix].clone(); + if let Some(url) = region.link_url { + cx.scene().push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + cx.scene().push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + cx.scene().push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) + .into_any(); + + let constraint = element.layout(constraint, view, cx); + + (constraint, element) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + view: &mut V, + cx: &mut gpui::ViewContext, + ) -> Self::PaintState { + layout.paint(bounds.origin(), visible_bounds, view, cx); + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: gpui::geometry::rect::RectF, + _: gpui::geometry::rect::RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &gpui::ViewContext, + ) -> Option { + layout.rect_for_text_range(range_utf16, view, cx) + } + + fn debug( + &self, + _: gpui::geometry::rect::RectF, + layout: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &gpui::ViewContext, + ) -> gpui::serde_json::Value { + layout.debug(view, cx) + } +} + +pub fn render_markdown(block: String, language_registry: &Arc) -> MarkdownData { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&block, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code(&mut text, &mut highlights, t.as_ref(), language); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if let Some(link_url) = link_url.clone() { + region_ranges.push(prev_len..text.len()); + regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len + && last_style == &Highlight::Highlight(style) + { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), Highlight::Highlight(style))); + } + } + } + } + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + if link_url.is_some() { + highlights.push(( + prev_len..text.len(), + Highlight::Highlight(HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }), + )); + } + regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); + } + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + Tag::Heading(_, _, _) => { + new_paragraph(&mut text, &mut list_stack); + bold_depth += 1; + } + Tag::CodeBlock(kind) => { + new_paragraph(&mut text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + None + } + } + Tag::Emphasis => italic_depth += 1, + Tag::Strong => bold_depth += 1, + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + Tag::List(number) => { + list_stack.push((number, false)); + } + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + _ => {} + }, + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + Event::HardBreak => text.push('\n'), + Event::SoftBreak => text.push(' '), + _ => {} + } + } + + MarkdownData { + text: text.trim().to_string(), + highlights, + region_ranges, + regions, + } +} + +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, Highlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + highlights.push(( + prev_len + range.start..prev_len + range.end, + Highlight::Id(highlight_id), + )); + } +} + +fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} From f1c743286dc0f6697e5a3a334283e4cfad0c17a0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 5 Oct 2023 09:02:52 -0600 Subject: [PATCH 57/67] Clear SelectionGoal on input --- crates/editor/src/editor.rs | 8 +++++++- crates/vim/src/test.rs | 17 +++++++++++++++++ crates/vim/test_data/test_selection_goal.json | 8 ++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 crates/vim/test_data/test_selection_goal.json diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e0b8af1c71..24ffa64a6a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2454,7 +2454,13 @@ impl Editor { let snapshot = this.buffer.read(cx).read(cx); let new_selections = resolve_multiple::(new_anchor_selections, &snapshot) .zip(new_selection_deltas) - .map(|(selection, delta)| selection.map(|e| e + delta)) + .map(|(selection, delta)| Selection { + id: selection.id, + start: selection.start + delta, + end: selection.end + delta, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) .collect::>(); let mut i = 0; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 82e4cc6863..52dcb54ce2 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -635,3 +635,20 @@ async fn test_zero(cx: &mut gpui::TestAppContext) { the lazy dog"}) .await; } + +#[gpui::test] +async fn test_selection_goal(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ;;ˇ; + Lorem Ipsum"}) + .await; + + cx.simulate_shared_keystrokes(["a", "down", "up", ";", "down", "up"]) + .await; + cx.assert_shared_state(indoc! {" + ;;;;ˇ + Lorem Ipsum"}) + .await; +} diff --git a/crates/vim/test_data/test_selection_goal.json b/crates/vim/test_data/test_selection_goal.json new file mode 100644 index 0000000000..ada2ab4c8e --- /dev/null +++ b/crates/vim/test_data/test_selection_goal.json @@ -0,0 +1,8 @@ +{"Put":{"state":";;ˇ;\nLorem Ipsum"}} +{"Key":"a"} +{"Key":"down"} +{"Key":"up"} +{"Key":";"} +{"Key":"down"} +{"Key":"up"} +{"Get":{"state":";;;;ˇ\nLorem Ipsum","mode":"Insert"}} From 44ada5218512de81c0539eb9e28f379b65bc7862 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 5 Oct 2023 11:06:29 -0700 Subject: [PATCH 58/67] Fix bug where chat text wouldn't wrap to width --- crates/collab_ui/src/chat_panel.rs | 23 ++++++++++------------- crates/collab_ui/src/collab_panel.rs | 6 +----- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 0dbcde31ed..d85c3ce934 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -398,12 +398,16 @@ impl ChatPanel { if is_continuation { Flex::row() - .with_child(MarkdownElement::new( - markdown.clone(), - style.body.clone(), - theme.editor.syntax.clone(), - theme.editor.document_highlight_read_background, - )) + .with_child( + Flex::column() + .with_child(MarkdownElement::new( + markdown.clone(), + style.body.clone(), + theme.editor.syntax.clone(), + theme.editor.document_highlight_read_background, + )) + .flex(1., true), + ) .with_children(message_id_to_remove.map(|id| { MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { let button_style = theme.chat_panel.icon_button.style_for(mouse_state); @@ -418,13 +422,6 @@ impl ChatPanel { }) .flex_float() })) - .contained() - .with_style(style.container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) .into_any() } else { Flex::column() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 66913c2da7..951c8bf70c 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1976,11 +1976,7 @@ impl CollabPanel { .left() .with_tooltip::( ix, - if is_active { - "Open channel notes" - } else { - "Join channel" - }, + "Join channel", None, theme.tooltip.clone(), cx, From 84ea34f918ec3ac0665f807684dde8f42c164f04 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 5 Oct 2023 14:50:48 -0400 Subject: [PATCH 59/67] Add session id --- crates/client/src/telemetry.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 38a4115ddd..0f753679e1 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -8,6 +8,7 @@ use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; +use uuid::Uuid; pub struct Telemetry { http_client: Arc, @@ -18,7 +19,8 @@ pub struct Telemetry { #[derive(Default)] struct TelemetryState { metrics_id: Option>, // Per logged-in user - installation_id: Option>, // Per app installation + installation_id: Option>, // Per app installation (different for dev, preview, and stable) + session_id: String, // Per app launch app_version: Option>, release_channel: Option<&'static str>, os_name: &'static str, @@ -41,6 +43,7 @@ lazy_static! { struct ClickhouseEventRequestBody { token: &'static str, installation_id: Option>, + session_id: String, is_staff: Option, app_version: Option>, os_name: &'static str, @@ -131,6 +134,7 @@ impl Telemetry { release_channel, installation_id: None, metrics_id: None, + session_id: Uuid::new_v4().to_string(), clickhouse_events_queue: Default::default(), flush_clickhouse_events_task: Default::default(), log_file: None, @@ -285,6 +289,7 @@ impl Telemetry { &ClickhouseEventRequestBody { token: ZED_SECRET_CLIENT_TOKEN, installation_id: state.installation_id.clone(), + session_id: state.session_id.clone(), is_staff: state.is_staff.clone(), app_version: state.app_version.clone(), os_name: state.os_name, From f57d563578c6d1e30e3d8b1a7ad523c997cc465c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 5 Oct 2023 11:58:41 -0700 Subject: [PATCH 60/67] Improve chat rendering --- crates/collab_ui/src/chat_panel.rs | 249 ++++++++++++++++------------ crates/theme/src/theme.rs | 4 +- styles/src/style_tree/chat_panel.ts | 69 +++++++- 3 files changed, 213 insertions(+), 109 deletions(-) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index d85c3ce934..0d60e7e155 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -13,8 +13,8 @@ use gpui::{ platform::{CursorStyle, MouseButton}, serde_json, views::{ItemType, Select, SelectStyle}, - AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{language_settings::SoftWrap, LanguageRegistry}; use markdown_element::{MarkdownData, MarkdownElement}; @@ -363,22 +363,23 @@ impl ChatPanel { && this_message.sender.id == last_message.sender.id; ( - active_chat.message(ix), + active_chat.message(ix).clone(), is_continuation, active_chat.message_count() == ix + 1, ) }; + let is_pending = message.is_pending(); let markdown = self.markdown_data.entry(message.id).or_insert_with(|| { Arc::new(markdown_element::render_markdown( - message.body.clone(), + message.body, &self.languages, )) }); let now = OffsetDateTime::now_utc(); let theme = theme::current(cx); - let style = if message.is_pending() { + let style = if is_pending { &theme.chat_panel.pending_message } else if is_continuation { &theme.chat_panel.continuation_message @@ -394,112 +395,90 @@ impl ChatPanel { None }; - enum DeleteMessage {} - - if is_continuation { - Flex::row() - .with_child( - Flex::column() - .with_child(MarkdownElement::new( + enum MessageBackgroundHighlight {} + MouseEventHandler::new::(ix, cx, |state, cx| { + let container = style.container.style_for(state); + if is_continuation { + Flex::row() + .with_child( + MarkdownElement::new( markdown.clone(), style.body.clone(), theme.editor.syntax.clone(), theme.editor.document_highlight_read_background, - )) + ) .flex(1., true), - ) - .with_children(message_id_to_remove.map(|id| { - MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { - let button_style = theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() + ) + .with_child(render_remove(message_id_to_remove, cx, &theme)) + .contained() + .with_style(*container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - })) - .into_any() - } else { - Flex::column() - .with_child( - Flex::row() - .with_child( - message - .sender - .avatar - .clone() - .map(|avatar| { - Image::from_data(avatar) - .with_style(theme.collab_panel.channel_avatar) - .into_any() - }) - .unwrap_or_else(|| { - Empty::new() - .constrained() - .with_width( - theme.collab_panel.channel_avatar.width.unwrap_or(12.), + .into_any() + } else { + Flex::column() + .with_child( + Flex::row() + .with_child( + Flex::row() + .with_child(render_avatar( + message.sender.avatar.clone(), + &theme, + )) + .with_child( + Label::new( + message.sender.github_login.clone(), + style.sender.text.clone(), ) - .into_any() - }) - .contained() - .with_margin_right(4.), - ) - .with_child( - Label::new( - message.sender.github_login.clone(), - style.sender.text.clone(), + .contained() + .with_style(style.sender.container), + ) + .with_child( + Label::new( + format_timestamp( + message.timestamp, + now, + self.local_timezone, + ), + style.timestamp.text.clone(), + ) + .contained() + .with_style(style.timestamp.container), + ) + .align_children_center() + .flex(1., true), ) - .contained() - .with_style(style.sender.container), - ) - .with_child( - Label::new( - format_timestamp(message.timestamp, now, self.local_timezone), - style.timestamp.text.clone(), + .with_child(render_remove(message_id_to_remove, cx, &theme)) + .align_children_center(), + ) + .with_child( + Flex::row() + .with_child( + MarkdownElement::new( + markdown.clone(), + style.body.clone(), + theme.editor.syntax.clone(), + theme.editor.document_highlight_read_background, + ) + .flex(1., true), ) - .contained() - .with_style(style.timestamp.container), - ) - .with_children(message_id_to_remove.map(|id| { - MouseEventHandler::new::( - id as usize, - cx, - |mouse_state, _| { - let button_style = - theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() - }, - ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - })) - .align_children_center(), - ) - .with_child(MarkdownElement::new( - markdown.clone(), - style.body.clone(), - theme.editor.syntax.clone(), - theme.editor.document_highlight_read_background, - )) - .contained() - .with_style(style.container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } + // Add a spacer to make everything line up + .with_child(render_remove(None, cx, &theme)), + ) + .contained() + .with_style(*container) + .with_margin_bottom(if is_last { + theme.chat_panel.last_message_bottom_spacing + } else { + 0. + }) + .into_any() + } + }) + .into_any() } fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { @@ -698,6 +677,72 @@ impl ChatPanel { } } +fn render_avatar(avatar: Option>, theme: &Arc) -> AnyElement { + let avatar_style = theme.chat_panel.avatar; + + avatar + .map(|avatar| { + Image::from_data(avatar) + .with_style(avatar_style.image) + .aligned() + .contained() + .with_corner_radius(avatar_style.outer_corner_radius) + .constrained() + .with_width(avatar_style.outer_width) + .with_height(avatar_style.outer_width) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width(avatar_style.outer_width) + .into_any() + }) + .contained() + .with_style(theme.chat_panel.avatar_container) + .into_any() +} + +fn render_remove( + message_id_to_remove: Option, + cx: &mut ViewContext<'_, '_, ChatPanel>, + theme: &Arc, +) -> AnyElement { + enum DeleteMessage {} + + message_id_to_remove + .map(|id| { + MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { + let button_style = theme.chat_panel.icon_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x.svg") + .aligned() + .into_any() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_message(id, cx); + }) + .flex_float() + .into_any() + }) + .unwrap_or_else(|| { + let style = theme.chat_panel.icon_button.default; + + Empty::new() + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_uniform_padding(2.) + .flex_float() + .into_any() + }) +} + impl Entity for ChatPanel { type Event = Event; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 63241668c4..e534ba4260 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -634,6 +634,8 @@ pub struct ChatPanel { pub list: ContainerStyle, pub channel_select: ChannelSelect, pub input_editor: FieldEditor, + pub avatar: AvatarStyle, + pub avatar_container: ContainerStyle, pub message: ChatMessage, pub continuation_message: ChatMessage, pub last_message_bottom_spacing: f32, @@ -645,7 +647,7 @@ pub struct ChatPanel { #[derive(Deserialize, Default, JsonSchema)] pub struct ChatMessage { #[serde(flatten)] - pub container: ContainerStyle, + pub container: Interactive, pub body: TextStyle, pub sender: ContainedText, pub timestamp: ContainedText, diff --git a/styles/src/style_tree/chat_panel.ts b/styles/src/style_tree/chat_panel.ts index 466d25f43d..829540de30 100644 --- a/styles/src/style_tree/chat_panel.ts +++ b/styles/src/style_tree/chat_panel.ts @@ -5,6 +5,7 @@ import { } from "./components" import { icon_button } from "../component/icon_button" import { useTheme } from "../theme" +import { interactive } from "../element" export default function chat_panel(): any { const theme = useTheme() @@ -27,11 +28,23 @@ export default function chat_panel(): any { return { background: background(layer), - list: { - margin: { - left: SPACING, - right: SPACING, + avatar: { + icon_width: 24, + icon_height: 24, + corner_radius: 4, + outer_width: 24, + outer_corner_radius: 16, + }, + avatar_container: { + padding: { + right: 6, + left: 2, + top: 2, + bottom: 2, } + }, + list: { + }, channel_select: { header: { @@ -79,6 +92,22 @@ export default function chat_panel(): any { }, }, message: { + ...interactive({ + base: { + margin: { top: SPACING }, + padding: { + top: 4, + bottom: 4, + left: SPACING / 2, + right: SPACING / 3, + } + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), body: text(layer, "sans", "base"), sender: { margin: { @@ -87,7 +116,6 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", { weight: "bold" }), }, timestamp: text(layer, "sans", "base", "disabled"), - margin: { top: SPACING } }, last_message_bottom_spacing: SPACING, continuation_message: { @@ -99,7 +127,21 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", { weight: "bold" }), }, timestamp: text(layer, "sans", "base", "disabled"), - + ...interactive({ + base: { + padding: { + top: 4, + bottom: 4, + left: SPACING / 2, + right: SPACING / 3, + } + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), }, pending_message: { body: text(layer, "sans", "base"), @@ -110,6 +152,21 @@ export default function chat_panel(): any { ...text(layer, "sans", "base", "disabled"), }, timestamp: text(layer, "sans", "base"), + ...interactive({ + base: { + padding: { + top: 4, + bottom: 4, + left: SPACING / 2, + right: SPACING / 3, + } + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), }, sign_in_prompt: { default: text(layer, "sans", "base"), From 438dd42f7dfa408a7815ed2a50b44fb76e5ebddf Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 13:28:46 -0700 Subject: [PATCH 61/67] Fix bugs in handling mutual following * Propagate all of leader's views to a new follower, even if those views were originally created by that follower. * Propagate active view changes to followers, even if the active view is following that follower. * Avoid redundant active view updates on the client. --- crates/collab/src/rpc.rs | 16 +- crates/collab/src/tests/following_tests.rs | 549 ++++++++++++++++++--- crates/workspace/src/workspace.rs | 18 +- 3 files changed, 491 insertions(+), 92 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6171803341..5eb434e167 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1906,13 +1906,10 @@ async fn follow( .check_room_participants(room_id, leader_id, session.connection_id) .await?; - let mut response_payload = session + let response_payload = session .peer .forward_request(session.connection_id, leader_id, request) .await?; - response_payload - .views - .retain(|view| view.leader_id != Some(follower_id.into())); response.send(response_payload)?; if let Some(project_id) = project_id { @@ -1973,14 +1970,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> .await? }; - let leader_id = request.variant.as_ref().and_then(|variant| match variant { - proto::update_followers::Variant::CreateView(payload) => payload.leader_id, + // For now, don't send view update messages back to that view's current leader. + let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant { proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, - proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id, + _ => None, }); + for follower_peer_id in request.follower_ids.iter().copied() { let follower_connection_id = follower_peer_id.into(); - if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) { + if Some(follower_peer_id) != connection_id_to_omit + && connection_ids.contains(&follower_connection_id) + { session.peer.forward_send( session.connection_id, follower_connection_id, diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 6d374b7920..3a489b9ac3 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; +use rpc::proto::PeerId; use serde_json::json; use std::{borrow::Cow, sync::Arc}; use workspace::{ @@ -724,10 +725,9 @@ async fn test_peers_following_each_other( .await .unwrap(); - // Client A opens some editors. + // Client A opens a file. let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let _editor_a1 = workspace_a + workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), None, true, cx) }) @@ -736,10 +736,9 @@ async fn test_peers_following_each_other( .downcast::() .unwrap(); - // Client B opens an editor. + // Client B opens a different file. let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let _editor_b1 = workspace_b + workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), None, true, cx) }) @@ -754,9 +753,7 @@ async fn test_peers_following_each_other( }); workspace_a .update(cx_a, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_a1); - let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(leader_id, cx).unwrap() + workspace.follow(client_b.peer_id().unwrap(), cx).unwrap() }) .await .unwrap(); @@ -765,85 +762,443 @@ async fn test_peers_following_each_other( }); workspace_b .update(cx_b, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_b1); - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(leader_id, cx).unwrap() + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() }) .await .unwrap(); - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_next_pane(cx); - }); - // Wait for focus effects to be fully flushed - workspace_a.update(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); + // Clients A and B return focus to the original files they had open + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.run_until_parked(); + // Both clients see the other client's focused file in their right pane. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(true, "1.txt".into())] + }, + PaneSummary { + active: false, + leader: client_b.peer_id(), + items: vec![(false, "1.txt".into()), (true, "2.txt".into())] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(true, "2.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![(false, "2.txt".into()), (true, "1.txt".into())] + }, + ] + ); + + // Clients A and B each open a new file. workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "3.txt"), None, true, cx) }) .await .unwrap(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_next_pane(cx); - }); workspace_b .update(cx_b, |workspace, cx| { - assert_eq!(*workspace.active_pane(), pane_b1); workspace.open_path((worktree_id, "4.txt"), None, true, cx) }) .await .unwrap(); - cx_a.foreground().run_until_parked(); + deterministic.run_until_parked(); - // Ensure leader updates don't change the active pane of followers - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_b1); - }); - - // Ensure peers following each other doesn't cause an infinite loop. + // Both client's see the other client open the new file, but keep their + // focus on their own active pane. assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .project_path(cx)), - Some((worktree_id, "3.txt").into()) + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: false, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] ); - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - workspace.activate_next_pane(cx); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (true, "3.txt".into()) + ] + }, + ] + ); + + // Client A focuses their right pane, in which they're following client B. + workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.run_until_parked(); + + // Client B sees that client A is now looking at the same file as them. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: true, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: false, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (false, "3.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + + // Client B focuses their right pane, in which they're following client A, + // who is following them. + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + deterministic.run_until_parked(); + + // Client A sees that client B is now looking at the same file as them. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (false, "3.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()) + ] + }, + ] + ); + + // Client B focuses a file that they previously followed A to, breaking + // the follow. + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); }); + deterministic.run_until_parked(); + + // Both clients see that client B is looking at that previous file. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "2.txt".into()), + (false, "1.txt".into()), + (true, "3.txt".into()), + (false, "4.txt".into()) + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (true, "3.txt".into()), + ] + }, + ] + ); + + // Client B closes tabs, some of which were originally opened by client A, + // and some of which were originally opened by client B. + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_inactive_items(&Default::default(), cx) + .unwrap() + .detach(); + }); + }); + + deterministic.run_until_parked(); + + // Both clients see that Client B is looking at the previous tab. + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![(true, "3.txt".into()),] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: client_b.peer_id(), + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (true, "3.txt".into()), + ] + }, + ] + ); + + // Client B follows client A again. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Client A cycles through some tabs. + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); + }); + deterministic.run_until_parked(); + + // Client B follows client A into those tabs. + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "1.txt".into()), + (false, "2.txt".into()), + (true, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![(false, "3.txt".into()), (true, "4.txt".into())] + }, + ] + ); workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); }); + deterministic.run_until_parked(); - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - workspace.activate_next_pane(cx); - }); + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (false, "1.txt".into()), + (true, "2.txt".into()), + (false, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "3.txt".into()), + (false, "4.txt".into()), + (true, "2.txt".into()) + ] + }, + ] + ); - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); + workspace_a.update(cx_a, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.activate_prev_item(true, cx); + }); }); + deterministic.run_until_parked(); + + assert_eq!( + pane_summaries(&workspace_a, cx_a), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "1.txt".into()), (true, "3.txt".into())] + }, + PaneSummary { + active: true, + leader: None, + items: vec![ + (true, "1.txt".into()), + (false, "2.txt".into()), + (false, "4.txt".into()), + (false, "3.txt".into()), + ] + }, + ] + ); + assert_eq!( + pane_summaries(&workspace_b, cx_b), + &[ + PaneSummary { + active: false, + leader: None, + items: vec![(false, "2.txt".into()), (true, "4.txt".into())] + }, + PaneSummary { + active: true, + leader: client_a.peer_id(), + items: vec![ + (false, "3.txt".into()), + (false, "4.txt".into()), + (false, "2.txt".into()), + (true, "1.txt".into()), + ] + }, + ] + ); } #[gpui::test(iterations = 10)] @@ -1074,24 +1429,6 @@ async fn test_peers_simultaneously_following_each_other( }); } -fn visible_push_notifications( - cx: &mut TestAppContext, -) -> Vec> { - let mut ret = Vec::new(); - for window in cx.windows() { - window.read_with(cx, |window| { - if let Some(handle) = window - .root_view() - .clone() - .downcast::() - { - ret.push(handle) - } - }); - } - ret -} - #[gpui::test(iterations = 10)] async fn test_following_across_workspaces( deterministic: Arc, @@ -1304,3 +1641,59 @@ async fn test_following_across_workspaces( assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); }); } + +fn visible_push_notifications( + cx: &mut TestAppContext, +) -> Vec> { + let mut ret = Vec::new(); + for window in cx.windows() { + window.read_with(cx, |window| { + if let Some(handle) = window + .root_view() + .clone() + .downcast::() + { + ret.push(handle) + } + }); + } + ret +} + +#[derive(Debug, PartialEq, Eq)] +struct PaneSummary { + active: bool, + leader: Option, + items: Vec<(bool, String)>, +} + +fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { + workspace.read_with(cx, |workspace, cx| { + let active_pane = workspace.active_pane(); + workspace + .panes() + .iter() + .map(|pane| { + let leader = workspace.leader_for_pane(pane); + let active = pane == active_pane; + let pane = pane.read(cx); + let active_ix = pane.active_item_index(); + PaneSummary { + active, + leader, + items: pane + .items() + .enumerate() + .map(|(ix, item)| { + ( + ix == active_ix, + item.tab_description(0, cx) + .map_or(String::new(), |s| s.to_string()), + ) + }) + .collect(), + } + }) + .collect() + }) +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f7bb409229..801c45fa89 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -573,6 +573,7 @@ pub struct Workspace { panes_by_item: HashMap>, active_pane: ViewHandle, last_active_center_pane: Option>, + last_active_view_id: Option, status_bar: ViewHandle, titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, @@ -786,6 +787,7 @@ impl Workspace { panes_by_item: Default::default(), active_pane: center_pane.clone(), last_active_center_pane: Some(center_pane.downgrade()), + last_active_view_id: None, status_bar, titlebar_item: None, notifications: Default::default(), @@ -2862,6 +2864,7 @@ impl Workspace { cx.notify(); + self.last_active_view_id = active_view_id.clone(); proto::FollowResponse { active_view_id, views: self @@ -3028,7 +3031,7 @@ impl Workspace { Ok(()) } - fn update_active_view_for_followers(&self, cx: &AppContext) { + fn update_active_view_for_followers(&mut self, cx: &AppContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); if self.active_pane.read(cx).has_focus() { @@ -3046,11 +3049,14 @@ impl Workspace { } } - self.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateActiveView(update), - cx, - ); + if update.id != self.last_active_view_id { + self.last_active_view_id = update.id.clone(); + self.update_followers( + is_project_item, + proto::update_followers::Variant::UpdateActiveView(update), + cx, + ); + } } fn update_followers( From c4870e1b6b257debdbbf25093fd3fb818531e6b5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 5 Oct 2023 14:19:13 -0700 Subject: [PATCH 62/67] re-unify markdown parsing between hover_popover and chat --- Cargo.lock | 39 +-- Cargo.toml | 2 +- crates/collab_ui/Cargo.toml | 2 +- crates/collab_ui/src/chat_panel.rs | 26 +- crates/editor/Cargo.toml | 1 + crates/editor/src/hover_popover.rs | 305 +++--------------- .../Cargo.toml | 4 +- .../src/rich_text.rs} | 204 +++++------- 8 files changed, 156 insertions(+), 427 deletions(-) rename crates/{markdown_element => rich_text}/Cargo.toml (90%) rename crates/{markdown_element/src/markdown_element.rs => rich_text/src/rich_text.rs} (62%) diff --git a/Cargo.lock b/Cargo.lock index 3eebf5e3d5..4443526775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,12 +1558,12 @@ dependencies = [ "gpui", "language", "log", - "markdown_element", "menu", "picker", "postage", "project", "recent_projects", + "rich_text", "schemars", "serde", "serde_derive", @@ -2406,6 +2406,7 @@ dependencies = [ "project", "pulldown-cmark", "rand 0.8.5", + "rich_text", "rpc", "schemars", "serde", @@ -4324,24 +4325,6 @@ dependencies = [ "libc", ] -[[package]] -name = "markdown_element" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.28", - "gpui", - "language", - "lazy_static", - "pulldown-cmark", - "smallvec", - "smol", - "sum_tree", - "theme", - "util", -] - [[package]] name = "matchers" version = "0.1.0" @@ -6261,6 +6244,24 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "rich_text" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui", + "language", + "lazy_static", + "pulldown-cmark", + "smallvec", + "smol", + "sum_tree", + "theme", + "util", +] + [[package]] name = "ring" version = "0.16.20" diff --git a/Cargo.toml b/Cargo.toml index bc04baa17c..7dae3bd81f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,6 @@ members = [ "crates/lsp", "crates/media", "crates/menu", - "crates/markdown_element", "crates/node_runtime", "crates/outline", "crates/picker", @@ -65,6 +64,7 @@ members = [ "crates/sqlez", "crates/sqlez_macros", "crates/feature_flags", + "crates/rich_text", "crates/storybook", "crates/sum_tree", "crates/terminal", diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 613ad4c7f5..98790778c9 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -37,7 +37,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } -markdown_element = { path = "../markdown_element" } +rich_text = { path = "../rich_text" } picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 0d60e7e155..b446521c5a 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -17,9 +17,9 @@ use gpui::{ View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{language_settings::SoftWrap, LanguageRegistry}; -use markdown_element::{MarkdownData, MarkdownElement}; use menu::Confirm; use project::Fs; +use rich_text::RichText; use serde::{Deserialize, Serialize}; use settings::SettingsStore; use std::sync::Arc; @@ -50,7 +50,7 @@ pub struct ChatPanel { subscriptions: Vec, workspace: WeakViewHandle, has_focus: bool, - markdown_data: HashMap>, + markdown_data: HashMap, } #[derive(Serialize, Deserialize)] @@ -370,12 +370,10 @@ impl ChatPanel { }; let is_pending = message.is_pending(); - let markdown = self.markdown_data.entry(message.id).or_insert_with(|| { - Arc::new(markdown_element::render_markdown( - message.body, - &self.languages, - )) - }); + let text = self + .markdown_data + .entry(message.id) + .or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None)); let now = OffsetDateTime::now_utc(); let theme = theme::current(cx); @@ -401,11 +399,11 @@ impl ChatPanel { if is_continuation { Flex::row() .with_child( - MarkdownElement::new( - markdown.clone(), - style.body.clone(), + text.element( theme.editor.syntax.clone(), + style.body.clone(), theme.editor.document_highlight_read_background, + cx, ) .flex(1., true), ) @@ -457,11 +455,11 @@ impl ChatPanel { .with_child( Flex::row() .with_child( - MarkdownElement::new( - markdown.clone(), - style.body.clone(), + text.element( theme.editor.syntax.clone(), + style.body.clone(), theme.editor.document_highlight_read_background, + cx, ) .flex(1., true), ) diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index b0f8323a76..2c3d6227a9 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -36,6 +36,7 @@ language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } rpc = { path = "../rpc" } +rich_text = { path = "../rich_text" } settings = { path = "../settings" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index f460b18bce..553cb321c3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -8,12 +8,12 @@ use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, - fonts::{HighlightStyle, Underline, Weight}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; +use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -346,158 +346,25 @@ fn show_hover( } fn render_blocks( - theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, language: Option<&Arc>, - style: &EditorStyle, -) -> RenderedInfo { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut region_ranges = Vec::new(); - let mut regions = Vec::new(); +) -> RichText { + let mut data = RichText { + text: Default::default(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }; for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut text, &mut Vec::new()); - text.push_str(&block.text); + new_paragraph(&mut data.text, &mut Vec::new()); + data.text.push_str(&block.text); } HoverBlockKind::Markdown => { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; - - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut link_url = None; - let mut current_language = None; - let mut list_stack = Vec::new(); - - for event in Parser::new_ext(&block.text, Options::all()) { - let prev_len = text.len(); - match event { - Event::Text(t) => { - if let Some(language) = ¤t_language { - render_code( - &mut text, - &mut highlights, - t.as_ref(), - language, - style, - ); - } else { - text.push_str(t.as_ref()); - - let mut style = HighlightStyle::default(); - if bold_depth > 0 { - style.weight = Some(Weight::BOLD); - } - if italic_depth > 0 { - style.italic = Some(true); - } - if let Some(link_url) = link_url.clone() { - region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { - link_url: Some(link_url), - code: false, - }); - style.underline = Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }); - } - - if style != HighlightStyle::default() { - let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == prev_len && last_style == &style { - last_range.end = text.len(); - new_highlight = false; - } - } - if new_highlight { - highlights.push((prev_len..text.len(), style)); - } - } - } - } - Event::Code(t) => { - text.push_str(t.as_ref()); - region_ranges.push(prev_len..text.len()); - if link_url.is_some() { - highlights.push(( - prev_len..text.len(), - HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }, - )); - } - regions.push(RenderedRegion { - code: true, - link_url: link_url.clone(), - }); - } - Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), - Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); - bold_depth += 1; - } - Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); - current_language = if let CodeBlockKind::Fenced(language) = kind { - language_registry - .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) - } else { - language.cloned() - } - } - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Link(_, url, _) => link_url = Some(url.to_string()), - Tag::List(number) => { - list_stack.push((number, false)); - } - Tag::Item => { - let len = list_stack.len(); - if let Some((list_number, has_content)) = list_stack.last_mut() { - *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - for _ in 0..len - 1 { - text.push_str(" "); - } - if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); - *number += 1; - *has_content = false; - } else { - text.push_str("- "); - } - } - } - _ => {} - }, - Event::End(tag) => match tag { - Tag::Heading(_, _, _) => bold_depth -= 1, - Tag::CodeBlock(_) => current_language = None, - Tag::Emphasis => italic_depth -= 1, - Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => link_url = None, - Tag::List(_) => drop(list_stack.pop()), - _ => {} - }, - Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push(' '), - _ => {} - } - } + render_markdown_mut(&block.text, language_registry, language, &mut data) } HoverBlockKind::Code { language } => { if let Some(language) = language_registry @@ -505,62 +372,17 @@ fn render_blocks( .now_or_never() .and_then(Result::ok) { - render_code(&mut text, &mut highlights, &block.text, &language, style); + render_code(&mut data.text, &mut data.highlights, &block.text, &language); } else { - text.push_str(&block.text); + data.text.push_str(&block.text); } } } } - RenderedInfo { - theme_id, - text: text.trim().to_string(), - highlights, - region_ranges, - regions, - } -} + data.text = data.text.trim().to_string(); -fn render_code( - text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, - content: &str, - language: &Arc, - style: &EditorStyle, -) { - let prev_len = text.len(); - text.push_str(content); - for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if let Some(style) = highlight_id.style(&style.syntax) { - highlights.push((prev_len + range.start..prev_len + range.end, style)); - } - } -} - -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { - let mut is_subsequent_paragraph_of_list = false; - if let Some((_, has_content)) = list_stack.last_mut() { - if *has_content { - is_subsequent_paragraph_of_list = true; - } else { - *has_content = true; - return; - } - } - - if !text.is_empty() { - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - } - for _ in 0..list_stack.len().saturating_sub(1) { - text.push_str(" "); - } - if is_subsequent_paragraph_of_list { - text.push_str(" "); - } + data } #[derive(Default)] @@ -623,22 +445,7 @@ pub struct InfoPopover { symbol_range: RangeInEditor, pub blocks: Vec, language: Option>, - rendered_content: Option, -} - -#[derive(Debug, Clone)] -struct RenderedInfo { - theme_id: usize, - text: String, - highlights: Vec<(Range, HighlightStyle)>, - region_ranges: Vec>, - regions: Vec, -} - -#[derive(Debug, Clone)] -struct RenderedRegion { - code: bool, - link_url: Option, + rendered_content: Option, } impl InfoPopover { @@ -647,63 +454,24 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - if let Some(rendered) = &self.rendered_content { - if rendered.theme_id != style.theme_id { - self.rendered_content = None; - } - } - let rendered_content = self.rendered_content.get_or_insert_with(|| { render_blocks( - style.theme_id, &self.blocks, self.project.read(cx).languages(), self.language.as_ref(), - style, ) }); - MouseEventHandler::new::(0, cx, |_, cx| { - let mut region_id = 0; - let view_id = cx.view_id(); - + MouseEventHandler::new::(0, cx, move |_, cx| { let code_span_background_color = style.document_highlight_read_background; - let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) - .with_child( - Text::new(rendered_content.text.clone(), style.text.clone()) - .with_highlights(rendered_content.highlights.clone()) - .with_custom_runs( - rendered_content.region_ranges.clone(), - move |ix, bounds, cx| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - cx.scene().push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - cx.scene().push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::( - MouseButton::Left, - move |_, _, cx| cx.platform().open_url(&url), - ), - ); - } - if region.code { - cx.scene().push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }, - ) - .with_soft_wrap(true), - ) + .with_child(rendered_content.element( + style.syntax.clone(), + style.text.clone(), + code_span_background_color, + cx, + )) .contained() .with_style(style.hover_popover.container) }) @@ -799,11 +567,12 @@ mod tests { InlayId, }; use collections::BTreeSet; - use gpui::fonts::Weight; + use gpui::fonts::{HighlightStyle, Underline, Weight}; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; + use rich_text::Highlight; use smol::stream::StreamExt; use unindent::Unindent; use util::test::marked_text_ranges; @@ -1014,7 +783,7 @@ mod tests { .await; cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, cx| { + cx.editor(|editor, _| { let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; assert_eq!( blocks, @@ -1024,8 +793,7 @@ mod tests { }], ); - let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(&blocks, &Default::default(), None); assert_eq!( rendered.text, code_str.trim(), @@ -1217,7 +985,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(&blocks, &Default::default(), None); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -1228,8 +996,21 @@ mod tests { rendered.text, expected_text, "wrong text for input {blocks:?}" ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let style = match highlight { + Highlight::Id(id) => id.style(&style.syntax)?, + Highlight::Highlight(style) => style.clone(), + }; + Some((range.clone(), style)) + }) + .collect(); + assert_eq!( - rendered.highlights, expected_highlights, + rendered_highlights, expected_highlights, "wrong highlights for input {blocks:?}" ); } diff --git a/crates/markdown_element/Cargo.toml b/crates/rich_text/Cargo.toml similarity index 90% rename from crates/markdown_element/Cargo.toml rename to crates/rich_text/Cargo.toml index 920a3c3005..3d2c25406d 100644 --- a/crates/markdown_element/Cargo.toml +++ b/crates/rich_text/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "markdown_element" +name = "rich_text" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/markdown_element.rs" +path = "src/rich_text.rs" doctest = false [features] diff --git a/crates/markdown_element/src/markdown_element.rs b/crates/rich_text/src/rich_text.rs similarity index 62% rename from crates/markdown_element/src/markdown_element.rs rename to crates/rich_text/src/rich_text.rs index 66011d2a25..72c7bdf6c1 100644 --- a/crates/markdown_element/src/markdown_element.rs +++ b/crates/rich_text/src/rich_text.rs @@ -6,94 +6,68 @@ use gpui::{ elements::Text, fonts::{HighlightStyle, TextStyle, Underline, Weight}, platform::{CursorStyle, MouseButton}, - AnyElement, CursorRegion, Element, MouseRegion, + AnyElement, CursorRegion, Element, MouseRegion, ViewContext, }; use language::{HighlightId, Language, LanguageRegistry}; use theme::SyntaxTheme; #[derive(Debug, Clone, PartialEq, Eq)] -enum Highlight { +pub enum Highlight { Id(HighlightId), Highlight(HighlightStyle), } #[derive(Debug, Clone)] -pub struct MarkdownData { - text: String, - highlights: Vec<(Range, Highlight)>, - region_ranges: Vec>, - regions: Vec, +pub struct RichText { + pub text: String, + pub highlights: Vec<(Range, Highlight)>, + pub region_ranges: Vec>, + pub regions: Vec, } #[derive(Debug, Clone)] -struct RenderedRegion { +pub struct RenderedRegion { code: bool, link_url: Option, } -pub struct MarkdownElement { - data: Arc, - syntax: Arc, - style: TextStyle, - code_span_background_color: Color, -} - -impl MarkdownElement { - pub fn new( - data: Arc, - style: TextStyle, +impl RichText { + pub fn element( + &self, syntax: Arc, + style: TextStyle, code_span_background_color: Color, - ) -> Self { - Self { - data, - style, - syntax, - code_span_background_color, - } - } -} - -impl Element for MarkdownElement { - type LayoutState = AnyElement; - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - view: &mut V, - cx: &mut gpui::ViewContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + cx: &mut ViewContext, + ) -> AnyElement { let mut region_id = 0; let view_id = cx.view_id(); - let code_span_background_color = self.code_span_background_color; - let data = self.data.clone(); - let mut element = Text::new(self.data.text.clone(), self.style.clone()) + let regions = self.regions.clone(); + + enum Markdown {} + Text::new(self.text.clone(), style.clone()) .with_highlights( - self.data - .highlights + self.highlights .iter() .filter_map(|(range, highlight)| { let style = match highlight { - Highlight::Id(id) => id.style(&self.syntax)?, + Highlight::Id(id) => id.style(&syntax)?, Highlight::Highlight(style) => style.clone(), }; Some((range.clone(), style)) }) .collect::>(), ) - .with_custom_runs(self.data.region_ranges.clone(), move |ix, bounds, cx| { + .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| { region_id += 1; - let region = data.regions[ix].clone(); + let region = regions[ix].clone(); if let Some(url) = region.link_url { cx.scene().push_cursor_region(CursorRegion { bounds, style: CursorStyle::PointingHand, }); cx.scene().push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) + MouseRegion::new::(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), @@ -109,55 +83,16 @@ impl Element for MarkdownElement { } }) .with_soft_wrap(true) - .into_any(); - - let constraint = element.layout(constraint, view, cx); - - (constraint, element) - } - - fn paint( - &mut self, - bounds: gpui::geometry::rect::RectF, - visible_bounds: gpui::geometry::rect::RectF, - layout: &mut Self::LayoutState, - view: &mut V, - cx: &mut gpui::ViewContext, - ) -> Self::PaintState { - layout.paint(bounds.origin(), visible_bounds, view, cx); - } - - fn rect_for_text_range( - &self, - range_utf16: std::ops::Range, - _: gpui::geometry::rect::RectF, - _: gpui::geometry::rect::RectF, - layout: &Self::LayoutState, - _: &Self::PaintState, - view: &V, - cx: &gpui::ViewContext, - ) -> Option { - layout.rect_for_text_range(range_utf16, view, cx) - } - - fn debug( - &self, - _: gpui::geometry::rect::RectF, - layout: &Self::LayoutState, - _: &Self::PaintState, - view: &V, - cx: &gpui::ViewContext, - ) -> gpui::serde_json::Value { - layout.debug(view, cx) + .into_any() } } -pub fn render_markdown(block: String, language_registry: &Arc) -> MarkdownData { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut region_ranges = Vec::new(); - let mut regions = Vec::new(); - +pub fn render_markdown_mut( + block: &str, + language_registry: &Arc, + language: Option<&Arc>, + data: &mut RichText, +) { use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; let mut bold_depth = 0; @@ -167,13 +102,13 @@ pub fn render_markdown(block: String, language_registry: &Arc) let mut list_stack = Vec::new(); for event in Parser::new_ext(&block, Options::all()) { - let prev_len = text.len(); + let prev_len = data.text.len(); match event { Event::Text(t) => { if let Some(language) = ¤t_language { - render_code(&mut text, &mut highlights, t.as_ref(), language); + render_code(&mut data.text, &mut data.highlights, t.as_ref(), language); } else { - text.push_str(t.as_ref()); + data.text.push_str(t.as_ref()); let mut style = HighlightStyle::default(); if bold_depth > 0 { @@ -183,8 +118,8 @@ pub fn render_markdown(block: String, language_registry: &Arc) style.italic = Some(true); } if let Some(link_url) = link_url.clone() { - region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { + data.region_ranges.push(prev_len..data.text.len()); + data.regions.push(RenderedRegion { link_url: Some(link_url), code: false, }); @@ -196,26 +131,27 @@ pub fn render_markdown(block: String, language_registry: &Arc) if style != HighlightStyle::default() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { + if let Some((last_range, last_style)) = data.highlights.last_mut() { if last_range.end == prev_len && last_style == &Highlight::Highlight(style) { - last_range.end = text.len(); + last_range.end = data.text.len(); new_highlight = false; } } if new_highlight { - highlights.push((prev_len..text.len(), Highlight::Highlight(style))); + data.highlights + .push((prev_len..data.text.len(), Highlight::Highlight(style))); } } } } Event::Code(t) => { - text.push_str(t.as_ref()); - region_ranges.push(prev_len..text.len()); + data.text.push_str(t.as_ref()); + data.region_ranges.push(prev_len..data.text.len()); if link_url.is_some() { - highlights.push(( - prev_len..text.len(), + data.highlights.push(( + prev_len..data.text.len(), Highlight::Highlight(HighlightStyle { underline: Some(Underline { thickness: 1.0.into(), @@ -225,26 +161,26 @@ pub fn render_markdown(block: String, language_registry: &Arc) }), )); } - regions.push(RenderedRegion { + data.regions.push(RenderedRegion { code: true, link_url: link_url.clone(), }); } Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack), Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); + new_paragraph(&mut data.text, &mut list_stack); bold_depth += 1; } Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); + new_paragraph(&mut data.text, &mut list_stack); current_language = if let CodeBlockKind::Fenced(language) = kind { language_registry .language_for_name(language.as_ref()) .now_or_never() .and_then(Result::ok) } else { - None + language.cloned() } } Tag::Emphasis => italic_depth += 1, @@ -257,18 +193,18 @@ pub fn render_markdown(block: String, language_registry: &Arc) let len = list_stack.len(); if let Some((list_number, has_content)) = list_stack.last_mut() { *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); + if !data.text.is_empty() && !data.text.ends_with('\n') { + data.text.push('\n'); } for _ in 0..len - 1 { - text.push_str(" "); + data.text.push_str(" "); } if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); + data.text.push_str(&format!("{}. ", number)); *number += 1; *has_content = false; } else { - text.push_str("- "); + data.text.push_str("- "); } } } @@ -283,21 +219,33 @@ pub fn render_markdown(block: String, language_registry: &Arc) Tag::List(_) => drop(list_stack.pop()), _ => {} }, - Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push(' '), + Event::HardBreak => data.text.push('\n'), + Event::SoftBreak => data.text.push(' '), _ => {} } } - - MarkdownData { - text: text.trim().to_string(), - highlights, - region_ranges, - regions, - } } -fn render_code( +pub fn render_markdown( + block: String, + language_registry: &Arc, + language: Option<&Arc>, +) -> RichText { + let mut data = RichText { + text: Default::default(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }; + + render_markdown_mut(&block, language_registry, language, &mut data); + + data.text = data.text.trim().to_string(); + + data +} + +pub fn render_code( text: &mut String, highlights: &mut Vec<(Range, Highlight)>, content: &str, @@ -313,7 +261,7 @@ fn render_code( } } -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { let mut is_subsequent_paragraph_of_list = false; if let Some((_, has_content)) = list_stack.last_mut() { if *has_content { From b3c9473bc835d636506f333ee7bc768187718b83 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 16:06:28 -0700 Subject: [PATCH 63/67] collab 0.23.2 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4443526775..c971846a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,7 +1467,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.23.1" +version = "0.23.2" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 0ede6483bc..0182129299 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.23.1" +version = "0.23.2" publish = false [[bin]] From 8fafae2cfafcea118d26a681ddd3a54db289996c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 16:17:49 -0700 Subject: [PATCH 64/67] Fix panic when immediately closing a window while opening paths --- crates/workspace/src/workspace.rs | 246 ++++++++++++++---------------- 1 file changed, 117 insertions(+), 129 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 801c45fa89..6496d81349 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -79,7 +79,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{async_iife, ResultExt}; +use util::ResultExt; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { @@ -936,7 +936,8 @@ impl Workspace { app_state, cx, ) - .await; + .await + .unwrap_or_default(); (workspace, opened_items) }) @@ -3400,140 +3401,124 @@ impl Workspace { serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, cx: &mut AppContext, - ) -> Task, anyhow::Error>>>> { + ) -> Task>>>> { cx.spawn(|mut cx| async move { - let result = async_iife! {{ - let (project, old_center_pane) = - workspace.read_with(&cx, |workspace, _| { - ( - workspace.project().clone(), - workspace.last_active_center_pane.clone(), - ) - })?; + let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { + ( + workspace.project().clone(), + workspace.last_active_center_pane.clone(), + ) + })?; - let mut center_items = None; - let mut center_group = None; - // Traverse the splits tree and add to things - if let Some((group, active_pane, items)) = serialized_workspace - .center_group - .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) - .await { - center_items = Some(items); - center_group = Some((group, active_pane)) + let mut center_group = None; + let mut center_items = None; + // Traverse the splits tree and add to things + if let Some((group, active_pane, items)) = serialized_workspace + .center_group + .deserialize(&project, serialized_workspace.id, &workspace, &mut cx) + .await + { + center_items = Some(items); + center_group = Some((group, active_pane)) + } + + let mut items_by_project_path = cx.read(|cx| { + center_items + .unwrap_or_default() + .into_iter() + .filter_map(|item| { + let item = item?; + let project_path = item.project_path(cx)?; + Some((project_path, item)) + }) + .collect::>() + }); + + let opened_items = paths_to_open + .into_iter() + .map(|path_to_open| { + path_to_open + .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) + }) + .collect::>(); + + // Remove old panes from workspace panes list + workspace.update(&mut cx, |workspace, cx| { + if let Some((center_group, active_pane)) = center_group { + workspace.remove_panes(workspace.center.root.clone(), cx); + + // Swap workspace center group + workspace.center = PaneGroup::with_root(center_group); + + // Change the focus to the workspace first so that we retrigger focus in on the pane. + cx.focus_self(); + + if let Some(active_pane) = active_pane { + cx.focus(&active_pane); + } else { + cx.focus(workspace.panes.last().unwrap()); + } + } else { + let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); + if let Some(old_center_handle) = old_center_handle { + cx.focus(&old_center_handle) + } else { + cx.focus_self() + } } - let resulting_list = cx.read(|cx| { - let mut opened_items = center_items - .unwrap_or_default() - .into_iter() - .filter_map(|item| { - let item = item?; - let project_path = item.project_path(cx)?; - Some((project_path, item)) - }) - .collect::>(); - - paths_to_open - .into_iter() - .map(|path_to_open| { - path_to_open.map(|path_to_open| { - Ok(opened_items.remove(&path_to_open)) - }) - .transpose() - .map(|item| item.flatten()) - .transpose() - }) - .collect::>() - }); - - // Remove old panes from workspace panes list - workspace.update(&mut cx, |workspace, cx| { - if let Some((center_group, active_pane)) = center_group { - workspace.remove_panes(workspace.center.root.clone(), cx); - - // Swap workspace center group - workspace.center = PaneGroup::with_root(center_group); - - // Change the focus to the workspace first so that we retrigger focus in on the pane. - cx.focus_self(); - - if let Some(active_pane) = active_pane { - cx.focus(&active_pane); - } else { - cx.focus(workspace.panes.last().unwrap()); + let docks = serialized_workspace.docks; + workspace.left_dock.update(cx, |dock, cx| { + dock.set_open(docks.left.visible, cx); + if let Some(active_panel) = docks.left.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); } - } else { - let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx)); - if let Some(old_center_handle) = old_center_handle { - cx.focus(&old_center_handle) - } else { - cx.focus_self() + } + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.left.zoom, cx)); + if docks.left.visible && docks.left.zoom { + cx.focus_self() + } + }); + // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something + workspace.right_dock.update(cx, |dock, cx| { + dock.set_open(docks.right.visible, cx); + if let Some(active_panel) = docks.right.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.right.zoom, cx)); + + if docks.right.visible && docks.right.zoom { + cx.focus_self() + } + }); + workspace.bottom_dock.update(cx, |dock, cx| { + dock.set_open(docks.bottom.visible, cx); + if let Some(active_panel) = docks.bottom.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); } } - let docks = serialized_workspace.docks; - workspace.left_dock.update(cx, |dock, cx| { - dock.set_open(docks.left.visible, cx); - if let Some(active_panel) = docks.left.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } - dock.active_panel() - .map(|panel| { - panel.set_zoomed(docks.left.zoom, cx) - }); - if docks.left.visible && docks.left.zoom { - cx.focus_self() - } - }); - // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something - workspace.right_dock.update(cx, |dock, cx| { - dock.set_open(docks.right.visible, cx); - if let Some(active_panel) = docks.right.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); + dock.active_panel() + .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx)); - } - } - dock.active_panel() - .map(|panel| { - panel.set_zoomed(docks.right.zoom, cx) - }); + if docks.bottom.visible && docks.bottom.zoom { + cx.focus_self() + } + }); - if docks.right.visible && docks.right.zoom { - cx.focus_self() - } - }); - workspace.bottom_dock.update(cx, |dock, cx| { - dock.set_open(docks.bottom.visible, cx); - if let Some(active_panel) = docks.bottom.active_panel { - if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { - dock.activate_panel(ix, cx); - } - } + cx.notify(); + })?; - dock.active_panel() - .map(|panel| { - panel.set_zoomed(docks.bottom.zoom, cx) - }); + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated + workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; - if docks.bottom.visible && docks.bottom.zoom { - cx.focus_self() - } - }); - - - cx.notify(); - })?; - - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?; - - Ok::<_, anyhow::Error>(resulting_list) - }}; - - result.await.unwrap_or_default() + Ok(opened_items) }) } @@ -3607,7 +3592,7 @@ async fn open_items( mut project_paths_to_open: Vec<(PathBuf, Option)>, app_state: Arc, mut cx: AsyncAppContext, -) -> Vec>>> { +) -> Result>>>> { let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); if let Some(serialized_workspace) = serialized_workspace { @@ -3625,16 +3610,19 @@ async fn open_items( cx, ) }) - .await; + .await?; let restored_project_paths = cx.read(|cx| { restored_items .iter() - .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx)) + .filter_map(|item| item.as_ref()?.project_path(cx)) .collect::>() }); - opened_items = restored_items; + for restored_item in restored_items { + opened_items.push(restored_item.map(Ok)); + } + project_paths_to_open .iter_mut() .for_each(|(_, project_path)| { @@ -3687,7 +3675,7 @@ async fn open_items( } } - opened_items + Ok(opened_items) } fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { From b391f5615b0e0826122004751751fd94e5a65883 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:43:03 +0200 Subject: [PATCH 65/67] rust: Highlight async functions in completions (#3095) Before (code in screenshot is from this branch, `crates/zed/languages/rust.rs:179`): ![image](https://github.com/zed-industries/zed/assets/24362066/6b709f8c-1b80-4aaa-8ddc-8db9dbca5a5e) Notice how the last 2 entries (that are async functions) are not highlighted properly. After: ![image](https://github.com/zed-industries/zed/assets/24362066/88337f43-b97f-4257-9c31-54c9023e8dbb) This is slightly suboptimal, as it's hard to tell that this is an async function - I guess adding an `async` prefix is not really an option, as then we should have a prefix for non-async functions too. Still, at least you can tell that something is a function in the first place. :) Release Notes: - Fixed Rust async functions not being highlighted in completions. --- crates/zed/src/languages/rust.rs | 51 +++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 62bdddab5b..dc2697ab19 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -165,17 +165,25 @@ impl LspAdapter for RustLspAdapter { lazy_static! { static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap(); } - let detail = completion.detail.as_ref().unwrap(); - if detail.starts_with("fn(") { - let text = REGEX.replace(&completion.label, &detail[2..]).to_string(); - let source = Rope::from(format!("fn {} {{}}", text).as_str()); - let runs = language.highlight_text(&source, 3..3 + text.len()); - return Some(CodeLabel { - filter_range: 0..completion.label.find('(').unwrap_or(text.len()), - text, - runs, - }); + const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"]; + let prefix = FUNCTION_PREFIXES + .iter() + .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix))); + // fn keyword should be followed by opening parenthesis. + if let Some((prefix, suffix)) = prefix { + if suffix.starts_with('(') { + let text = REGEX.replace(&completion.label, suffix).to_string(); + let source = Rope::from(format!("{prefix} {} {{}}", text).as_str()); + let run_start = prefix.len() + 1; + let runs = + language.highlight_text(&source, run_start..run_start + text.len()); + return Some(CodeLabel { + filter_range: 0..completion.label.find('(').unwrap_or(text.len()), + text, + runs, + }); + } } } Some(kind) => { @@ -377,7 +385,28 @@ mod tests { ], }) ); - + assert_eq!( + language + .label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "hello(…)".to_string(), + detail: Some("async fn(&mut Option) -> Vec".to_string()), + ..Default::default() + }) + .await, + Some(CodeLabel { + text: "hello(&mut Option) -> Vec".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (7..10, highlight_keyword), + (11..17, highlight_type), + (18..19, highlight_type), + (25..28, highlight_type), + (29..30, highlight_type), + ], + }) + ); assert_eq!( language .label_for_completion(&lsp::CompletionItem { From c46137e40da3ba688870f28008f554eae51d2d64 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:50:29 +0200 Subject: [PATCH 66/67] chore: Upgrade to Rust 1.73 (#3096) Release Notes: - N/A --- Dockerfile | 2 +- crates/language/src/buffer_tests.rs | 2 +- crates/ui/src/components/list.rs | 4 ++-- crates/ui/src/elements/details.rs | 2 +- rust-toolchain.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 208700f7fb..f3d0b601b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.72-bullseye as builder +FROM rust:1.73-bullseye as builder WORKDIR app COPY . . diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 3bedf5b7a8..b7e9001c6a 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1427,7 +1427,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex // Insert the block at column zero. The entire block is indented // so that the first line matches the previous line's indentation. buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], Some(AutoindentMode::Block { original_indent_columns: original_indent_columns.clone(), }), diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs index 8389c8c2c7..b7dff6b2c5 100644 --- a/crates/ui/src/components/list.rs +++ b/crates/ui/src/components/list.rs @@ -135,7 +135,7 @@ impl ListHeader { .size(IconSize::Small) })) .child( - Label::new(self.label.clone()) + Label::new(self.label) .color(LabelColor::Muted) .size(LabelSize::Small), ), @@ -191,7 +191,7 @@ impl ListSubHeader { .size(IconSize::Small) })) .child( - Label::new(self.label.clone()) + Label::new(self.label) .color(LabelColor::Muted) .size(LabelSize::Small), ), diff --git a/crates/ui/src/elements/details.rs b/crates/ui/src/elements/details.rs index d36b674291..9c829bcd41 100644 --- a/crates/ui/src/elements/details.rs +++ b/crates/ui/src/elements/details.rs @@ -27,7 +27,7 @@ impl Details { .gap_0p5() .text_xs() .text_color(theme.lowest.base.default.foreground) - .child(self.text.clone()) + .child(self.text) .children(self.meta.map(|m| m)) } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9720c52eaa..38d54d7aed 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.72.1" +channel = "1.73" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] From 456baaa112e8eb6b13fd031abd6818e61b97e812 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 6 Oct 2023 13:18:56 -0400 Subject: [PATCH 67/67] Mainline GPUI2 UI work (#3099) This PR mainlines the current state of new GPUI2-based UI from the `gpui2-ui` branch. Included in this is a performance improvement to make use of the `TextLayoutCache` when calling `layout` for `Text` elements. Release Notes: - N/A --------- Co-authored-by: Nate Butler Co-authored-by: Antonio Scandurra --- crates/gpui/src/app/window.rs | 6 +- crates/gpui2/src/elements/text.rs | 16 ++--- crates/storybook/src/stories/components.rs | 4 ++ .../stories/components/language_selector.rs | 16 +++++ .../src/stories/components/multi_buffer.rs | 24 +++++++ .../src/stories/components/recent_projects.rs | 16 +++++ .../src/stories/components/theme_selector.rs | 16 +++++ crates/storybook/src/story_selector.rs | 14 ++++ crates/ui/src/components.rs | 10 +++ crates/ui/src/components/language_selector.rs | 36 ++++++++++ crates/ui/src/components/multi_buffer.rs | 42 ++++++++++++ crates/ui/src/components/palette.rs | 20 ++++-- crates/ui/src/components/recent_projects.rs | 32 +++++++++ crates/ui/src/components/theme_selector.rs | 37 +++++++++++ crates/ui/src/components/toast.rs | 66 +++++++++++++++++++ crates/ui/src/components/workspace.rs | 13 ++++ crates/ui/src/elements/icon.rs | 2 + crates/ui/src/prelude.rs | 20 ++++++ 18 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 crates/storybook/src/stories/components/language_selector.rs create mode 100644 crates/storybook/src/stories/components/multi_buffer.rs create mode 100644 crates/storybook/src/stories/components/recent_projects.rs create mode 100644 crates/storybook/src/stories/components/theme_selector.rs create mode 100644 crates/ui/src/components/language_selector.rs create mode 100644 crates/ui/src/components/multi_buffer.rs create mode 100644 crates/ui/src/components/recent_projects.rs create mode 100644 crates/ui/src/components/theme_selector.rs create mode 100644 crates/ui/src/components/toast.rs diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 4eca6f3a30..7ba1e85100 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -71,7 +71,7 @@ pub struct Window { pub(crate) hovered_region_ids: Vec, pub(crate) clicked_region_ids: Vec, pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>, - text_layout_cache: TextLayoutCache, + text_layout_cache: Arc, refreshing: bool, } @@ -107,7 +107,7 @@ impl Window { cursor_regions: Default::default(), mouse_regions: Default::default(), event_handlers: Default::default(), - text_layout_cache: TextLayoutCache::new(cx.font_system.clone()), + text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())), last_mouse_moved_event: None, last_mouse_position: Vector2F::zero(), pressed_buttons: Default::default(), @@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> { self.window.refreshing } - pub fn text_layout_cache(&self) -> &TextLayoutCache { + pub fn text_layout_cache(&self) -> &Arc { &self.window.text_layout_cache } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 6f89375df0..323b3d9f89 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::Result; use gpui::{ geometry::{vector::Vector2F, Size}, - text_layout::LineLayout, + text_layout::Line, LayoutId, }; use parking_lot::Mutex; @@ -32,7 +32,7 @@ impl Element for Text { _view: &mut V, cx: &mut ViewContext, ) -> Result<(LayoutId, Self::PaintState)> { - let fonts = cx.platform().fonts(); + let layout_cache = cx.text_layout_cache().clone(); let text_style = cx.text_style(); let line_height = cx.font_cache().line_height(text_style.font_size); let text = self.text.clone(); @@ -41,14 +41,14 @@ impl Element for Text { let layout_id = cx.add_measured_layout_node(Default::default(), { let paint_state = paint_state.clone(); move |_params| { - let line_layout = fonts.layout_line( + let line_layout = layout_cache.layout_str( text.as_ref(), text_style.font_size, &[(text.len(), text_style.to_run())], ); let size = Size { - width: line_layout.width, + width: line_layout.width(), height: line_height, }; @@ -85,13 +85,9 @@ impl Element for Text { line_height = paint_state.line_height; } - let text_style = cx.text_style(); - let line = - gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]); - // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder. let visible_bounds = bounds; - line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx); + line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx); } } @@ -104,6 +100,6 @@ impl IntoElement for Text { } pub struct TextLayout { - line_layout: Arc, + line_layout: Arc, line_height: f32, } diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs index 345fcfa309..85d5ce088f 100644 --- a/crates/storybook/src/stories/components.rs +++ b/crates/storybook/src/stories/components.rs @@ -6,13 +6,17 @@ pub mod collab_panel; pub mod context_menu; pub mod facepile; pub mod keybinding; +pub mod language_selector; +pub mod multi_buffer; pub mod palette; pub mod panel; pub mod project_panel; +pub mod recent_projects; pub mod status_bar; pub mod tab; pub mod tab_bar; pub mod terminal; +pub mod theme_selector; pub mod title_bar; pub mod toolbar; pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/language_selector.rs b/crates/storybook/src/stories/components/language_selector.rs new file mode 100644 index 0000000000..c6dbd13d3f --- /dev/null +++ b/crates/storybook/src/stories/components/language_selector.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::LanguageSelector; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct LanguageSelectorStory {} + +impl LanguageSelectorStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, LanguageSelector>(cx)) + .child(Story::label(cx, "Default")) + .child(LanguageSelector::new()) + } +} diff --git a/crates/storybook/src/stories/components/multi_buffer.rs b/crates/storybook/src/stories/components/multi_buffer.rs new file mode 100644 index 0000000000..cd760c54dc --- /dev/null +++ b/crates/storybook/src/stories/components/multi_buffer.rs @@ -0,0 +1,24 @@ +use ui::prelude::*; +use ui::{hello_world_rust_buffer_example, MultiBuffer}; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct MultiBufferStory {} + +impl MultiBufferStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + Story::container(cx) + .child(Story::title_for::<_, MultiBuffer>(cx)) + .child(Story::label(cx, "Default")) + .child(MultiBuffer::new(vec![ + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + hello_world_rust_buffer_example(&theme), + ])) + } +} diff --git a/crates/storybook/src/stories/components/recent_projects.rs b/crates/storybook/src/stories/components/recent_projects.rs new file mode 100644 index 0000000000..f924654695 --- /dev/null +++ b/crates/storybook/src/stories/components/recent_projects.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::RecentProjects; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct RecentProjectsStory {} + +impl RecentProjectsStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, RecentProjects>(cx)) + .child(Story::label(cx, "Default")) + .child(RecentProjects::new()) + } +} diff --git a/crates/storybook/src/stories/components/theme_selector.rs b/crates/storybook/src/stories/components/theme_selector.rs new file mode 100644 index 0000000000..43e2a704e7 --- /dev/null +++ b/crates/storybook/src/stories/components/theme_selector.rs @@ -0,0 +1,16 @@ +use ui::prelude::*; +use ui::ThemeSelector; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ThemeSelectorStory {} + +impl ThemeSelectorStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container(cx) + .child(Story::title_for::<_, ThemeSelector>(cx)) + .child(Story::label(cx, "Default")) + .child(ThemeSelector::new()) + } +} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index acc965aa0a..6b0a9ac78d 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -42,13 +42,17 @@ pub enum ComponentStory { CollabPanel, Facepile, Keybinding, + LanguageSelector, + MultiBuffer, Palette, Panel, ProjectPanel, + RecentProjects, StatusBar, Tab, TabBar, Terminal, + ThemeSelector, TitleBar, Toolbar, TrafficLights, @@ -69,15 +73,25 @@ impl ComponentStory { Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(), Self::Facepile => components::facepile::FacepileStory::default().into_any(), Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(), + Self::LanguageSelector => { + components::language_selector::LanguageSelectorStory::default().into_any() + } + Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(), Self::Palette => components::palette::PaletteStory::default().into_any(), Self::Panel => components::panel::PanelStory::default().into_any(), Self::ProjectPanel => { components::project_panel::ProjectPanelStory::default().into_any() } + Self::RecentProjects => { + components::recent_projects::RecentProjectsStory::default().into_any() + } Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(), Self::Tab => components::tab::TabStory::default().into_any(), Self::TabBar => components::tab_bar::TabBarStory::default().into_any(), Self::Terminal => components::terminal::TerminalStory::default().into_any(), + Self::ThemeSelector => { + components::theme_selector::ThemeSelectorStory::default().into_any() + } Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(), Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(), Self::TrafficLights => { diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 0af13040f7..65b0218565 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -9,17 +9,22 @@ mod editor_pane; mod facepile; mod icon_button; mod keybinding; +mod language_selector; mod list; +mod multi_buffer; mod palette; mod panel; mod panes; mod player_stack; mod project_panel; +mod recent_projects; mod status_bar; mod tab; mod tab_bar; mod terminal; +mod theme_selector; mod title_bar; +mod toast; mod toolbar; mod traffic_lights; mod workspace; @@ -35,17 +40,22 @@ pub use editor_pane::*; pub use facepile::*; pub use icon_button::*; pub use keybinding::*; +pub use language_selector::*; pub use list::*; +pub use multi_buffer::*; pub use palette::*; pub use panel::*; pub use panes::*; pub use player_stack::*; pub use project_panel::*; +pub use recent_projects::*; pub use status_bar::*; pub use tab::*; pub use tab_bar::*; pub use terminal::*; +pub use theme_selector::*; pub use title_bar::*; +pub use toast::*; pub use toolbar::*; pub use traffic_lights::*; pub use workspace::*; diff --git a/crates/ui/src/components/language_selector.rs b/crates/ui/src/components/language_selector.rs new file mode 100644 index 0000000000..124d7f13ee --- /dev/null +++ b/crates/ui/src/components/language_selector.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct LanguageSelector { + scroll_state: ScrollState, +} + +impl LanguageSelector { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("C"), + PaletteItem::new("C++"), + PaletteItem::new("CSS"), + PaletteItem::new("Elixir"), + PaletteItem::new("Elm"), + PaletteItem::new("ERB"), + PaletteItem::new("Rust (current)"), + PaletteItem::new("Scheme"), + PaletteItem::new("TOML"), + PaletteItem::new("TypeScript"), + ]) + .placeholder("Select a language...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/multi_buffer.rs b/crates/ui/src/components/multi_buffer.rs new file mode 100644 index 0000000000..d38603457a --- /dev/null +++ b/crates/ui/src/components/multi_buffer.rs @@ -0,0 +1,42 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize}; + +#[derive(Element)] +pub struct MultiBuffer { + view_type: PhantomData, + buffers: Vec, +} + +impl MultiBuffer { + pub fn new(buffers: Vec) -> Self { + Self { + view_type: PhantomData, + buffers, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + v_stack() + .w_full() + .h_full() + .flex_1() + .children(self.buffers.clone().into_iter().map(|buffer| { + v_stack() + .child( + div() + .flex() + .items_center() + .justify_between() + .p_4() + .fill(theme.lowest.base.default.background) + .child(Label::new("main.rs").size(LabelSize::Small)) + .child(IconButton::new(Icon::ArrowUpRight)), + ) + .child(buffer) + })) + } +} diff --git a/crates/ui/src/components/palette.rs b/crates/ui/src/components/palette.rs index 430ab8be63..16001e50c1 100644 --- a/crates/ui/src/components/palette.rs +++ b/crates/ui/src/components/palette.rs @@ -93,19 +93,17 @@ impl Palette { .fill(theme.lowest.base.hovered.background) .active() .fill(theme.lowest.base.pressed.background) - .child( - PaletteItem::new(item.label) - .keybinding(item.keybinding.clone()), - ) + .child(item.clone()) })), ), ) } } -#[derive(Element)] +#[derive(Element, Clone)] pub struct PaletteItem { pub label: &'static str, + pub sublabel: Option<&'static str>, pub keybinding: Option, } @@ -113,6 +111,7 @@ impl PaletteItem { pub fn new(label: &'static str) -> Self { Self { label, + sublabel: None, keybinding: None, } } @@ -122,6 +121,11 @@ impl PaletteItem { self } + pub fn sublabel>>(mut self, sublabel: L) -> Self { + self.sublabel = sublabel.into(); + self + } + pub fn keybinding(mut self, keybinding: K) -> Self where K: Into>, @@ -138,7 +142,11 @@ impl PaletteItem { .flex_row() .grow() .justify_between() - .child(Label::new(self.label)) + .child( + v_stack() + .child(Label::new(self.label)) + .children(self.sublabel.map(|sublabel| Label::new(sublabel))), + ) .children(self.keybinding.clone()) } } diff --git a/crates/ui/src/components/recent_projects.rs b/crates/ui/src/components/recent_projects.rs new file mode 100644 index 0000000000..6aca6631b9 --- /dev/null +++ b/crates/ui/src/components/recent_projects.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct RecentProjects { + scroll_state: ScrollState, +} + +impl RecentProjects { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("zed").sublabel("~/projects/zed"), + PaletteItem::new("saga").sublabel("~/projects/saga"), + PaletteItem::new("journal").sublabel("~/journal"), + PaletteItem::new("dotfiles").sublabel("~/dotfiles"), + PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"), + PaletteItem::new("laminar").sublabel("~/projects/laminar"), + ]) + .placeholder("Recent Projects...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/theme_selector.rs b/crates/ui/src/components/theme_selector.rs new file mode 100644 index 0000000000..e6f5237afe --- /dev/null +++ b/crates/ui/src/components/theme_selector.rs @@ -0,0 +1,37 @@ +use crate::prelude::*; +use crate::{OrderMethod, Palette, PaletteItem}; + +#[derive(Element)] +pub struct ThemeSelector { + scroll_state: ScrollState, +} + +impl ThemeSelector { + pub fn new() -> Self { + Self { + scroll_state: ScrollState::default(), + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + Palette::new(self.scroll_state.clone()) + .items(vec![ + PaletteItem::new("One Dark"), + PaletteItem::new("Rosé Pine"), + PaletteItem::new("Rosé Pine Moon"), + PaletteItem::new("Sandcastle"), + PaletteItem::new("Solarized Dark"), + PaletteItem::new("Summercamp"), + PaletteItem::new("Atelier Cave Light"), + PaletteItem::new("Atelier Dune Light"), + PaletteItem::new("Atelier Estuary Light"), + PaletteItem::new("Atelier Forest Light"), + PaletteItem::new("Atelier Heath Light"), + ]) + .placeholder("Select Theme...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/components/toast.rs b/crates/ui/src/components/toast.rs new file mode 100644 index 0000000000..c299cdd6bc --- /dev/null +++ b/crates/ui/src/components/toast.rs @@ -0,0 +1,66 @@ +use crate::prelude::*; + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToastOrigin { + #[default] + Bottom, + BottomRight, +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum ToastVariant { + #[default] + Toast, + Status, +} + +/// A toast is a small, temporary window that appears to show a message to the user +/// or indicate a required action. +/// +/// Toasts should not persist on the screen for more than a few seconds unless +/// they are actively showing the a process in progress. +/// +/// Only one toast may be visible at a time. +#[derive(Element)] +pub struct Toast { + origin: ToastOrigin, + children: HackyChildren, + payload: HackyChildrenPayload, +} + +impl Toast { + pub fn new( + origin: ToastOrigin, + children: HackyChildren, + payload: HackyChildrenPayload, + ) -> Self { + Self { + origin, + children, + payload, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let color = ThemeColor::new(cx); + + let mut div = div(); + + if self.origin == ToastOrigin::Bottom { + div = div.right_1_2(); + } else { + div = div.right_4(); + } + + div.absolute() + .bottom_4() + .flex() + .py_2() + .px_1p5() + .min_w_40() + .rounded_md() + .fill(color.elevated_surface) + .max_w_64() + .children_any((self.children)(cx, self.payload.as_ref())) + } +} diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs index b609546f7f..b3d375bd64 100644 --- a/crates/ui/src/components/workspace.rs +++ b/crates/ui/src/components/workspace.rs @@ -82,6 +82,7 @@ impl WorkspaceElement { ); div() + .relative() .size_full() .flex() .flex_col() @@ -169,5 +170,17 @@ impl WorkspaceElement { ), ) .child(StatusBar::new()) + // An example of a toast is below + // Currently because of stacking order this gets obscured by other elements + + // .child(Toast::new( + // ToastOrigin::Bottom, + // |_, payload| { + // let theme = payload.downcast_ref::>().unwrap(); + + // vec![Label::new("label").into_any()] + // }, + // Box::new(theme.clone()), + // )) } } diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index 6d4053a4ae..26bf7dab22 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -60,6 +60,7 @@ pub enum Icon { ChevronUp, Close, ExclamationTriangle, + ExternalLink, File, FileGeneric, FileDoc, @@ -109,6 +110,7 @@ impl Icon { Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", Icon::ExclamationTriangle => "icons/warning.svg", + Icon::ExternalLink => "icons/external_link.svg", Icon::File => "icons/file.svg", Icon::FileGeneric => "icons/file_icons/file.svg", Icon::FileDoc => "icons/file_icons/book.svg", diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index b19b2becd1..e0da3579e2 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -29,6 +29,26 @@ impl SystemColor { } } +#[derive(Clone, Copy)] +pub struct ThemeColor { + pub border: Hsla, + pub border_variant: Hsla, + /// The background color of an elevated surface, like a modal, tooltip or toast. + pub elevated_surface: Hsla, +} + +impl ThemeColor { + pub fn new(cx: &WindowContext) -> Self { + let theme = theme(cx); + + Self { + border: theme.lowest.base.default.border, + border_variant: theme.lowest.variant.default.border, + elevated_surface: theme.middle.base.default.background, + } + } +} + #[derive(Default, PartialEq, EnumIter, Clone, Copy)] pub enum HighlightColor { #[default]