From f20f096a30f02dc565475d0c873f3a0a04ce9664 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 2 Oct 2023 19:15:59 +0300 Subject: [PATCH 01/60] searching the semantic index, and passing returned snippets to prompt generation --- Cargo.lock | 1 + crates/assistant/Cargo.toml | 2 + crates/assistant/src/assistant_panel.rs | 57 +++++++++++++++++++++++-- crates/assistant/src/prompts.rs | 1 + 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76de671620..2b7d74578d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,7 @@ dependencies = [ "regex", "schemars", "search", + "semantic_index", "serde", "serde_json", "settings", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 5d141b32d5..8b69e82109 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -23,7 +23,9 @@ theme = { path = "../theme" } util = { path = "../util" } uuid = { version = "1.1.2", features = ["v4"] } workspace = { path = "../workspace" } +semantic_index = { path = "../semantic_index" } +log.workspace = true anyhow.workspace = true chrono = { version = "0.4", features = ["serde"] } futures.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b69c12a2a3..8fa0327134 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -36,6 +36,7 @@ use gpui::{ }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use search::BufferSearchBar; +use semantic_index::SemanticIndex; use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, @@ -145,6 +146,7 @@ pub struct AssistantPanel { include_conversation_in_next_inline_assist: bool, inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, + semantic_index: Option>, } impl AssistantPanel { @@ -191,6 +193,9 @@ impl AssistantPanel { toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); toolbar }); + + let semantic_index = SemanticIndex::global(cx); + let mut this = Self { workspace: workspace_handle, active_editor_index: Default::default(), @@ -215,6 +220,7 @@ impl AssistantPanel { include_conversation_in_next_inline_assist: false, inline_prompt_history: Default::default(), _watch_saved_conversations, + semantic_index, }; let mut old_dock_position = this.position(cx); @@ -578,10 +584,55 @@ impl AssistantPanel { let codegen_kind = codegen.read(cx).kind().clone(); let user_prompt = user_prompt.to_string(); - let prompt = cx.background().spawn(async move { - let language_name = language_name.as_deref(); - generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind) + + let project = if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.read(cx).project() + } else { + return; + }; + + let project = project.to_owned(); + let search_results = if let Some(semantic_index) = self.semantic_index.clone() { + let search_results = semantic_index.update(cx, |this, cx| { + this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) + }); + + cx.background() + .spawn(async move { search_results.await.unwrap_or_default() }) + } else { + Task::ready(Vec::new()) + }; + + let snippets = cx.spawn(|_, cx| async move { + let mut snippets = Vec::new(); + for result in search_results.await { + snippets.push(result.buffer.read_with(&cx, |buffer, _| { + buffer + .snapshot() + .text_for_range(result.range) + .collect::() + })); + } + snippets }); + + let prompt = cx.background().spawn(async move { + let snippets = snippets.await; + for snippet in &snippets { + println!("SNIPPET: \n{:?}", snippet); + } + + let language_name = language_name.as_deref(); + generate_content_prompt( + user_prompt, + language_name, + &buffer, + range, + codegen_kind, + snippets, + ) + }); + 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 2451369a18..2301cd88ff 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -121,6 +121,7 @@ pub fn generate_content_prompt( buffer: &BufferSnapshot, range: Range, kind: CodegenKind, + search_results: Vec, ) -> String { let mut prompt = String::new(); From e9637267efb636e3da4b08570ed3b64cc17dce02 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 2 Oct 2023 19:50:57 +0300 Subject: [PATCH 02/60] add placeholder button for retrieving additional context --- crates/assistant/src/assistant_panel.rs | 34 +++++++++++++++ crates/theme/src/theme.rs | 1 + styles/src/style_tree/assistant.ts | 56 +++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 8fa0327134..8cba4c4d9f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -73,6 +73,7 @@ actions!( ResetKey, InlineAssist, ToggleIncludeConversation, + ToggleRetrieveContext, ] ); @@ -109,6 +110,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(InlineAssistant::confirm); cx.add_action(InlineAssistant::cancel); cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::toggle_retrieve_context); cx.add_action(InlineAssistant::move_up); cx.add_action(InlineAssistant::move_down); } @@ -147,6 +149,7 @@ pub struct AssistantPanel { inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, semantic_index: Option>, + retrieve_context_in_next_inline_assist: bool, } impl AssistantPanel { @@ -221,6 +224,7 @@ impl AssistantPanel { inline_prompt_history: Default::default(), _watch_saved_conversations, semantic_index, + retrieve_context_in_next_inline_assist: false, }; let mut old_dock_position = this.position(cx); @@ -314,6 +318,7 @@ impl AssistantPanel { codegen.clone(), self.workspace.clone(), cx, + self.retrieve_context_in_next_inline_assist, ); cx.focus_self(); assistant @@ -446,6 +451,9 @@ impl AssistantPanel { } => { self.include_conversation_in_next_inline_assist = *include_conversation; } + InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => { + self.retrieve_context_in_next_inline_assist = *retrieve_context + } } } @@ -2679,6 +2687,9 @@ enum InlineAssistantEvent { IncludeConversationToggled { include_conversation: bool, }, + RetrieveContextToggled { + retrieve_context: bool, + }, } struct InlineAssistant { @@ -2694,6 +2705,7 @@ struct InlineAssistant { pending_prompt: String, codegen: ModelHandle, _subscriptions: Vec, + retrieve_context: bool, } impl Entity for InlineAssistant { @@ -2722,6 +2734,18 @@ impl View for InlineAssistant { .element() .aligned(), ) + .with_child( + Button::action(ToggleRetrieveContext) + .with_tooltip("Retrieve Context", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new( + "icons/magnifying_glass.svg", + )) + .toggleable(self.retrieve_context) + .with_style(theme.assistant.inline.retrieve_context.clone()) + .element() + .aligned(), + ) .with_children(if let Some(error) = self.codegen.read(cx).error() { Some( Svg::new("icons/error.svg") @@ -2802,6 +2826,7 @@ impl InlineAssistant { codegen: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, + retrieve_context: bool, ) -> Self { let prompt_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( @@ -2832,6 +2857,7 @@ impl InlineAssistant { pending_prompt: String::new(), codegen, _subscriptions: subscriptions, + retrieve_context, } } @@ -2902,6 +2928,14 @@ impl InlineAssistant { } } + fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { + self.retrieve_context = !self.retrieve_context; + cx.emit(InlineAssistantEvent::RetrieveContextToggled { + retrieve_context: self.retrieve_context, + }); + cx.notify(); + } + fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5ea5ce8778..1ebdcd0ba6 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1190,6 +1190,7 @@ pub struct InlineAssistantStyle { pub disabled_editor: FieldEditor, pub pending_edit_background: Color, pub include_conversation: ToggleIconButtonStyle, + pub retrieve_context: ToggleIconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index cc6ee4b080..7fd1388d9c 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -79,6 +79,62 @@ export default function assistant(): any { }, }, pending_edit_background: background(theme.highest, "positive"), + retrieve_context: toggleable({ + base: interactive({ + base: { + icon_size: 12, + color: foreground(theme.highest, "variant"), + + button_width: 12, + background: background(theme.highest, "on"), + corner_radius: 2, + border: { + width: 1., color: background(theme.highest, "on") + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1., color: background(theme.highest, "on", "hovered") + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1., color: background(theme.highest, "on", "pressed") + }, + }, + }, + }), + state: { + active: { + default: { + icon_size: 12, + button_width: 12, + color: foreground(theme.highest, "variant"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), + }, + hovered: { + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), + }, + clicked: { + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), + }, + }, + }, + }), include_conversation: toggleable({ base: interactive({ base: { From bfe76467b03c23f30a00a3055c0699a7dc171615 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 11:19:54 +0300 Subject: [PATCH 03/60] add retrieve context button to inline assistant --- Cargo.lock | 21 +---- crates/assistant/Cargo.toml | 2 +- crates/assistant/src/assistant_panel.rs | 89 +++++++++++-------- crates/assistant/src/prompts.rs | 112 ++++++++++++++++-------- 4 files changed, 131 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b7d74578d..92d17ec0db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,7 +108,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", - "tiktoken-rs 0.5.4", + "tiktoken-rs", "util", ] @@ -327,7 +327,7 @@ dependencies = [ "settings", "smol", "theme", - "tiktoken-rs 0.4.5", + "tiktoken-rs", "util", "uuid 1.4.1", "workspace", @@ -6798,7 +6798,7 @@ dependencies = [ "smol", "tempdir", "theme", - "tiktoken-rs 0.5.4", + "tiktoken-rs", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -7875,21 +7875,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "tiktoken-rs" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614" -dependencies = [ - "anyhow", - "base64 0.21.4", - "bstr", - "fancy-regex", - "lazy_static", - "parking_lot 0.12.1", - "rustc-hash", -] - [[package]] name = "tiktoken-rs" version = "0.5.4" diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 8b69e82109..12f52eee02 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -38,7 +38,7 @@ schemars.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true -tiktoken-rs = "0.4" +tiktoken-rs = "0.5" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 8cba4c4d9f..16d7ee6b81 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -437,8 +437,15 @@ impl AssistantPanel { InlineAssistantEvent::Confirmed { prompt, include_conversation, + retrieve_context, } => { - self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); + self.confirm_inline_assist( + assist_id, + prompt, + *include_conversation, + cx, + *retrieve_context, + ); } InlineAssistantEvent::Canceled => { self.finish_inline_assist(assist_id, true, cx); @@ -532,6 +539,7 @@ impl AssistantPanel { user_prompt: &str, include_conversation: bool, cx: &mut ViewContext, + retrieve_context: bool, ) { let conversation = if include_conversation { self.active_editor() @@ -593,42 +601,49 @@ impl AssistantPanel { let codegen_kind = codegen.read(cx).kind().clone(); let user_prompt = user_prompt.to_string(); - let project = if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.read(cx).project() - } else { - return; - }; + let snippets = if retrieve_context { + let project = if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.read(cx).project() + } else { + return; + }; - let project = project.to_owned(); - let search_results = if let Some(semantic_index) = self.semantic_index.clone() { - let search_results = semantic_index.update(cx, |this, cx| { - this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) + let project = project.to_owned(); + let search_results = if let Some(semantic_index) = self.semantic_index.clone() { + let search_results = semantic_index.update(cx, |this, cx| { + this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) + }); + + cx.background() + .spawn(async move { search_results.await.unwrap_or_default() }) + } else { + Task::ready(Vec::new()) + }; + + let snippets = cx.spawn(|_, cx| async move { + let mut snippets = Vec::new(); + for result in search_results.await { + snippets.push(result.buffer.read_with(&cx, |buffer, _| { + buffer + .snapshot() + .text_for_range(result.range) + .collect::() + })); + } + snippets }); - - cx.background() - .spawn(async move { search_results.await.unwrap_or_default() }) + snippets } else { Task::ready(Vec::new()) }; - let snippets = cx.spawn(|_, cx| async move { - let mut snippets = Vec::new(); - for result in search_results.await { - snippets.push(result.buffer.read_with(&cx, |buffer, _| { - buffer - .snapshot() - .text_for_range(result.range) - .collect::() - })); - } - snippets - }); + let mut model = settings::get::(cx) + .default_open_ai_model + .clone(); + let model_name = model.full_name(); let prompt = cx.background().spawn(async move { let snippets = snippets.await; - for snippet in &snippets { - println!("SNIPPET: \n{:?}", snippet); - } let language_name = language_name.as_deref(); generate_content_prompt( @@ -638,13 +653,11 @@ impl AssistantPanel { range, codegen_kind, snippets, + model_name, ) }); let mut messages = Vec::new(); - let mut model = settings::get::(cx) - .default_open_ai_model - .clone(); if let Some(conversation) = conversation { let conversation = conversation.read(cx); let buffer = conversation.buffer.read(cx); @@ -1557,12 +1570,14 @@ impl Conversation { Role::Assistant => "assistant".into(), Role::System => "system".into(), }, - content: self - .buffer - .read(cx) - .text_for_range(message.offset_range) - .collect(), + content: Some( + self.buffer + .read(cx) + .text_for_range(message.offset_range) + .collect(), + ), name: None, + function_call: None, }) }) .collect::>(); @@ -2681,6 +2696,7 @@ enum InlineAssistantEvent { Confirmed { prompt: String, include_conversation: bool, + retrieve_context: bool, }, Canceled, Dismissed, @@ -2922,6 +2938,7 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::Confirmed { prompt, include_conversation: self.include_conversation, + retrieve_context: self.retrieve_context, }); self.confirmed = true; cx.notify(); diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 2301cd88ff..1e43833fea 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,8 +1,10 @@ use crate::codegen::CodegenKind; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; use std::cmp; +use std::fmt::Write; +use std::iter; use std::ops::Range; -use std::{fmt::Write, iter}; +use tiktoken_rs::ChatCompletionRequestMessage; fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] @@ -122,69 +124,103 @@ pub fn generate_content_prompt( range: Range, kind: CodegenKind, search_results: Vec, + model: &str, ) -> String { - let mut prompt = String::new(); + const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; + + let mut prompts = Vec::new(); // General Preamble if let Some(language_name) = language_name { - writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap(); + prompts.push(format!("You're an expert {language_name} engineer.\n")); } else { - writeln!(prompt, "You're an expert engineer.\n").unwrap(); + prompts.push("You're an expert engineer.\n".to_string()); } + // Snippets + let mut snippet_position = prompts.len() - 1; + let outline = summarize(buffer, range); - writeln!( - prompt, - "The file you are currently working on has the following outline:" - ) - .unwrap(); + prompts.push("The file you are currently working on has the following outline:".to_string()); if let Some(language_name) = language_name { let language_name = language_name.to_lowercase(); - writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + prompts.push(format!("```{language_name}\n{outline}\n```")); } else { - writeln!(prompt, "```\n{outline}\n```").unwrap(); + prompts.push(format!("```\n{outline}\n```")); } 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." - ) - .unwrap(); - writeln!( - prompt, + prompts.push("In particular, the user's cursor is currently on the '<|START|>' span in the above outline, with no text selected.".to_string()); + prompts + .push("Assume the cursor is located where the `<|START|` marker is.".to_string()); + prompts.push( "Text can't be replaced, so assume your answer will be inserted at the cursor." - ) - .unwrap(); - writeln!( - prompt, + .to_string(), + ); + prompts.push(format!( "Generate text based on the users prompt: {user_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, + prompts.push("In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.".to_string()); + prompts.push(format!( "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(); + )); + prompts.push("You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file.".to_string()); } } if let Some(language_name) = language_name { - writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap(); + prompts.push(format!("Your answer MUST always be valid {language_name}")); } - writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap(); - writeln!(prompt, "Never make remarks about the output.").unwrap(); + prompts.push("Always wrap your response in a Markdown codeblock".to_string()); + prompts.push("Never make remarks about the output.".to_string()); + let current_messages = [ChatCompletionRequestMessage { + role: "user".to_string(), + content: Some(prompts.join("\n")), + function_call: None, + name: None, + }]; + + let remaining_token_count = if let Ok(current_token_count) = + tiktoken_rs::num_tokens_from_messages(model, ¤t_messages) + { + let max_token_count = tiktoken_rs::model::get_context_size(model); + max_token_count - current_token_count + } else { + // If tiktoken fails to count token count, assume we have no space remaining. + 0 + }; + + // TODO: + // - add repository name to snippet + // - add file path + // - add language + if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) { + let template = "You are working inside a large repository, here are a few code snippets that may be useful"; + + for search_result in search_results { + let mut snippet_prompt = template.to_string(); + writeln!(snippet_prompt, "```\n{search_result}\n```").unwrap(); + + let token_count = encoding + .encode_with_special_tokens(snippet_prompt.as_str()) + .len(); + if token_count <= remaining_token_count { + if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT { + prompts.insert(snippet_position, snippet_prompt); + snippet_position += 1; + } + } else { + break; + } + } + } + + let prompt = prompts.join("\n"); + println!("PROMPT: {:?}", prompt); prompt } From ed894cc06fc010ae6ea15880d02923130a669e11 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 12:09:35 +0300 Subject: [PATCH 04/60] only render retrieve context button if semantic index is enabled --- crates/assistant/src/assistant_panel.rs | 28 ++++++++++++++----------- crates/assistant/src/prompts.rs | 1 - 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 16d7ee6b81..33d42c45dc 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2750,18 +2750,22 @@ impl View for InlineAssistant { .element() .aligned(), ) - .with_child( - Button::action(ToggleRetrieveContext) - .with_tooltip("Retrieve Context", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new( - "icons/magnifying_glass.svg", - )) - .toggleable(self.retrieve_context) - .with_style(theme.assistant.inline.retrieve_context.clone()) - .element() - .aligned(), - ) + .with_children(if SemanticIndex::enabled(cx) { + Some( + Button::action(ToggleRetrieveContext) + .with_tooltip("Retrieve Context", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new( + "icons/magnifying_glass.svg", + )) + .toggleable(self.retrieve_context) + .with_style(theme.assistant.inline.retrieve_context.clone()) + .element() + .aligned(), + ) + } else { + None + }) .with_children(if let Some(error) = self.codegen.read(cx).error() { Some( Svg::new("icons/error.svg") diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 716fd43505..487950dbef 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -2,7 +2,6 @@ use crate::codegen::CodegenKind; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; use std::cmp::{self, Reverse}; use std::fmt::Write; -use std::iter; use std::ops::Range; use tiktoken_rs::ChatCompletionRequestMessage; From 1a2756a2325ddea88d8f8679b8022a8f17d97a30 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 14:07:42 +0300 Subject: [PATCH 05/60] start greedily indexing when inline assistant is started, if project has been previously indexed --- crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant_panel.rs | 47 ++++++++++++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 12f52eee02..e0f90a4284 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -24,6 +24,7 @@ util = { path = "../util" } uuid = { version = "1.1.2", features = ["v4"] } workspace = { path = "../workspace" } semantic_index = { path = "../semantic_index" } +project = { path = "../project" } log.workspace = true anyhow.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 33d42c45dc..be46a63c8f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -31,10 +31,11 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, - ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use project::Project; use search::BufferSearchBar; use semantic_index::SemanticIndex; use settings::SettingsStore; @@ -272,12 +273,19 @@ impl AssistantPanel { return; }; + let project = workspace.project(); + this.update(cx, |assistant, cx| { - assistant.new_inline_assist(&active_editor, cx) + assistant.new_inline_assist(&active_editor, cx, project) }); } - fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + fn new_inline_assist( + &mut self, + editor: &ViewHandle, + cx: &mut ViewContext, + project: &ModelHandle, + ) { let api_key = if let Some(api_key) = self.api_key.borrow().clone() { api_key } else { @@ -308,6 +316,27 @@ impl AssistantPanel { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); + if let Some(semantic_index) = self.semantic_index.clone() { + let project = project.clone(); + cx.spawn(|_, mut cx| async move { + let previously_indexed = semantic_index + .update(&mut cx, |index, cx| { + index.project_previously_indexed(&project, cx) + }) + .await + .unwrap_or(false); + if previously_indexed { + let _ = semantic_index + .update(&mut cx, |index, cx| { + index.index_project(project.clone(), cx) + }) + .await; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant::new( @@ -359,6 +388,7 @@ impl AssistantPanel { editor: editor.downgrade(), inline_assistant: Some((block_id, inline_assistant.clone())), codegen: codegen.clone(), + project: project.downgrade(), _subscriptions: vec![ cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), cx.subscribe(editor, { @@ -561,6 +591,8 @@ impl AssistantPanel { return; }; + let project = pending_assist.project.clone(); + self.inline_prompt_history .retain(|prompt| prompt != user_prompt); self.inline_prompt_history.push_back(user_prompt.into()); @@ -602,13 +634,10 @@ impl AssistantPanel { let user_prompt = user_prompt.to_string(); let snippets = if retrieve_context { - let project = if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.read(cx).project() - } else { + let Some(project) = project.upgrade(cx) else { return; }; - let project = project.to_owned(); let search_results = if let Some(semantic_index) = self.semantic_index.clone() { let search_results = semantic_index.update(cx, |this, cx| { this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) @@ -2864,6 +2893,7 @@ impl InlineAssistant { cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), ]; + Self { id, prompt_editor, @@ -3019,6 +3049,7 @@ struct PendingInlineAssist { inline_assistant: Option<(BlockId, ViewHandle)>, codegen: ModelHandle, _subscriptions: Vec, + project: WeakModelHandle, } fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { From f40d3e82c0dbef9633c86bcb9175a4908206f222 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 16:26:08 +0300 Subject: [PATCH 06/60] add user prompt for permission to index the project, for context retrieval --- crates/assistant/src/assistant_panel.rs | 77 +++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index be46a63c8f..99151e5ac2 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -29,7 +29,7 @@ use gpui::{ }, fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, - platform::{CursorStyle, MouseButton}, + platform::{CursorStyle, MouseButton, PromptLevel}, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, WindowContext, @@ -348,6 +348,8 @@ impl AssistantPanel { self.workspace.clone(), cx, self.retrieve_context_in_next_inline_assist, + self.semantic_index.clone(), + project.clone(), ); cx.focus_self(); assistant @@ -2751,6 +2753,9 @@ struct InlineAssistant { codegen: ModelHandle, _subscriptions: Vec, retrieve_context: bool, + semantic_index: Option>, + semantic_permissioned: Option, + project: ModelHandle, } impl Entity for InlineAssistant { @@ -2876,6 +2881,8 @@ impl InlineAssistant { workspace: WeakViewHandle, cx: &mut ViewContext, retrieve_context: bool, + semantic_index: Option>, + project: ModelHandle, ) -> Self { let prompt_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( @@ -2908,9 +2915,26 @@ impl InlineAssistant { codegen, _subscriptions: subscriptions, retrieve_context, + semantic_permissioned: None, + semantic_index, + project, } } + fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + let project = self.project.clone(); + self.semantic_index + .as_mut() + .map(|semantic| { + semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) + } + fn handle_prompt_editor_events( &mut self, _: ViewHandle, @@ -2980,11 +3004,52 @@ impl InlineAssistant { } fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { - self.retrieve_context = !self.retrieve_context; - cx.emit(InlineAssistantEvent::RetrieveContextToggled { - retrieve_context: self.retrieve_context, - }); - cx.notify(); + let semantic_permissioned = self.semantic_permissioned(cx); + let project = self.project.clone(); + let project_name = project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"); + let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0; + let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + + cx.spawn(|this, mut cx| async move { + // If Necessary prompt user + if !semantic_permissioned.await.unwrap_or(false) { + let mut answer = this.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; + + if answer.next().await == Some(0) { + this.update(&mut cx, |this, _| { + this.semantic_permissioned = Some(true); + })?; + } else { + return anyhow::Ok(()); + } + } + + // If permissioned, update context appropriately + this.update(&mut cx, |this, cx| { + this.retrieve_context = !this.retrieve_context; + + cx.emit(InlineAssistantEvent::RetrieveContextToggled { + retrieve_context: this.retrieve_context, + }); + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } fn toggle_include_conversation( From 933c21f3d3dead2cd0717fe289aff5bda1784edd Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 16:53:57 +0300 Subject: [PATCH 07/60] add initial (non updating status) toast --- crates/assistant/src/assistant_panel.rs | 46 ++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 99151e5ac2..e6c120cd64 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -48,7 +48,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use theme::{ components::{action_button::Button, ComponentExt}, @@ -3044,6 +3044,16 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::RetrieveContextToggled { retrieve_context: this.retrieve_context, }); + + if this.retrieve_context { + let context_status = this.retrieve_context_status(cx); + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_toast(Toast::new(0, context_status), cx) + }); + } + } + cx.notify(); })?; @@ -3052,6 +3062,40 @@ impl InlineAssistant { .detach_and_log_err(cx); } + fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { + let project = self.project.clone(); + if let Some(semantic_index) = self.semantic_index.clone() { + let status = semantic_index.update(cx, |index, cx| index.status(&project)); + return match status { + // This theoretically shouldnt be a valid code path + semantic_index::SemanticIndexStatus::NotAuthenticated => { + "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() + } + semantic_index::SemanticIndexStatus::Indexed => { + "Indexing for Context Retrieval Complete!".to_string() + } + semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { + + let mut status = format!("Indexing for Context Retrieval...\nRemaining files to index: {remaining_files}"); + + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = + rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) { + writeln!(status, "Rate limit resets in {}s", remaining_seconds.as_secs()).unwrap(); + } + } + status + } + _ => { + "Indexing for Context Retrieval...\nRemaining files to index: 48".to_string() + } + }; + } + + "".to_string() + } + fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, From ec1b4e6f8563d52eaa96977cb780fcd6be61c2c1 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 5 Oct 2023 13:01:11 +0300 Subject: [PATCH 08/60] added initial working status in inline assistant prompt --- crates/ai/src/embedding.rs | 2 +- crates/assistant/src/assistant_panel.rs | 200 +++++++++++++++--------- crates/theme/src/theme.rs | 1 + styles/src/style_tree/assistant.ts | 3 + 4 files changed, 129 insertions(+), 77 deletions(-) diff --git a/crates/ai/src/embedding.rs b/crates/ai/src/embedding.rs index 332470aa54..510f987cca 100644 --- a/crates/ai/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -290,7 +290,7 @@ impl EmbeddingProvider for OpenAIEmbeddings { let mut request_number = 0; let mut rate_limiting = false; - let mut request_timeout: u64 = 15; + let mut request_timeout: u64 = 30; let mut response: Response; while request_number < MAX_RETRIES { response = self diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e6c120cd64..c49d60b8ee 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -24,10 +24,10 @@ use futures::StreamExt; use gpui::{ actions, elements::{ - ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, - Stack, Svg, Text, UniformList, UniformListState, + ChildView, Component, Empty, Flex, Label, LabelStyle, MouseEventHandler, ParentElement, + SafeStylable, Stack, Svg, Text, UniformList, UniformListState, }, - fonts::HighlightStyle, + fonts::{HighlightStyle, TextStyle}, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton, PromptLevel}, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, @@ -37,7 +37,7 @@ use gpui::{ use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; use search::BufferSearchBar; -use semantic_index::SemanticIndex; +use semantic_index::{SemanticIndex, SemanticIndexStatus}; use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, @@ -2756,6 +2756,7 @@ struct InlineAssistant { semantic_index: Option>, semantic_permissioned: Option, project: ModelHandle, + maintain_rate_limit: Option>, } impl Entity for InlineAssistant { @@ -2772,67 +2773,65 @@ impl View for InlineAssistant { let theme = theme::current(cx); Flex::row() - .with_child( - Flex::row() - .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_children([Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if SemanticIndex::enabled(cx) { + Some( + Button::action(ToggleRetrieveContext) + .with_tooltip("Retrieve Context", theme.tooltip.clone()) .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) + .with_contents(theme::components::svg::Svg::new( + "icons/magnifying_glass.svg", + )) + .toggleable(self.retrieve_context) + .with_style(theme.assistant.inline.retrieve_context.clone()) .element() .aligned(), ) - .with_children(if SemanticIndex::enabled(cx) { - Some( - Button::action(ToggleRetrieveContext) - .with_tooltip("Retrieve Context", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new( - "icons/magnifying_glass.svg", - )) - .toggleable(self.retrieve_context) - .with_style(theme.assistant.inline.retrieve_context.clone()) - .element() - .aligned(), - ) - } else { - None - }) - .with_children(if let Some(error) = self.codegen.read(cx).error() { - Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), - ) - } else { - None - }) - .aligned() - .constrained() - .dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f(measurements.gutter_width, constraint.min.y()), - max: vec2f(measurements.gutter_width, constraint.max.y()), - } + } else { + None + }) + .with_children(if let Some(error) = self.codegen.read(cx).error() { + Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }) + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), } - }), - ) + } + })]) .with_child(Empty::new().constrained().dynamically({ let measurements = self.measurements.clone(); move |constraint, _, _| { @@ -2855,6 +2854,19 @@ impl View for InlineAssistant { .left() .flex(1., true), ) + .with_children(if self.retrieve_context { + Some( + Flex::row() + .with_child(Label::new( + self.retrieve_context_status(cx), + theme.assistant.inline.context_status.text.clone(), + )) + .flex(1., true) + .aligned(), + ) + } else { + None + }) .contained() .with_style(theme.assistant.inline.container) .into_any() @@ -2896,11 +2908,15 @@ impl InlineAssistant { editor.set_placeholder_text(placeholder, cx); editor }); - let subscriptions = vec![ + let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), ]; + if let Some(semantic_index) = semantic_index.clone() { + subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); + } + Self { id, prompt_editor, @@ -2918,6 +2934,7 @@ impl InlineAssistant { semantic_permissioned: None, semantic_index, project, + maintain_rate_limit: None, } } @@ -2947,6 +2964,34 @@ impl InlineAssistant { } } + fn semantic_index_changed( + &mut self, + semantic_index: ModelHandle, + cx: &mut ViewContext, + ) { + let project = self.project.clone(); + let status = semantic_index.read(cx).status(&project); + match status { + SemanticIndexStatus::Indexing { + rate_limit_expiry: Some(_), + .. + } => { + if self.maintain_rate_limit.is_none() { + self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { + loop { + cx.background().timer(Duration::from_secs(1)).await; + this.update(&mut cx, |_, cx| cx.notify()).log_err(); + } + })); + } + return; + } + _ => { + self.maintain_rate_limit = None; + } + } + } + fn handle_codegen_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { let is_read_only = !self.codegen.read(cx).idle(); self.prompt_editor.update(cx, |editor, cx| { @@ -3044,16 +3089,7 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::RetrieveContextToggled { retrieve_context: this.retrieve_context, }); - - if this.retrieve_context { - let context_status = this.retrieve_context_status(cx); - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.show_toast(Toast::new(0, context_status), cx) - }); - } - } - + this.index_project(project, cx).log_err(); cx.notify(); })?; @@ -3062,6 +3098,18 @@ impl InlineAssistant { .detach_and_log_err(cx); } + fn index_project( + &self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> anyhow::Result<()> { + if let Some(semantic_index) = self.semantic_index.clone() { + let _ = semantic_index.update(cx, |index, cx| index.index_project(project, cx)); + } + + anyhow::Ok(()) + } + fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { let project = self.project.clone(); if let Some(semantic_index) = self.semantic_index.clone() { @@ -3072,23 +3120,23 @@ impl InlineAssistant { "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() } semantic_index::SemanticIndexStatus::Indexed => { - "Indexing for Context Retrieval Complete!".to_string() + "Indexing Complete!".to_string() } semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { - let mut status = format!("Indexing for Context Retrieval...\nRemaining files to index: {remaining_files}"); + let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); if let Some(rate_limit_expiry) = rate_limit_expiry { let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); if remaining_seconds > Duration::from_secs(0) { - writeln!(status, "Rate limit resets in {}s", remaining_seconds.as_secs()).unwrap(); + write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); } } status } - _ => { - "Indexing for Context Retrieval...\nRemaining files to index: 48".to_string() + semantic_index::SemanticIndexStatus::NotIndexed => { + "Not Indexed for Context Retrieval".to_string() } }; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 600ac7f14a..4ed32b6d1b 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1191,6 +1191,7 @@ pub struct InlineAssistantStyle { pub pending_edit_background: Color, pub include_conversation: ToggleIconButtonStyle, pub retrieve_context: ToggleIconButtonStyle, + pub context_status: ContainedText, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 7fd1388d9c..7e7b597956 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -79,6 +79,9 @@ export default function assistant(): any { }, }, pending_edit_background: background(theme.highest, "positive"), + context_status: { + ...text(theme.highest, "mono", "disabled", { size: "sm" }), + }, retrieve_context: toggleable({ base: interactive({ base: { From 0666fa80ac934f91a744988b405be9e80a4ccfb3 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 5 Oct 2023 16:49:25 +0300 Subject: [PATCH 09/60] moved status to icon with additional information in tooltip --- crates/ai/src/embedding.rs | 22 +--- crates/assistant/src/assistant_panel.rs | 164 +++++++++++++++++++----- crates/theme/src/theme.rs | 9 +- styles/src/style_tree/assistant.ts | 17 ++- 4 files changed, 161 insertions(+), 51 deletions(-) diff --git a/crates/ai/src/embedding.rs b/crates/ai/src/embedding.rs index 510f987cca..4587ece0a2 100644 --- a/crates/ai/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -85,25 +85,6 @@ impl Embedding { } } -// impl FromSql for Embedding { -// fn column_result(value: ValueRef) -> FromSqlResult { -// let bytes = value.as_blob()?; -// let embedding: Result, Box> = bincode::deserialize(bytes); -// if embedding.is_err() { -// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); -// } -// Ok(Embedding(embedding.unwrap())) -// } -// } - -// impl ToSql for Embedding { -// fn to_sql(&self) -> rusqlite::Result { -// let bytes = bincode::serialize(&self.0) -// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; -// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) -// } -// } - #[derive(Clone)] pub struct OpenAIEmbeddings { pub client: Arc, @@ -290,7 +271,7 @@ impl EmbeddingProvider for OpenAIEmbeddings { let mut request_number = 0; let mut rate_limiting = false; - let mut request_timeout: u64 = 30; + let mut request_timeout: u64 = 15; let mut response: Response; while request_number < MAX_RETRIES { response = self @@ -300,6 +281,7 @@ impl EmbeddingProvider for OpenAIEmbeddings { request_timeout, ) .await?; + request_number += 1; match response.status() { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index c49d60b8ee..25c7241688 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -52,7 +52,7 @@ use std::{ }; use theme::{ components::{action_button::Button, ComponentExt}, - AssistantStyle, + AssistantStyle, Icon, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -2857,10 +2857,7 @@ impl View for InlineAssistant { .with_children(if self.retrieve_context { Some( Flex::row() - .with_child(Label::new( - self.retrieve_context_status(cx), - theme.assistant.inline.context_status.text.clone(), - )) + .with_children(self.retrieve_context_status(cx)) .flex(1., true) .aligned(), ) @@ -3110,40 +3107,149 @@ impl InlineAssistant { anyhow::Ok(()) } - fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { + fn retrieve_context_status( + &self, + cx: &mut ViewContext, + ) -> Option> { + enum ContextStatusIcon {} let project = self.project.clone(); - if let Some(semantic_index) = self.semantic_index.clone() { - let status = semantic_index.update(cx, |index, cx| index.status(&project)); - return match status { - // This theoretically shouldnt be a valid code path - semantic_index::SemanticIndexStatus::NotAuthenticated => { - "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() - } - semantic_index::SemanticIndexStatus::Indexed => { - "Indexing Complete!".to_string() - } - semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { + if let Some(semantic_index) = SemanticIndex::global(cx) { + let status = semantic_index.update(cx, |index, _| index.status(&project)); + let theme = theme::current(cx); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::NotIndexed {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.inline.context_status.error_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.error_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.error_icon.container) + .with_tooltip::( + self.id, + "Not Indexed", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { - let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = - rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) { - write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); + let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); } } - status + Some( + Svg::new("icons/bolt.svg") + .with_color(theme.assistant.inline.context_status.in_progress_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.in_progress_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.in_progress_icon.container) + .with_tooltip::( + self.id, + status_text, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ) } - semantic_index::SemanticIndexStatus::NotIndexed => { - "Not Indexed for Context Retrieval".to_string() - } - }; + SemanticIndexStatus::Indexed {} => Some( + Svg::new("icons/circle_check.svg") + .with_color(theme.assistant.inline.context_status.complete_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.complete_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.complete_icon.container) + .with_tooltip::( + self.id, + "Indexing Complete", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + } + } else { + None } - - "".to_string() } + // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { + // let project = self.project.clone(); + // if let Some(semantic_index) = self.semantic_index.clone() { + // let status = semantic_index.update(cx, |index, cx| index.status(&project)); + // return match status { + // // This theoretically shouldnt be a valid code path + // // As the inline assistant cant be launched without an API key + // // We keep it here for safety + // semantic_index::SemanticIndexStatus::NotAuthenticated => { + // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexed => { + // "Indexing Complete!".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { + + // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); + + // if let Some(rate_limit_expiry) = rate_limit_expiry { + // let remaining_seconds = + // rate_limit_expiry.duration_since(Instant::now()); + // if remaining_seconds > Duration::from_secs(0) { + // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); + // } + // } + // status + // } + // semantic_index::SemanticIndexStatus::NotIndexed => { + // "Not Indexed for Context Retrieval".to_string() + // } + // }; + // } + + // "".to_string() + // } + fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4ed32b6d1b..21673b0f04 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1191,7 +1191,14 @@ pub struct InlineAssistantStyle { pub pending_edit_background: Color, pub include_conversation: ToggleIconButtonStyle, pub retrieve_context: ToggleIconButtonStyle, - pub context_status: ContainedText, + pub context_status: ContextStatusStyle, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ContextStatusStyle { + pub error_icon: Icon, + pub in_progress_icon: Icon, + pub complete_icon: Icon, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 7e7b597956..57737eab06 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -80,7 +80,21 @@ export default function assistant(): any { }, pending_edit_background: background(theme.highest, "positive"), context_status: { - ...text(theme.highest, "mono", "disabled", { size: "sm" }), + error_icon: { + margin: { left: 8, right: 8 }, + color: foreground(theme.highest, "negative"), + width: 12, + }, + in_progress_icon: { + margin: { left: 8, right: 8 }, + color: foreground(theme.highest, "warning"), + width: 12, + }, + complete_icon: { + margin: { left: 8, right: 8 }, + color: foreground(theme.highest, "positive"), + width: 12, + } }, retrieve_context: toggleable({ base: interactive({ @@ -94,6 +108,7 @@ export default function assistant(): any { border: { width: 1., color: background(theme.highest, "on") }, + margin: { left: 2 }, padding: { left: 4, right: 4, From c0a13285321754a27cb04b0ad3ff034262e515ef Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 6 Oct 2023 08:30:54 +0300 Subject: [PATCH 10/60] fix spawn bug from calling --- crates/assistant/src/assistant_panel.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 25c7241688..e25514a4e4 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -3100,8 +3100,13 @@ impl InlineAssistant { project: ModelHandle, cx: &mut ViewContext, ) -> anyhow::Result<()> { - if let Some(semantic_index) = self.semantic_index.clone() { - let _ = semantic_index.update(cx, |index, cx| index.index_project(project, cx)); + if let Some(semantic_index) = SemanticIndex::global(cx) { + cx.spawn(|_, mut cx| async move { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx)) + .await + }) + .detach_and_log_err(cx); } anyhow::Ok(()) From 38ccf23567f134fc6e43bdfc6fecb64e6d358eb8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 6 Oct 2023 08:46:40 +0300 Subject: [PATCH 11/60] add indexing on inline assistant opening --- crates/assistant/src/assistant_panel.rs | 58 +++++++++++++++++-------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e25514a4e4..7e199a4a2f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -24,10 +24,10 @@ use futures::StreamExt; use gpui::{ actions, elements::{ - ChildView, Component, Empty, Flex, Label, LabelStyle, MouseEventHandler, ParentElement, - SafeStylable, Stack, Svg, Text, UniformList, UniformListState, + ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, + Stack, Svg, Text, UniformList, UniformListState, }, - fonts::{HighlightStyle, TextStyle}, + fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton, PromptLevel}, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, @@ -52,7 +52,7 @@ use std::{ }; use theme::{ components::{action_button::Button, ComponentExt}, - AssistantStyle, Icon, + AssistantStyle, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -2755,7 +2755,7 @@ struct InlineAssistant { retrieve_context: bool, semantic_index: Option>, semantic_permissioned: Option, - project: ModelHandle, + project: WeakModelHandle, maintain_rate_limit: Option>, } @@ -2914,7 +2914,7 @@ impl InlineAssistant { subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); } - Self { + let assistant = Self { id, prompt_editor, workspace, @@ -2930,9 +2930,13 @@ impl InlineAssistant { retrieve_context, semantic_permissioned: None, semantic_index, - project, + project: project.downgrade(), maintain_rate_limit: None, - } + }; + + assistant.index_project(cx).log_err(); + + assistant } fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { @@ -2940,7 +2944,10 @@ impl InlineAssistant { return Task::ready(Ok(value)); } - let project = self.project.clone(); + let Some(project) = self.project.upgrade(cx) else { + return Task::ready(Err(anyhow!("project was dropped"))); + }; + self.semantic_index .as_mut() .map(|semantic| { @@ -2966,7 +2973,10 @@ impl InlineAssistant { semantic_index: ModelHandle, cx: &mut ViewContext, ) { - let project = self.project.clone(); + let Some(project) = self.project.upgrade(cx) else { + return; + }; + let status = semantic_index.read(cx).status(&project); match status { SemanticIndexStatus::Indexing { @@ -3047,7 +3057,11 @@ impl InlineAssistant { fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { let semantic_permissioned = self.semantic_permissioned(cx); - let project = self.project.clone(); + + let Some(project) = self.project.upgrade(cx) else { + return; + }; + let project_name = project .read(cx) .worktree_root_names(cx) @@ -3086,7 +3100,11 @@ impl InlineAssistant { cx.emit(InlineAssistantEvent::RetrieveContextToggled { retrieve_context: this.retrieve_context, }); - this.index_project(project, cx).log_err(); + + if this.retrieve_context { + this.index_project(cx).log_err(); + } + cx.notify(); })?; @@ -3095,11 +3113,11 @@ impl InlineAssistant { .detach_and_log_err(cx); } - fn index_project( - &self, - project: ModelHandle, - cx: &mut ViewContext, - ) -> anyhow::Result<()> { + fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { + let Some(project) = self.project.upgrade(cx) else { + return Err(anyhow!("project was dropped!")); + }; + if let Some(semantic_index) = SemanticIndex::global(cx) { cx.spawn(|_, mut cx| async move { semantic_index @@ -3117,7 +3135,11 @@ impl InlineAssistant { cx: &mut ViewContext, ) -> Option> { enum ContextStatusIcon {} - let project = self.project.clone(); + + let Some(project) = self.project.upgrade(cx) else { + return None; + }; + if let Some(semantic_index) = SemanticIndex::global(cx) { let status = semantic_index.update(cx, |index, _| index.status(&project)); let theme = theme::current(cx); From 84553899f6d3cb5f423857fd301cb53c46c2dfb9 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 6 Oct 2023 15:43:28 +0200 Subject: [PATCH 12/60] updated spacing for assistant context status icon --- styles/src/style_tree/assistant.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 57737eab06..08297731bb 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -81,17 +81,17 @@ export default function assistant(): any { pending_edit_background: background(theme.highest, "positive"), context_status: { error_icon: { - margin: { left: 8, right: 8 }, + margin: { left: 8, right: 18 }, color: foreground(theme.highest, "negative"), width: 12, }, in_progress_icon: { - margin: { left: 8, right: 8 }, - color: foreground(theme.highest, "warning"), + margin: { left: 8, right: 18 }, + color: foreground(theme.highest, "positive"), width: 12, }, complete_icon: { - margin: { left: 8, right: 8 }, + margin: { left: 8, right: 18 }, color: foreground(theme.highest, "positive"), width: 12, } From ed548a0de223d03dcb1067309be060e556f1ca55 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 6 Oct 2023 16:08:36 +0200 Subject: [PATCH 13/60] ensure indexing is only done when permissioned --- crates/assistant/src/assistant_panel.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 7e199a4a2f..17e5c161c7 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2939,7 +2939,7 @@ impl InlineAssistant { assistant } - fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { + fn semantic_permissioned(&self, cx: &mut ViewContext) -> Task> { if let Some(value) = self.semantic_permissioned { return Task::ready(Ok(value)); } @@ -2949,7 +2949,7 @@ impl InlineAssistant { }; self.semantic_index - .as_mut() + .as_ref() .map(|semantic| { semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) }) @@ -3118,11 +3118,17 @@ impl InlineAssistant { return Err(anyhow!("project was dropped!")); }; + let semantic_permissioned = self.semantic_permissioned(cx); if let Some(semantic_index) = SemanticIndex::global(cx) { cx.spawn(|_, mut cx| async move { - semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx)) - .await + // This has to be updated to accomodate for semantic_permissions + if semantic_permissioned.await.unwrap_or(false) { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx)) + .await + } else { + Err(anyhow!("project is not permissioned for semantic indexing")) + } }) .detach_and_log_err(cx); } From 391179657cdd30f2d4f851c6c945b39fa9b6b9da Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 6 Oct 2023 16:43:19 +0200 Subject: [PATCH 14/60] clean up redundancies in prompts and ensure tokens are being reserved for generation when filling semantic context --- crates/assistant/src/prompts.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 487950dbef..a3a2be1a00 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -125,6 +125,7 @@ pub fn generate_content_prompt( model: &str, ) -> String { const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; + const RESERVED_TOKENS_FOR_GENERATION: usize = 1000; let mut prompts = Vec::new(); @@ -182,11 +183,17 @@ pub fn generate_content_prompt( name: None, }]; - let remaining_token_count = if let Ok(current_token_count) = + let mut remaining_token_count = if let Ok(current_token_count) = tiktoken_rs::num_tokens_from_messages(model, ¤t_messages) { let max_token_count = tiktoken_rs::model::get_context_size(model); - max_token_count - current_token_count + let intermediate_token_count = max_token_count - current_token_count; + + if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION { + 0 + } else { + intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION + } } else { // If tiktoken fails to count token count, assume we have no space remaining. 0 @@ -197,7 +204,7 @@ pub fn generate_content_prompt( // - add file path // - add language if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) { - let template = "You are working inside a large repository, here are a few code snippets that may be useful"; + let mut template = "You are working inside a large repository, here are a few code snippets that may be useful"; for search_result in search_results { let mut snippet_prompt = template.to_string(); @@ -210,6 +217,9 @@ pub fn generate_content_prompt( if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT { prompts.insert(snippet_position, snippet_prompt); snippet_position += 1; + remaining_token_count -= token_count; + // If you have already added the template to the prompt, remove the template. + template = ""; } } else { break; From 7c867b6e5456eac337d7008fc7ed89ba3b9d669a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 09:23:06 -0600 Subject: [PATCH 15/60] New entitlements: * Universal links * Shared keychain group (to make development easier) --- crates/zed/contents/embedded.provisionprofile | Bin 0 -> 12512 bytes crates/zed/resources/zed.entitlements | 12 ++++-------- script/bundle | 2 ++ 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 crates/zed/contents/embedded.provisionprofile diff --git a/crates/zed/contents/embedded.provisionprofile b/crates/zed/contents/embedded.provisionprofile new file mode 100644 index 0000000000000000000000000000000000000000..8979e1fb9fb72e9f5adbbc05d3498e6218016581 GIT binary patch literal 12512 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 dW%r!D=*yLl-1Fw1WXXK + com.apple.developer.associated-domains + applinks:zed.dev com.apple.security.automation.apple-events com.apple.security.cs.allow-jit @@ -10,14 +12,8 @@ com.apple.security.device.camera - com.apple.security.personal-information.addressbook - - com.apple.security.personal-information.calendars - - com.apple.security.personal-information.location - - com.apple.security.personal-information.photos-library - + com.apple.security.keychain-access-groups + MQ55VZLNZQ.dev.zed.Shared diff --git a/script/bundle b/script/bundle index a1d0b305c8..e4eb23b217 100755 --- a/script/bundle +++ b/script/bundle @@ -134,6 +134,8 @@ else cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" fi +cp crates/zed/contents/embedded.provisionprofile "${app_path}/Contents/" + if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo "" From f6d0934b5d87af4c2e36a14884f498430e14a980 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 15:17:46 -0600 Subject: [PATCH 16/60] deep considered harmful --- script/bundle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/script/bundle b/script/bundle index e4eb23b217..dc5022bea5 100755 --- a/script/bundle +++ b/script/bundle @@ -145,7 +145,12 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign rm /tmp/zed-certificate.p12 security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain - /usr/bin/codesign --force --deep --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v + + # sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514 + /usr/bin/codesign --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks" -v + /usr/bin/codesign --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v + /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/zed" -v + security default-keychain -s login.keychain else echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD" From 690d9fb971996b17cd58136558118e9e2a02068d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 16:34:11 -0600 Subject: [PATCH 17/60] Add a role column to the database and start using it We cannot yet stop using `admin` because stable will continue writing it. --- .../20221109000000_test_schema.sql | 1 + .../20231011214412_add_guest_role.sql | 4 +++ crates/collab/src/db/ids.rs | 11 ++++++ crates/collab/src/db/queries/channels.rs | 34 +++++++++++++------ crates/collab/src/db/tables/channel_member.rs | 6 ++-- crates/collab/src/db/tests/buffer_tests.rs | 4 +-- crates/collab/src/db/tests/channel_tests.rs | 18 ++++++---- crates/collab/src/db/tests/message_tests.rs | 6 ++-- crates/collab/src/rpc.rs | 18 +++++++--- .../src/tests/random_channel_buffer_tests.rs | 4 ++- 10 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 crates/collab/migrations/20231011214412_add_guest_role.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 5a84bfd796..dd6e80150b 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -226,6 +226,7 @@ CREATE TABLE "channel_members" ( "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "admin" BOOLEAN NOT NULL DEFAULT false, + "role" VARCHAR, "accepted" BOOLEAN NOT NULL DEFAULT false, "updated_at" TIMESTAMP NOT NULL DEFAULT now ); diff --git a/crates/collab/migrations/20231011214412_add_guest_role.sql b/crates/collab/migrations/20231011214412_add_guest_role.sql new file mode 100644 index 0000000000..378590a0f9 --- /dev/null +++ b/crates/collab/migrations/20231011214412_add_guest_role.sql @@ -0,0 +1,4 @@ +-- Add migration script here + +ALTER TABLE channel_members ADD COLUMN role TEXT; +UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END; diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 23bb9e53bf..747e3a7d3b 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -80,3 +80,14 @@ id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); id_type!(FlagId); + +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "String(None)")] +pub enum ChannelRole { + #[sea_orm(string_value = "admin")] + Admin, + #[sea_orm(string_value = "member")] + Member, + #[sea_orm(string_value = "guest")] + Guest, +} diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index c576d2406b..0fe7820916 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -74,11 +74,12 @@ impl Database { } channel_member::ActiveModel { + id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel.id), user_id: ActiveValue::Set(creator_id), accepted: ActiveValue::Set(true), admin: ActiveValue::Set(true), - ..Default::default() + role: ActiveValue::Set(Some(ChannelRole::Admin)), } .insert(&*tx) .await?; @@ -160,18 +161,19 @@ impl Database { channel_id: ChannelId, invitee_id: UserId, inviter_id: UserId, - is_admin: bool, + role: ChannelRole, ) -> Result<()> { self.transaction(move |tx| async move { self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) .await?; channel_member::ActiveModel { + id: ActiveValue::NotSet, channel_id: ActiveValue::Set(channel_id), user_id: ActiveValue::Set(invitee_id), accepted: ActiveValue::Set(false), - admin: ActiveValue::Set(is_admin), - ..Default::default() + admin: ActiveValue::Set(role == ChannelRole::Admin), + role: ActiveValue::Set(Some(role)), } .insert(&*tx) .await?; @@ -417,7 +419,13 @@ impl Database { let channels_with_admin_privileges = channel_memberships .iter() - .filter_map(|membership| membership.admin.then_some(membership.channel_id)) + .filter_map(|membership| { + if membership.role == Some(ChannelRole::Admin) || membership.admin { + Some(membership.channel_id) + } else { + None + } + }) .collect(); let graph = self @@ -470,12 +478,12 @@ impl Database { .await } - pub async fn set_channel_member_admin( + pub async fn set_channel_member_role( &self, channel_id: ChannelId, from: UserId, for_user: UserId, - admin: bool, + role: ChannelRole, ) -> Result<()> { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel_id, from, &*tx) @@ -488,7 +496,8 @@ impl Database { .and(channel_member::Column::UserId.eq(for_user)), ) .set(channel_member::ActiveModel { - admin: ActiveValue::set(admin), + admin: ActiveValue::set(role == ChannelRole::Admin), + role: ActiveValue::set(Some(role)), ..Default::default() }) .exec(&*tx) @@ -516,6 +525,7 @@ impl Database { enum QueryMemberDetails { UserId, Admin, + Role, IsDirectMember, Accepted, } @@ -528,6 +538,7 @@ impl Database { .select_only() .column(channel_member::Column::UserId) .column(channel_member::Column::Admin) + .column(channel_member::Column::Role) .column_as( channel_member::Column::ChannelId.eq(channel_id), QueryMemberDetails::IsDirectMember, @@ -540,9 +551,10 @@ impl Database { let mut rows = Vec::::new(); while let Some(row) = stream.next().await { - let (user_id, is_admin, is_direct_member, is_invite_accepted): ( + let (user_id, is_admin, channel_role, is_direct_member, is_invite_accepted): ( UserId, bool, + Option, bool, bool, ) = row?; @@ -558,7 +570,7 @@ impl Database { if last_row.user_id == user_id { if is_direct_member { last_row.kind = kind; - last_row.admin = is_admin; + last_row.admin = channel_role == Some(ChannelRole::Admin) || is_admin; } continue; } @@ -566,7 +578,7 @@ impl Database { rows.push(proto::ChannelMember { user_id, kind, - admin: is_admin, + admin: channel_role == Some(ChannelRole::Admin) || is_admin, }); } diff --git a/crates/collab/src/db/tables/channel_member.rs b/crates/collab/src/db/tables/channel_member.rs index ba3db5a155..e8162bfcbd 100644 --- a/crates/collab/src/db/tables/channel_member.rs +++ b/crates/collab/src/db/tables/channel_member.rs @@ -1,7 +1,7 @@ -use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId}; +use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId}; use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "channel_members")] pub struct Model { #[sea_orm(primary_key)] @@ -10,6 +10,8 @@ pub struct Model { pub user_id: UserId, pub accepted: bool, pub admin: bool, + // only optional while migrating + pub role: Option, } 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 0ac41a8b0b..51ba9bf655 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -56,7 +56,7 @@ async fn test_channel_buffers(db: &Arc) { let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - db.invite_channel_member(zed_id, b_id, a_id, false) + db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) .await .unwrap(); @@ -211,7 +211,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { .await .unwrap(); - db.invite_channel_member(channel, observer_id, user_id, false) + db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member) .await .unwrap(); db.respond_to_channel_invite(channel, observer_id, true) diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 7d2bc04a35..ed4b9e061b 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -8,7 +8,7 @@ use crate::{ db::{ queries::channels::ChannelGraph, tests::{graph, TEST_RELEASE_CHANNEL}, - ChannelId, Database, NewUserParams, + ChannelId, ChannelRole, Database, NewUserParams, }, test_both_dbs, }; @@ -50,7 +50,7 @@ async fn test_channels(db: &Arc) { // Make sure that people cannot read channels they haven't been invited to assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); - db.invite_channel_member(zed_id, b_id, a_id, false) + db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) .await .unwrap(); @@ -125,9 +125,13 @@ async fn test_channels(db: &Arc) { ); // Update member permissions - let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; + let set_subchannel_admin = db + .set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin) + .await; assert!(set_subchannel_admin.is_err()); - let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; + let set_channel_admin = db + .set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin) + .await; assert!(set_channel_admin.is_ok()); let result = db.get_channels_for_user(b_id).await.unwrap(); @@ -284,13 +288,13 @@ async fn test_channel_invites(db: &Arc) { let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap(); - db.invite_channel_member(channel_1_1, user_2, user_1, false) + db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member) .await .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) + db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member) .await .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) + db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin) .await .unwrap(); diff --git a/crates/collab/src/db/tests/message_tests.rs b/crates/collab/src/db/tests/message_tests.rs index e758fcfb5d..272d8e0100 100644 --- a/crates/collab/src/db/tests/message_tests.rs +++ b/crates/collab/src/db/tests/message_tests.rs @@ -1,5 +1,5 @@ use crate::{ - db::{Database, MessageId, NewUserParams}, + db::{ChannelRole, Database, MessageId, NewUserParams}, test_both_dbs, }; use std::sync::Arc; @@ -155,7 +155,7 @@ async fn test_channel_message_new_notification(db: &Arc) { let channel_2 = db.create_channel("channel-2", None, user).await.unwrap(); - db.invite_channel_member(channel_1, observer, user, false) + db.invite_channel_member(channel_1, observer, user, ChannelRole::Member) .await .unwrap(); @@ -163,7 +163,7 @@ async fn test_channel_message_new_notification(db: &Arc) { .await .unwrap(); - db.invite_channel_member(channel_2, observer, user, false) + db.invite_channel_member(channel_2, observer, user, ChannelRole::Member) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e5c6d94ce0..f13f482c2b 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, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, - ServerId, User, UserId, + self, BufferId, ChannelId, ChannelRole, ChannelsForUser, Database, MessageId, ProjectId, + RoomId, ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -2282,7 +2282,12 @@ async fn invite_channel_member( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let invitee_id = UserId::from_proto(request.user_id); - db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin) + let role = if request.admin { + ChannelRole::Admin + } else { + ChannelRole::Member + }; + db.invite_channel_member(channel_id, invitee_id, session.user_id, role) .await?; let (channel, _) = db @@ -2342,7 +2347,12 @@ async fn set_channel_member_admin( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin) + let role = if request.admin { + ChannelRole::Admin + } else { + ChannelRole::Member + }; + db.set_channel_member_role(channel_id, session.user_id, member_id, role) .await?; let (channel, has_accepted) = db diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 6e0bef225c..1b24c7a3d2 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -1,3 +1,5 @@ +use crate::db::ChannelRole; + use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use anyhow::Result; use async_trait::async_trait; @@ -50,7 +52,7 @@ impl RandomizedTest for RandomChannelBufferTest { .await .unwrap(); for user in &users[1..] { - db.invite_channel_member(id, user.user_id, users[0].user_id, false) + db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member) .await .unwrap(); db.respond_to_channel_invite(id, user.user_id, true) From 540436a1f9892b9f3c6aafbf21d525335880d8bc Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 21:57:46 -0600 Subject: [PATCH 18/60] Push role refactoring through RPC/client --- .cargo/config.toml | 2 +- crates/channel/src/channel_store.rs | 24 ++++---- crates/channel/src/channel_store_tests.rs | 4 +- crates/collab/src/db/ids.rs | 28 +++++++++ crates/collab/src/db/queries/channels.rs | 18 ++++-- crates/collab/src/db/tests/channel_tests.rs | 10 ++-- crates/collab/src/rpc.rs | 46 +++++++-------- crates/collab/src/tests/channel_tests.rs | 43 +++++++++++--- crates/collab/src/tests/test_server.rs | 11 +++- .../src/collab_panel/channel_modal.rs | 57 ++++++++++++------- crates/rpc/proto/zed.proto | 20 ++++--- crates/rpc/src/proto.rs | 4 +- 12 files changed, 178 insertions(+), 89 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 9da6b3be08..e22bdb0f2c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0"] +rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 2a2fa454f2..64c76a0a39 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -9,7 +9,7 @@ use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ - proto::{self, ChannelEdge, ChannelPermission}, + proto::{self, ChannelEdge, ChannelPermission, ChannelRole}, TypedEnvelope, }; use serde_derive::{Deserialize, Serialize}; @@ -79,7 +79,7 @@ pub struct ChannelPath(Arc<[ChannelId]>); pub struct ChannelMembership { pub user: Arc, pub kind: proto::channel_member::Kind, - pub admin: bool, + pub role: proto::ChannelRole, } pub enum ChannelEvent { @@ -436,7 +436,7 @@ impl ChannelStore { insert_edge: parent_edge, channel_permissions: vec![ChannelPermission { channel_id, - is_admin: true, + role: ChannelRole::Admin.into(), }], ..Default::default() }, @@ -512,7 +512,7 @@ impl ChannelStore { &mut self, channel_id: ChannelId, user_id: UserId, - admin: bool, + role: proto::ChannelRole, cx: &mut ModelContext, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { @@ -526,7 +526,7 @@ impl ChannelStore { .request(proto::InviteChannelMember { channel_id, user_id, - admin, + role: role.into(), }) .await; @@ -570,11 +570,11 @@ impl ChannelStore { }) } - pub fn set_member_admin( + pub fn set_member_role( &mut self, channel_id: ChannelId, user_id: UserId, - admin: bool, + role: proto::ChannelRole, cx: &mut ModelContext, ) -> Task> { if !self.outgoing_invites.insert((channel_id, user_id)) { @@ -585,10 +585,10 @@ impl ChannelStore { let client = self.client.clone(); cx.spawn(|this, mut cx| async move { let result = client - .request(proto::SetChannelMemberAdmin { + .request(proto::SetChannelMemberRole { channel_id, user_id, - admin, + role: role.into(), }) .await; @@ -676,8 +676,8 @@ impl ChannelStore { .filter_map(|(user, member)| { Some(ChannelMembership { user, - admin: member.admin, - kind: proto::channel_member::Kind::from_i32(member.kind)?, + role: member.role(), + kind: member.kind(), }) }) .collect()) @@ -935,7 +935,7 @@ impl ChannelStore { } for permission in payload.channel_permissions { - if permission.is_admin { + if permission.role() == proto::ChannelRole::Admin { self.channels_with_admin_privileges .insert(permission.channel_id); } else { diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 9303a52092..f8828159bd 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -26,7 +26,7 @@ fn test_update_channels(cx: &mut AppContext) { ], channel_permissions: vec![proto::ChannelPermission { channel_id: 1, - is_admin: true, + role: proto::ChannelRole::Admin.into(), }], ..Default::default() }, @@ -114,7 +114,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { ], channel_permissions: vec![proto::ChannelPermission { channel_id: 0, - is_admin: true, + role: proto::ChannelRole::Admin.into(), }], ..Default::default() }, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 747e3a7d3b..946702f36c 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -1,4 +1,5 @@ use crate::Result; +use rpc::proto; use sea_orm::{entity::prelude::*, DbErr}; use serde::{Deserialize, Serialize}; @@ -91,3 +92,30 @@ pub enum ChannelRole { #[sea_orm(string_value = "guest")] Guest, } + +impl From for ChannelRole { + fn from(value: proto::ChannelRole) -> Self { + match value { + proto::ChannelRole::Admin => ChannelRole::Admin, + proto::ChannelRole::Member => ChannelRole::Member, + proto::ChannelRole::Guest => ChannelRole::Guest, + } + } +} + +impl Into for ChannelRole { + fn into(self) -> proto::ChannelRole { + match self { + ChannelRole::Admin => proto::ChannelRole::Admin, + ChannelRole::Member => proto::ChannelRole::Member, + ChannelRole::Guest => proto::ChannelRole::Guest, + } + } +} + +impl Into for ChannelRole { + fn into(self) -> i32 { + let proto: proto::ChannelRole = self.into(); + proto.into() + } +} diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0fe7820916..5c96955eba 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -564,13 +564,18 @@ impl Database { (false, true) => proto::channel_member::Kind::AncestorMember, (false, false) => continue, }; + let channel_role = channel_role.unwrap_or(if is_admin { + ChannelRole::Admin + } else { + ChannelRole::Member + }); let user_id = user_id.to_proto(); let kind = kind.into(); if let Some(last_row) = rows.last_mut() { if last_row.user_id == user_id { if is_direct_member { last_row.kind = kind; - last_row.admin = channel_role == Some(ChannelRole::Admin) || is_admin; + last_row.role = channel_role.into() } continue; } @@ -578,7 +583,7 @@ impl Database { rows.push(proto::ChannelMember { user_id, kind, - admin: channel_role == Some(ChannelRole::Admin) || is_admin, + role: channel_role.into(), }); } @@ -851,10 +856,11 @@ impl Database { &self, user: UserId, channel: ChannelId, - to: ChannelId, + new_parent: ChannelId, tx: &DatabaseTransaction, ) -> Result { - self.check_user_is_channel_admin(to, user, &*tx).await?; + self.check_user_is_channel_admin(new_parent, user, &*tx) + .await?; let paths = channel_path::Entity::find() .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel))) @@ -872,7 +878,7 @@ impl Database { } let paths_to_new_parent = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(to)) + .filter(channel_path::Column::ChannelId.eq(new_parent)) .all(tx) .await?; @@ -906,7 +912,7 @@ impl Database { if let Some(channel) = channel_descendants.get_mut(&channel) { // Remove the other parents channel.clear(); - channel.insert(to); + channel.insert(new_parent); } let channels = self diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index ed4b9e061b..90b3a0cd2e 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -328,17 +328,17 @@ async fn test_channel_invites(db: &Arc) { proto::ChannelMember { user_id: user_1.to_proto(), kind: proto::channel_member::Kind::Member.into(), - admin: true, + role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), - admin: false, + role: proto::ChannelRole::Member.into(), }, proto::ChannelMember { user_id: user_3.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), - admin: true, + role: proto::ChannelRole::Admin.into(), }, ] ); @@ -362,12 +362,12 @@ async fn test_channel_invites(db: &Arc) { proto::ChannelMember { user_id: user_1.to_proto(), kind: proto::channel_member::Kind::Member.into(), - admin: true, + role: proto::ChannelRole::Admin.into(), }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, + role: proto::ChannelRole::Member.into(), }, ] ); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f13f482c2b..b05421e960 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, BufferId, ChannelId, ChannelRole, ChannelsForUser, Database, MessageId, ProjectId, - RoomId, ServerId, User, UserId, + self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, + ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -254,7 +254,7 @@ impl Server { .add_request_handler(delete_channel) .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) - .add_request_handler(set_channel_member_admin) + .add_request_handler(set_channel_member_role) .add_request_handler(rename_channel) .add_request_handler(join_channel_buffer) .add_request_handler(leave_channel_buffer) @@ -2282,13 +2282,13 @@ async fn invite_channel_member( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let invitee_id = UserId::from_proto(request.user_id); - let role = if request.admin { - ChannelRole::Admin - } else { - ChannelRole::Member - }; - db.invite_channel_member(channel_id, invitee_id, session.user_id, role) - .await?; + db.invite_channel_member( + channel_id, + invitee_id, + session.user_id, + request.role().into(), + ) + .await?; let (channel, _) = db .get_channel(channel_id, session.user_id) @@ -2339,21 +2339,21 @@ async fn remove_channel_member( Ok(()) } -async fn set_channel_member_admin( - request: proto::SetChannelMemberAdmin, - response: Response, +async fn set_channel_member_role( + request: proto::SetChannelMemberRole, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - let role = if request.admin { - ChannelRole::Admin - } else { - ChannelRole::Member - }; - db.set_channel_member_role(channel_id, session.user_id, member_id, role) - .await?; + db.set_channel_member_role( + channel_id, + session.user_id, + member_id, + request.role().into(), + ) + .await?; let (channel, has_accepted) = db .get_channel(channel_id, member_id) @@ -2364,7 +2364,7 @@ async fn set_channel_member_admin( if has_accepted { update.channel_permissions.push(proto::ChannelPermission { channel_id: channel.id.to_proto(), - is_admin: request.admin, + role: request.role, }); } @@ -2603,7 +2603,7 @@ async fn respond_to_channel_invite( .into_iter() .map(|channel_id| proto::ChannelPermission { channel_id: channel_id.to_proto(), - is_admin: true, + role: proto::ChannelRole::Admin.into(), }), ); } @@ -3106,7 +3106,7 @@ fn build_initial_channels_update( .into_iter() .map(|id| proto::ChannelPermission { channel_id: id.to_proto(), - is_admin: true, + role: proto::ChannelRole::Admin.into(), }), ); diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 7cfcce832b..bc814d06a2 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -68,7 +68,12 @@ async fn test_core_channels( .update(cx_a, |store, cx| { assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); - let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx); + let invite = store.invite_member( + channel_a_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ); // Make sure we're synchronously storing the pending invite assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); @@ -103,12 +108,12 @@ async fn test_core_channels( &[ ( client_a.user_id().unwrap(), - true, + proto::ChannelRole::Admin, proto::channel_member::Kind::Member, ), ( client_b.user_id().unwrap(), - false, + proto::ChannelRole::Member, proto::channel_member::Kind::Invitee, ), ], @@ -183,7 +188,12 @@ async fn test_core_channels( client_a .channel_store() .update(cx_a, |store, cx| { - store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx) + store.set_member_role( + channel_a_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Admin, + cx, + ) }) .await .unwrap(); @@ -305,12 +315,12 @@ fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u #[track_caller] fn assert_members_eq( members: &[ChannelMembership], - expected_members: &[(u64, bool, proto::channel_member::Kind)], + expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)], ) { assert_eq!( members .iter() - .map(|member| (member.user.id, member.admin, member.kind)) + .map(|member| (member.user.id, member.role, member.kind)) .collect::>(), expected_members ); @@ -611,7 +621,12 @@ async fn test_permissions_update_while_invited( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx) + channel_store.invite_member( + rust_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ) }) .await .unwrap(); @@ -634,7 +649,12 @@ async fn test_permissions_update_while_invited( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx) + channel_store.set_member_role( + rust_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Admin, + cx, + ) }) .await .unwrap(); @@ -803,7 +823,12 @@ async fn test_lost_channel_creation( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx) + channel_store.invite_member( + channel_id, + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ) }) .await .unwrap(); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 2e13874125..54a59c0c00 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -17,7 +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 rpc::{proto::ChannelRole, RECEIVE_TIMEOUT}; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -325,7 +325,7 @@ impl TestServer { channel_store.invite_member( channel_id, member_client.user_id().unwrap(), - false, + ChannelRole::Member, cx, ) }) @@ -613,7 +613,12 @@ impl TestClient { cx_self .read(ChannelStore::global) .update(cx_self, |channel_store, cx| { - channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx) + channel_store.invite_member( + channel, + other_client.user_id().unwrap(), + ChannelRole::Admin, + cx, + ) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 4c811a2df5..16d5e48f45 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,5 +1,8 @@ use channel::{ChannelId, ChannelMembership, ChannelStore}; -use client::{proto, User, UserId, UserStore}; +use client::{ + proto::{self, ChannelRole}, + User, UserId, UserStore, +}; use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -343,9 +346,11 @@ impl PickerDelegate for ChannelModalDelegate { } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) { + if let Some((selected_user, role)) = self.user_at_index(self.selected_index) { match self.mode { - Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx), + Mode::ManageMembers => { + self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx) + } Mode::InviteMembers => match self.member_status(selected_user.id, cx) { Some(proto::channel_member::Kind::Invitee) => { self.remove_selected_member(cx); @@ -373,7 +378,7 @@ impl PickerDelegate for ChannelModalDelegate { let full_theme = &theme::current(cx); let theme = &full_theme.collab_panel.channel_modal; let tabbed_modal = &full_theme.collab_panel.tabbed_modal; - let (user, admin) = self.user_at_index(ix).unwrap(); + let (user, role) = self.user_at_index(ix).unwrap(); let request_status = self.member_status(user.id, cx); let style = tabbed_modal @@ -409,15 +414,25 @@ impl PickerDelegate for ChannelModalDelegate { }, ) }) - .with_children(admin.and_then(|admin| { - (in_manage && admin).then(|| { + .with_children(if in_manage && role == Some(ChannelRole::Admin) { + Some( Label::new("Admin", theme.member_tag.text.clone()) .contained() .with_style(theme.member_tag.container) .aligned() - .left() - }) - })) + .left(), + ) + } else if in_manage && role == Some(ChannelRole::Guest) { + Some( + Label::new("Guest", theme.member_tag.text.clone()) + .contained() + .with_style(theme.member_tag.container) + .aligned() + .left(), + ) + } else { + None + }) .with_children({ let svg = match self.mode { Mode::ManageMembers => Some( @@ -502,13 +517,13 @@ impl ChannelModalDelegate { }) } - fn user_at_index(&self, ix: usize) -> Option<(Arc, Option)> { + fn user_at_index(&self, ix: usize) -> Option<(Arc, Option)> { match self.mode { Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| { let channel_membership = self.members.get(*ix)?; Some(( channel_membership.user.clone(), - Some(channel_membership.admin), + Some(channel_membership.role), )) }), Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)), @@ -516,17 +531,21 @@ impl ChannelModalDelegate { } fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext>) -> Option<()> { - let (user, admin) = self.user_at_index(self.selected_index)?; - let admin = !admin.unwrap_or(false); + let (user, role) = self.user_at_index(self.selected_index)?; + let new_role = if role == Some(ChannelRole::Admin) { + ChannelRole::Member + } else { + ChannelRole::Admin + }; let update = self.channel_store.update(cx, |store, cx| { - store.set_member_admin(self.channel_id, user.id, admin, cx) + store.set_member_role(self.channel_id, user.id, new_role, cx) }); cx.spawn(|picker, mut cx| async move { update.await?; picker.update(&mut cx, |picker, cx| { let this = picker.delegate_mut(); if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) { - member.admin = admin; + member.role = new_role; } cx.focus_self(); cx.notify(); @@ -572,7 +591,7 @@ impl ChannelModalDelegate { fn invite_member(&mut self, user: Arc, cx: &mut ViewContext>) { let invite_member = self.channel_store.update(cx, |store, cx| { - store.invite_member(self.channel_id, user.id, false, cx) + store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx) }); cx.spawn(|this, mut cx| async move { @@ -582,7 +601,7 @@ impl ChannelModalDelegate { this.delegate_mut().members.push(ChannelMembership { user, kind: proto::channel_member::Kind::Invitee, - admin: false, + role: ChannelRole::Member, }); cx.notify(); }) @@ -590,7 +609,7 @@ impl ChannelModalDelegate { .detach_and_log_err(cx); } - fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext>) { + fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext>) { self.context_menu.update(cx, |context_menu, cx| { context_menu.show( Default::default(), @@ -598,7 +617,7 @@ impl ChannelModalDelegate { vec![ ContextMenuItem::action("Remove", RemoveMember), ContextMenuItem::action( - if user_is_admin { + if role == ChannelRole::Admin { "Make non-admin" } else { "Make admin" diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3501e70e6a..dbd28bcf5d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -144,7 +144,7 @@ message Envelope { DeleteChannel delete_channel = 118; GetChannelMembers get_channel_members = 119; GetChannelMembersResponse get_channel_members_response = 120; - SetChannelMemberAdmin set_channel_member_admin = 121; + SetChannelMemberRole set_channel_member_role = 145; RenameChannel rename_channel = 122; RenameChannelResponse rename_channel_response = 123; @@ -170,7 +170,7 @@ message Envelope { LinkChannel link_channel = 140; UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 144 + MoveChannel move_channel = 142; // current max: 145 } } @@ -979,7 +979,7 @@ message ChannelEdge { message ChannelPermission { uint64 channel_id = 1; - bool is_admin = 2; + ChannelRole role = 3; } message ChannelParticipants { @@ -1005,8 +1005,8 @@ message GetChannelMembersResponse { message ChannelMember { uint64 user_id = 1; - bool admin = 2; Kind kind = 3; + ChannelRole role = 4; enum Kind { Member = 0; @@ -1028,7 +1028,7 @@ message CreateChannelResponse { message InviteChannelMember { uint64 channel_id = 1; uint64 user_id = 2; - bool admin = 3; + ChannelRole role = 4; } message RemoveChannelMember { @@ -1036,10 +1036,16 @@ message RemoveChannelMember { uint64 user_id = 2; } -message SetChannelMemberAdmin { +enum ChannelRole { + Admin = 0; + Member = 1; + Guest = 2; +} + +message SetChannelMemberRole { uint64 channel_id = 1; uint64 user_id = 2; - bool admin = 3; + ChannelRole role = 3; } message RenameChannel { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0d7937f6f..57292a52ca 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -230,7 +230,7 @@ messages!( (SaveBuffer, Foreground), (RenameChannel, Foreground), (RenameChannelResponse, Foreground), - (SetChannelMemberAdmin, Foreground), + (SetChannelMemberRole, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (ShareProject, Foreground), @@ -326,7 +326,7 @@ request_messages!( (RemoveContact, Ack), (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), - (SetChannelMemberAdmin, Ack), + (SetChannelMemberRole, Ack), (SendChannelMessage, SendChannelMessageResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannelMembers, GetChannelMembersResponse), From 78432d08ca7c120e246ec854ca34ff224374dab8 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 12 Oct 2023 12:21:09 -0700 Subject: [PATCH 19/60] Add channel visibility columns and protos --- crates/channel/src/channel_store_tests.rs | 10 +++++- .../20231011214412_add_guest_role.sql | 4 +-- crates/collab/src/db/ids.rs | 35 +++++++++++++++++++ crates/collab/src/db/tables/channel.rs | 3 +- crates/collab/src/rpc.rs | 20 +++++++++-- crates/rpc/proto/zed.proto | 6 ++++ 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index f8828159bd..faa0ade51d 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent; use super::*; use client::{test::FakeServer, Client, UserStore}; use gpui::{AppContext, ModelHandle, TestAppContext}; -use rpc::proto; +use rpc::proto::{self, ChannelRole}; use settings::SettingsStore; use util::http::FakeHttpClient; @@ -18,10 +18,12 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 1, name: "b".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, proto::Channel { id: 2, name: "a".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, ], channel_permissions: vec![proto::ChannelPermission { @@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 3, name: "x".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, proto::Channel { id: 4, name: "y".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, ], insert_edge: vec![ @@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { proto::Channel { id: 0, name: "a".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, proto::Channel { id: 1, name: "b".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, proto::Channel { id: 2, name: "c".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }, ], insert_edge: vec![ @@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { channels: vec![proto::Channel { id: channel_id, name: "the-channel".to_string(), + visibility: proto::ChannelVisibility::ChannelMembers as i32, }], ..Default::default() }); diff --git a/crates/collab/migrations/20231011214412_add_guest_role.sql b/crates/collab/migrations/20231011214412_add_guest_role.sql index 378590a0f9..bd178ec63d 100644 --- a/crates/collab/migrations/20231011214412_add_guest_role.sql +++ b/crates/collab/migrations/20231011214412_add_guest_role.sql @@ -1,4 +1,4 @@ --- Add migration script here - ALTER TABLE channel_members ADD COLUMN role TEXT; UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END; + +ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'channel_members'; diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 946702f36c..d2e990a640 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -119,3 +119,38 @@ impl Into for ChannelRole { proto.into() } } + +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)] +#[sea_orm(rs_type = "String", db_type = "String(None)")] +pub enum ChannelVisibility { + #[sea_orm(string_value = "public")] + Public, + #[sea_orm(string_value = "channel_members")] + #[default] + ChannelMembers, +} + +impl From for ChannelVisibility { + fn from(value: proto::ChannelVisibility) -> Self { + match value { + proto::ChannelVisibility::Public => ChannelVisibility::Public, + proto::ChannelVisibility::ChannelMembers => ChannelVisibility::ChannelMembers, + } + } +} + +impl Into for ChannelVisibility { + fn into(self) -> proto::ChannelVisibility { + match self { + ChannelVisibility::Public => proto::ChannelVisibility::Public, + ChannelVisibility::ChannelMembers => proto::ChannelVisibility::ChannelMembers, + } + } +} + +impl Into for ChannelVisibility { + fn into(self) -> i32 { + let proto: proto::ChannelVisibility = self.into(); + proto.into() + } +} diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 54f12defc1..efda02ec43 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use crate::db::ChannelId; +use crate::db::{ChannelId, ChannelVisibility}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, + pub visbility: ChannelVisibility, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b05421e960..962a032ece 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -38,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, - LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, + self, Ack, AnyTypedEnvelope, ChannelEdge, ChannelVisibility, EntityMessage, + EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -2210,6 +2210,8 @@ async fn create_channel( let channel = proto::Channel { id: id.to_proto(), name: request.name, + // TODO: Visibility + visibility: proto::ChannelVisibility::ChannelMembers as i32, }; response.send(proto::CreateChannelResponse { @@ -2299,6 +2301,8 @@ async fn invite_channel_member( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: proto::ChannelVisibility::ChannelMembers as i32, }); for connection_id in session .connection_pool() @@ -2394,6 +2398,8 @@ async fn rename_channel( let channel = proto::Channel { id: request.channel_id, name: new_name, + // TODO: Visibility + visibility: proto::ChannelVisibility::ChannelMembers as i32, }; response.send(proto::RenameChannelResponse { channel: Some(channel.clone()), @@ -2432,6 +2438,8 @@ async fn link_channel( .map(|channel| proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: proto::ChannelVisibility::ChannelMembers as i32, }) .collect(), insert_edge: channels_to_send.edges, @@ -2523,6 +2531,8 @@ async fn move_channel( .map(|channel| proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: proto::ChannelVisibility::ChannelMembers as i32, }) .collect(), insert_edge: channels_to_send.edges, @@ -2579,6 +2589,8 @@ async fn respond_to_channel_invite( .map(|channel| proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: ChannelVisibility::ChannelMembers.into(), }), ); update.unseen_channel_messages = result.channel_messages; @@ -3082,6 +3094,8 @@ fn build_initial_channels_update( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: ChannelVisibility::Public.into(), }); } @@ -3114,6 +3128,8 @@ fn build_initial_channels_update( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + // TODO: Visibility + visibility: ChannelVisibility::Public.into(), }); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbd28bcf5d..fec56ad9dc 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1539,9 +1539,15 @@ message Nonce { uint64 lower_half = 2; } +enum ChannelVisibility { + Public = 0; + ChannelMembers = 1; +} + message Channel { uint64 id = 1; string name = 2; + ChannelVisibility visibility = 3; } message Contact { From a7db2aa39dfd5293c0569db22fa132887f53c63c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 12 Oct 2023 19:59:50 -0600 Subject: [PATCH 20/60] Add check_is_channel_participant Refactor permission checks to load ancestor permissions into memory for all checks to make the different logics more explicit. --- .../20221109000000_test_schema.sql | 3 +- crates/collab/src/db/ids.rs | 4 + crates/collab/src/db/queries/channels.rs | 194 +++++++++++++++--- crates/collab/src/db/tables/channel.rs | 2 +- crates/collab/src/db/tests/channel_tests.rs | 121 ++++++++++- crates/collab/src/tests/channel_tests.rs | 5 +- crates/rpc/proto/zed.proto | 1 + 7 files changed, 292 insertions(+), 38 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index dd6e80150b..dcb793aa51 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -192,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now + "created_at" TIMESTAMP NOT NULL DEFAULT now, + "visibility" VARCHAR NOT NULL ); CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index d2e990a640..5ba724dd12 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -91,6 +91,8 @@ pub enum ChannelRole { Member, #[sea_orm(string_value = "guest")] Guest, + #[sea_orm(string_value = "banned")] + Banned, } impl From for ChannelRole { @@ -99,6 +101,7 @@ impl From for ChannelRole { proto::ChannelRole::Admin => ChannelRole::Admin, proto::ChannelRole::Member => ChannelRole::Member, proto::ChannelRole::Guest => ChannelRole::Guest, + proto::ChannelRole::Banned => ChannelRole::Banned, } } } @@ -109,6 +112,7 @@ impl Into for ChannelRole { ChannelRole::Admin => proto::ChannelRole::Admin, ChannelRole::Member => proto::ChannelRole::Member, ChannelRole::Guest => proto::ChannelRole::Guest, + ChannelRole::Banned => proto::ChannelRole::Banned, } } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 5c96955eba..7ce20e1a20 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -37,8 +37,9 @@ impl Database { } let channel = channel::ActiveModel { + id: ActiveValue::NotSet, name: ActiveValue::Set(name.to_string()), - ..Default::default() + visibility: ActiveValue::Set(ChannelVisibility::ChannelMembers), } .insert(&*tx) .await?; @@ -89,6 +90,29 @@ impl Database { .await } + pub async fn set_channel_visibility( + &self, + channel_id: ChannelId, + visibility: ChannelVisibility, + user_id: UserId, + ) -> Result<()> { + self.transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + + channel::ActiveModel { + id: ActiveValue::Unchanged(channel_id), + visibility: ActiveValue::Set(visibility), + ..Default::default() + } + .update(&*tx) + .await?; + + Ok(()) + }) + .await + } + pub async fn delete_channel( &self, channel_id: ChannelId, @@ -160,11 +184,11 @@ impl Database { &self, channel_id: ChannelId, invitee_id: UserId, - inviter_id: UserId, + admin_id: UserId, role: ChannelRole, ) -> Result<()> { self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; channel_member::ActiveModel { @@ -262,10 +286,10 @@ impl Database { &self, channel_id: ChannelId, member_id: UserId, - remover_id: UserId, + admin_id: UserId, ) -> Result<()> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, remover_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; let result = channel_member::Entity::delete_many() @@ -481,12 +505,12 @@ impl Database { pub async fn set_channel_member_role( &self, channel_id: ChannelId, - from: UserId, + admin_id: UserId, for_user: UserId, role: ChannelRole, ) -> Result<()> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, from, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; let result = channel_member::Entity::update_many() @@ -613,43 +637,147 @@ impl Database { Ok(user_ids) } - pub async fn check_user_is_channel_member( - &self, - channel_id: ChannelId, - user_id: UserId, - tx: &DatabaseTransaction, - ) -> Result<()> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .is_in(channel_ids) - .and(channel_member::Column::UserId.eq(user_id)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?; - Ok(()) - } - pub async fn check_user_is_channel_admin( &self, channel_id: ChannelId, user_id: UserId, tx: &DatabaseTransaction, ) -> Result<()> { + match self.channel_role_for_user(channel_id, user_id, tx).await? { + Some(ChannelRole::Admin) => Ok(()), + Some(ChannelRole::Member) + | Some(ChannelRole::Banned) + | Some(ChannelRole::Guest) + | None => Err(anyhow!( + "user is not a channel admin or channel does not exist" + ))?, + } + } + + pub async fn check_user_is_channel_member( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<()> { + match self.channel_role_for_user(channel_id, user_id, tx).await? { + Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(()), + Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!( + "user is not a channel member or channel does not exist" + ))?, + } + } + + pub async fn check_user_is_channel_participant( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<()> { + match self.channel_role_for_user(channel_id, user_id, tx).await? { + Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => { + Ok(()) + } + Some(ChannelRole::Banned) | None => Err(anyhow!( + "user is not a channel participant or channel does not exist" + ))?, + } + } + + pub async fn channel_role_for_user( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result> { let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelMembership { + ChannelId, + Role, + Admin, + Visibility, + } + + let mut rows = channel_member::Entity::find() + .left_join(channel::Entity) .filter( channel_member::Column::ChannelId .is_in(channel_ids) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Admin.eq(true)), + .and(channel_member::Column::UserId.eq(user_id)), ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; - Ok(()) + .select_only() + .column(channel_member::Column::ChannelId) + .column(channel_member::Column::Role) + .column(channel_member::Column::Admin) + .column(channel::Column::Visibility) + .into_values::<_, QueryChannelMembership>() + .stream(&*tx) + .await?; + + let mut is_admin = false; + let mut is_member = false; + let mut is_participant = false; + let mut is_banned = false; + let mut current_channel_visibility = None; + + // note these channels are not iterated in any particular order, + // our current logic takes the highest permission available. + while let Some(row) = rows.next().await { + let (ch_id, role, admin, visibility): ( + ChannelId, + Option, + bool, + ChannelVisibility, + ) = row?; + match role { + Some(ChannelRole::Admin) => is_admin = true, + Some(ChannelRole::Member) => is_member = true, + Some(ChannelRole::Guest) => { + if visibility == ChannelVisibility::Public { + is_participant = true + } + } + Some(ChannelRole::Banned) => is_banned = true, + None => { + // rows created from pre-role collab server. + if admin { + is_admin = true + } else { + is_member = true + } + } + } + if channel_id == ch_id { + current_channel_visibility = Some(visibility); + } + } + // free up database connection + drop(rows); + + Ok(if is_admin { + Some(ChannelRole::Admin) + } else if is_member { + Some(ChannelRole::Member) + } else if is_banned { + Some(ChannelRole::Banned) + } else if is_participant { + if current_channel_visibility.is_none() { + current_channel_visibility = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await? + .map(|channel| channel.visibility); + } + if current_channel_visibility == Some(ChannelVisibility::Public) { + Some(ChannelRole::Guest) + } else { + None + } + } else { + None + }) } /// Returns the channel ancestors, deepest first diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index efda02ec43..0975a8cc30 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -7,7 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, - pub visbility: ChannelVisibility, + pub visibility: ChannelVisibility, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 90b3a0cd2e..2263920955 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -8,11 +8,14 @@ use crate::{ db::{ queries::channels::ChannelGraph, tests::{graph, TEST_RELEASE_CHANNEL}, - ChannelId, ChannelRole, Database, NewUserParams, + ChannelId, ChannelRole, Database, NewUserParams, UserId, }, test_both_dbs, }; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicI32, Ordering}, + Arc, +}; test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); @@ -850,6 +853,101 @@ async fn test_db_channel_moving_bugs(db: &Arc) { ); } +test_both_dbs!( + test_user_is_channel_participant, + test_user_is_channel_participant_postgres, + test_user_is_channel_participant_sqlite +); + +async fn test_user_is_channel_participant(db: &Arc) { + let admin_id = new_test_user(db, "admin@example.com").await; + let member_id = new_test_user(db, "member@example.com").await; + let guest_id = new_test_user(db, "guest@example.com").await; + + let zed_id = db.create_root_channel("zed", admin_id).await.unwrap(); + let intermediate_id = db + .create_channel("active", Some(zed_id), admin_id) + .await + .unwrap(); + let public_id = db + .create_channel("active", Some(intermediate_id), admin_id) + .await + .unwrap(); + + db.set_channel_visibility(public_id, crate::db::ChannelVisibility::Public, admin_id) + .await + .unwrap(); + db.invite_channel_member(intermediate_id, member_id, admin_id, ChannelRole::Member) + .await + .unwrap(); + db.invite_channel_member(public_id, guest_id, admin_id, ChannelRole::Guest) + .await + .unwrap(); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(public_id, admin_id, &*tx) + .await + }) + .await + .unwrap(); + db.transaction(|tx| async move { + db.check_user_is_channel_participant(public_id, member_id, &*tx) + .await + }) + .await + .unwrap(); + db.transaction(|tx| async move { + db.check_user_is_channel_participant(public_id, guest_id, &*tx) + .await + }) + .await + .unwrap(); + + db.set_channel_member_role(public_id, admin_id, guest_id, ChannelRole::Banned) + .await + .unwrap(); + assert!(db + .transaction(|tx| async move { + db.check_user_is_channel_participant(public_id, guest_id, &*tx) + .await + }) + .await + .is_err()); + + db.remove_channel_member(public_id, guest_id, admin_id) + .await + .unwrap(); + + db.set_channel_visibility(zed_id, crate::db::ChannelVisibility::Public, admin_id) + .await + .unwrap(); + + db.invite_channel_member(zed_id, guest_id, admin_id, ChannelRole::Guest) + .await + .unwrap(); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(zed_id, guest_id, &*tx) + .await + }) + .await + .unwrap(); + assert!(db + .transaction(|tx| async move { + db.check_user_is_channel_participant(intermediate_id, guest_id, &*tx) + .await + }) + .await + .is_err(),); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(public_id, guest_id, &*tx) + .await + }) + .await + .unwrap(); +} + #[track_caller] fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { let mut actual_map: HashMap> = HashMap::default(); @@ -874,3 +972,22 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) pretty_assertions::assert_eq!(actual_map, expected_map) } + +static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); + +async fn new_test_user(db: &Arc, email: &str) -> UserId { + let gid = GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst); + + db.create_user( + email, + false, + NewUserParams { + github_login: email[0..email.find("@").unwrap()].to_string(), + github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst), + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index bc814d06a2..95a672e76c 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -6,7 +6,10 @@ use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use rpc::{proto, RECEIVE_TIMEOUT}; +use rpc::{ + proto::{self}, + RECEIVE_TIMEOUT, +}; use std::sync::Arc; #[gpui::test] diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fec56ad9dc..90e425a39f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1040,6 +1040,7 @@ enum ChannelRole { Admin = 0; Member = 1; Guest = 2; + Banned = 3; } message SetChannelMemberRole { From da2b8082b36d704131e6ce9f2555b8a17ca6ca35 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 12 Oct 2023 20:42:42 -0600 Subject: [PATCH 21/60] Rename members to participants in db crate --- crates/collab/src/db/queries/buffers.rs | 4 +++- crates/collab/src/db/queries/channels.rs | 6 +++--- crates/collab/src/db/queries/messages.rs | 4 +++- crates/collab/src/db/queries/rooms.rs | 13 +++++++++---- crates/collab/src/db/tests/channel_tests.rs | 4 ++-- crates/collab/src/rpc.rs | 2 +- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index c85432f2bb..69f100e6b8 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -482,7 +482,9 @@ impl Database { ) .await?; - channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + channel_members = self + .get_channel_participants_internal(channel_id, &*tx) + .await?; let collaborators = self .get_channel_buffer_collaborators_internal(channel_id, &*tx) .await?; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 7ce20e1a20..a9601d54c8 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -498,7 +498,7 @@ impl Database { } pub async fn get_channel_members(&self, id: ChannelId) -> Result> { - self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await }) + self.transaction(|tx| async move { self.get_channel_participants_internal(id, &*tx).await }) .await } @@ -536,7 +536,7 @@ impl Database { .await } - pub async fn get_channel_member_details( + pub async fn get_channel_participant_details( &self, channel_id: ChannelId, user_id: UserId, @@ -616,7 +616,7 @@ impl Database { .await } - pub async fn get_channel_members_internal( + pub async fn get_channel_participants_internal( &self, id: ChannelId, tx: &DatabaseTransaction, diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index a48d425d90..7b38919775 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -180,7 +180,9 @@ impl Database { ) .await?; - let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?; + let mut channel_members = self + .get_channel_participants_internal(channel_id, &*tx) + .await?; channel_members.retain(|member| !participant_user_ids.contains(member)); Ok(( diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index a38c77dc0f..625615db5f 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -53,7 +53,9 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let channel_members; if let Some(channel_id) = channel_id { - channel_members = self.get_channel_members_internal(channel_id, &tx).await?; + channel_members = self + .get_channel_participants_internal(channel_id, &tx) + .await?; } else { channel_members = Vec::new(); @@ -377,7 +379,8 @@ impl Database { let room = self.get_room(room_id, &tx).await?; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? + self.get_channel_participants_internal(channel_id, &tx) + .await? } else { Vec::new() }; @@ -681,7 +684,8 @@ impl Database { let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? + self.get_channel_participants_internal(channel_id, &tx) + .await? } else { Vec::new() }; @@ -839,7 +843,8 @@ impl Database { }; let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? + self.get_channel_participants_internal(channel_id, &tx) + .await? } else { Vec::new() }; diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 2263920955..846af94a52 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -322,7 +322,7 @@ async fn test_channel_invites(db: &Arc) { assert_eq!(user_3_invites, &[channel_1_1]); let members = db - .get_channel_member_details(channel_1_1, user_1) + .get_channel_participant_details(channel_1_1, user_1) .await .unwrap(); assert_eq!( @@ -356,7 +356,7 @@ async fn test_channel_invites(db: &Arc) { .unwrap(); let members = db - .get_channel_member_details(channel_1_3, user_1) + .get_channel_participant_details(channel_1_3, user_1) .await .unwrap(); assert_eq!( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 962a032ece..f8ac77325c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2557,7 +2557,7 @@ async fn get_channel_members( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let members = db - .get_channel_member_details(channel_id, session.user_id) + .get_channel_participant_details(channel_id, session.user_id) .await?; response.send(proto::GetChannelMembersResponse { members })?; Ok(()) From 65a0ebf97598134e19a55d89e324e6655f208d28 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 12 Oct 2023 21:36:21 -0600 Subject: [PATCH 22/60] Update get_channel_participant_details to include guests --- crates/collab/src/db/ids.rs | 12 ++ crates/collab/src/db/queries/channels.rs | 108 ++++++++--- crates/collab/src/db/tests/channel_tests.rs | 205 +++++++++++++------- 3 files changed, 233 insertions(+), 92 deletions(-) diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 5ba724dd12..ee8c879ed3 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -95,6 +95,18 @@ pub enum ChannelRole { Banned, } +impl ChannelRole { + pub fn should_override(&self, other: Self) -> bool { + use ChannelRole::*; + match self { + Admin => matches!(other, Member | Banned | Guest), + Member => matches!(other, Banned | Guest), + Banned => matches!(other, Guest), + Guest => false, + } + } +} + impl From for ChannelRole { fn from(value: proto::ChannelRole) -> Self { match value { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index a9601d54c8..4cb3d00b16 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,5 +1,7 @@ +use std::cmp::Ordering; + use super::*; -use rpc::proto::ChannelEdge; +use rpc::proto::{channel_member::Kind, ChannelEdge}; use smallvec::SmallVec; type ChannelDescendants = HashMap>; @@ -539,12 +541,19 @@ impl Database { pub async fn get_channel_participant_details( &self, channel_id: ChannelId, - user_id: UserId, + admin_id: UserId, ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; + let channel_visibility = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await? + .map(|channel| channel.visibility) + .unwrap_or(ChannelVisibility::ChannelMembers); + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { UserId, @@ -552,12 +561,13 @@ impl Database { Role, IsDirectMember, Accepted, + Visibility, } let tx = tx; let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; let mut stream = channel_member::Entity::find() - .distinct() + .left_join(channel::Entity) .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) .select_only() .column(channel_member::Column::UserId) @@ -568,19 +578,32 @@ impl Database { QueryMemberDetails::IsDirectMember, ) .column(channel_member::Column::Accepted) - .order_by_asc(channel_member::Column::UserId) + .column(channel::Column::Visibility) .into_values::<_, QueryMemberDetails>() .stream(&*tx) .await?; - let mut rows = Vec::::new(); + struct UserDetail { + kind: Kind, + channel_role: ChannelRole, + } + let mut user_details: HashMap = HashMap::default(); + while let Some(row) = stream.next().await { - let (user_id, is_admin, channel_role, is_direct_member, is_invite_accepted): ( + let ( + user_id, + is_admin, + channel_role, + is_direct_member, + is_invite_accepted, + visibility, + ): ( UserId, bool, Option, bool, bool, + ChannelVisibility, ) = row?; let kind = match (is_direct_member, is_invite_accepted) { (true, true) => proto::channel_member::Kind::Member, @@ -593,25 +616,64 @@ impl Database { } else { ChannelRole::Member }); - let user_id = user_id.to_proto(); - let kind = kind.into(); - if let Some(last_row) = rows.last_mut() { - if last_row.user_id == user_id { - if is_direct_member { - last_row.kind = kind; - last_row.role = channel_role.into() - } - continue; - } + + if channel_role == ChannelRole::Guest + && visibility != ChannelVisibility::Public + && channel_visibility != ChannelVisibility::Public + { + continue; + } + + if let Some(details_mut) = user_details.get_mut(&user_id) { + if channel_role.should_override(details_mut.channel_role) { + details_mut.channel_role = channel_role; + } + if kind == Kind::Member { + details_mut.kind = kind; + // the UI is going to be a bit confusing if you already have permissions + // that are greater than or equal to the ones you're being invited to. + } else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember { + details_mut.kind = kind; + } + } else { + user_details.insert(user_id, UserDetail { kind, channel_role }); } - rows.push(proto::ChannelMember { - user_id, - kind, - role: channel_role.into(), - }); } - Ok(rows) + // sort by permissions descending, within each section, show members, then ancestor members, then invitees. + let mut results: Vec<(UserId, UserDetail)> = user_details.into_iter().collect(); + results.sort_by(|a, b| { + if a.1.channel_role.should_override(b.1.channel_role) { + return Ordering::Less; + } else if b.1.channel_role.should_override(a.1.channel_role) { + return Ordering::Greater; + } + + if a.1.kind == Kind::Member && b.1.kind != Kind::Member { + return Ordering::Less; + } else if b.1.kind == Kind::Member && a.1.kind != Kind::Member { + return Ordering::Greater; + } + + if a.1.kind == Kind::AncestorMember && b.1.kind != Kind::AncestorMember { + return Ordering::Less; + } else if b.1.kind == Kind::AncestorMember && a.1.kind != Kind::AncestorMember { + return Ordering::Greater; + } + + // would be nice to sort alphabetically instead of by user id. + // (or defer all sorting to the UI, but we need something to help the tests) + return a.0.cmp(&b.0); + }); + + Ok(results + .into_iter() + .map(|(user_id, details)| proto::ChannelMember { + user_id: user_id.to_proto(), + kind: details.kind.into(), + role: details.channel_role.into(), + }) + .collect()) }) .await } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 846af94a52..2044310d8e 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -246,46 +246,9 @@ test_both_dbs!( async fn test_channel_invites(db: &Arc) { db.create_server("test").await.unwrap(); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = new_test_user(db, "user1@example.com").await; + let user_2 = new_test_user(db, "user2@example.com").await; + let user_3 = new_test_user(db, "user3@example.com").await; let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); @@ -333,16 +296,16 @@ async fn test_channel_invites(db: &Arc) { kind: proto::channel_member::Kind::Member.into(), role: proto::ChannelRole::Admin.into(), }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - role: proto::ChannelRole::Member.into(), - }, proto::ChannelMember { user_id: user_3.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), role: proto::ChannelRole::Admin.into(), }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + role: proto::ChannelRole::Member.into(), + }, ] ); @@ -860,92 +823,198 @@ test_both_dbs!( ); async fn test_user_is_channel_participant(db: &Arc) { - let admin_id = new_test_user(db, "admin@example.com").await; - let member_id = new_test_user(db, "member@example.com").await; - let guest_id = new_test_user(db, "guest@example.com").await; + let admin = new_test_user(db, "admin@example.com").await; + let member = new_test_user(db, "member@example.com").await; + let guest = new_test_user(db, "guest@example.com").await; - let zed_id = db.create_root_channel("zed", admin_id).await.unwrap(); - let intermediate_id = db - .create_channel("active", Some(zed_id), admin_id) + let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); + let active_channel = db + .create_channel("active", Some(zed_channel), admin) .await .unwrap(); - let public_id = db - .create_channel("active", Some(intermediate_id), admin_id) + let vim_channel = db + .create_channel("vim", Some(active_channel), admin) .await .unwrap(); - db.set_channel_visibility(public_id, crate::db::ChannelVisibility::Public, admin_id) + db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) .await .unwrap(); - db.invite_channel_member(intermediate_id, member_id, admin_id, ChannelRole::Member) + db.invite_channel_member(active_channel, member, admin, ChannelRole::Member) .await .unwrap(); - db.invite_channel_member(public_id, guest_id, admin_id, ChannelRole::Guest) + db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest) + .await + .unwrap(); + + db.respond_to_channel_invite(active_channel, member, true) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(public_id, admin_id, &*tx) + db.check_user_is_channel_participant(vim_channel, admin, &*tx) .await }) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(public_id, member_id, &*tx) + db.check_user_is_channel_participant(vim_channel, member, &*tx) .await }) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(public_id, guest_id, &*tx) + db.check_user_is_channel_participant(vim_channel, guest, &*tx) .await }) .await .unwrap(); - db.set_channel_member_role(public_id, admin_id, guest_id, ChannelRole::Banned) + let members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + proto::ChannelMember { + user_id: guest.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + role: proto::ChannelRole::Guest.into(), + }, + ] + ); + + db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned) .await .unwrap(); assert!(db .transaction(|tx| async move { - db.check_user_is_channel_participant(public_id, guest_id, &*tx) + db.check_user_is_channel_participant(vim_channel, guest, &*tx) .await }) .await .is_err()); - db.remove_channel_member(public_id, guest_id, admin_id) + let members = db + .get_channel_participant_details(vim_channel, admin) .await .unwrap(); - db.set_channel_visibility(zed_id, crate::db::ChannelVisibility::Public, admin_id) + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + proto::ChannelMember { + user_id: guest.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + role: proto::ChannelRole::Banned.into(), + }, + ] + ); + + db.remove_channel_member(vim_channel, guest, admin) .await .unwrap(); - db.invite_channel_member(zed_id, guest_id, admin_id, ChannelRole::Guest) + db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest) .await .unwrap(); db.transaction(|tx| async move { - db.check_user_is_channel_participant(zed_id, guest_id, &*tx) + db.check_user_is_channel_participant(zed_channel, guest, &*tx) .await }) .await .unwrap(); assert!(db .transaction(|tx| async move { - db.check_user_is_channel_participant(intermediate_id, guest_id, &*tx) + db.check_user_is_channel_participant(active_channel, guest, &*tx) .await }) .await .is_err(),); db.transaction(|tx| async move { - db.check_user_is_channel_participant(public_id, guest_id, &*tx) + db.check_user_is_channel_participant(vim_channel, guest, &*tx) .await }) .await .unwrap(); + + // currently people invited to parent channels are not shown here + // (though they *do* have permissions!) + let members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + ] + ); + + db.respond_to_channel_invite(zed_channel, guest, true) + .await + .unwrap(); + + let members = db + .get_channel_participant_details(vim_channel, admin) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: admin.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + role: proto::ChannelRole::Admin.into(), + }, + proto::ChannelMember { + user_id: member.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Member.into(), + }, + proto::ChannelMember { + user_id: guest.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + role: proto::ChannelRole::Guest.into(), + }, + ] + ); } #[track_caller] @@ -976,8 +1045,6 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5); async fn new_test_user(db: &Arc, email: &str) -> UserId { - let gid = GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst); - db.create_user( email, false, From a8e352a473eac6d3581ba3ee39bd0ee6da814752 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 11:34:13 -0600 Subject: [PATCH 23/60] Rewrite get_user_channels with new permissions --- crates/channel/src/channel_store_tests.rs | 2 +- crates/collab/src/db/queries/channels.rs | 176 +++++++++++++++++++--- 2 files changed, 160 insertions(+), 18 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index faa0ade51d..ea47c7c7b7 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent; use super::*; use client::{test::FakeServer, Client, UserStore}; use gpui::{AppContext, ModelHandle, TestAppContext}; -use rpc::proto::{self, ChannelRole}; +use rpc::proto::{self}; use settings::SettingsStore; use util::http::FakeHttpClient; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 4cb3d00b16..625655f277 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -439,25 +439,108 @@ impl Database { channel_memberships: Vec, tx: &DatabaseTransaction, ) -> Result { - let parents_by_child_id = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + let mut edges = self + .get_channel_descendants_2(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; - let channels_with_admin_privileges = channel_memberships - .iter() - .filter_map(|membership| { - if membership.role == Some(ChannelRole::Admin) || membership.admin { - Some(membership.channel_id) + let mut role_for_channel: HashMap = HashMap::default(); + + for membership in channel_memberships.iter() { + role_for_channel.insert( + membership.channel_id, + membership.role.unwrap_or(if membership.admin { + ChannelRole::Admin } else { - None - } - }) - .collect(); + ChannelRole::Member + }), + ); + } - let graph = self - .get_channel_graph(parents_by_child_id, true, &tx) + for ChannelEdge { + parent_id, + channel_id, + } in edges.iter() + { + let parent_id = ChannelId::from_proto(*parent_id); + let channel_id = ChannelId::from_proto(*channel_id); + debug_assert!(role_for_channel.get(&parent_id).is_some()); + let parent_role = role_for_channel[&parent_id]; + if let Some(existing_role) = role_for_channel.get(&channel_id) { + if existing_role.should_override(parent_role) { + continue; + } + } + role_for_channel.insert(channel_id, parent_role); + } + + let mut channels: Vec = Vec::new(); + let mut channels_with_admin_privileges: HashSet = HashSet::default(); + let mut channels_to_remove: HashSet = HashSet::default(); + + let mut rows = channel::Entity::find() + .filter(channel::Column::Id.is_in(role_for_channel.keys().cloned())) + .stream(&*tx) .await?; + while let Some(row) = rows.next().await { + let channel = row?; + let role = role_for_channel[&channel.id]; + + if role == ChannelRole::Banned + || role == ChannelRole::Guest && channel.visibility != ChannelVisibility::Public + { + channels_to_remove.insert(channel.id.0 as u64); + continue; + } + + channels.push(Channel { + id: channel.id, + name: channel.name, + }); + + if role == ChannelRole::Admin { + channels_with_admin_privileges.insert(channel.id); + } + } + drop(rows); + + if !channels_to_remove.is_empty() { + // Note: this code assumes each channel has one parent. + let mut replacement_parent: HashMap = HashMap::default(); + for ChannelEdge { + parent_id, + channel_id, + } in edges.iter() + { + if channels_to_remove.contains(channel_id) { + replacement_parent.insert(*channel_id, *parent_id); + } + } + + let mut new_edges: Vec = Vec::new(); + 'outer: for ChannelEdge { + mut parent_id, + channel_id, + } in edges.iter() + { + if channels_to_remove.contains(channel_id) { + continue; + } + while channels_to_remove.contains(&parent_id) { + if let Some(new_parent_id) = replacement_parent.get(&parent_id) { + parent_id = *new_parent_id; + } else { + continue 'outer; + } + } + new_edges.push(ChannelEdge { + parent_id, + channel_id: *channel_id, + }) + } + edges = new_edges; + } + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { ChannelId, @@ -468,7 +551,7 @@ impl Database { { let mut rows = room_participant::Entity::find() .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id))) + .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) .select_only() .column(room::Column::ChannelId) .column(room_participant::Column::UserId) @@ -481,7 +564,7 @@ impl Database { } } - let channel_ids = graph.channels.iter().map(|c| c.id).collect::>(); + let channel_ids = channels.iter().map(|c| c.id).collect::>(); let channel_buffer_changes = self .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx) .await?; @@ -491,7 +574,7 @@ impl Database { .await?; Ok(ChannelsForUser { - channels: graph, + channels: ChannelGraph { channels, edges }, channel_participants, channels_with_admin_privileges, unseen_buffer_changes: channel_buffer_changes, @@ -842,7 +925,7 @@ impl Database { }) } - /// Returns the channel ancestors, deepest first + /// Returns the channel ancestors, include itself, deepest first pub async fn get_channel_ancestors( &self, channel_id: ChannelId, @@ -867,6 +950,65 @@ impl Database { Ok(channel_ids) } + // Returns the channel desendants as a sorted list of edges for further processing. + // The edges are sorted such that you will see unknown channel ids as children + // before you see them as parents. + async fn get_channel_descendants_2( + &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![]); + } + + let sql = format!( + r#" + SELECT + descendant_paths.* + FROM + channel_paths parent_paths, channel_paths descendant_paths + WHERE + parent_paths.channel_id IN ({values}) AND + descendant_paths.id_path != parent_paths.id_path AND + descendant_paths.id_path LIKE (parent_paths.id_path || '%') + ORDER BY + descendant_paths.id_path + "# + ); + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + + let mut paths = channel_path::Entity::find() + .from_raw_sql(stmt) + .stream(tx) + .await?; + + let mut results: Vec = Vec::new(); + while let Some(path) = paths.next().await { + let path = path?; + let ids: Vec<&str> = path.id_path.trim_matches('/').split('/').collect(); + + debug_assert!(ids.len() >= 2); + debug_assert!(ids[ids.len() - 1] == path.channel_id.to_string()); + + results.push(ChannelEdge { + parent_id: ids[ids.len() - 2].parse().unwrap(), + channel_id: ids[ids.len() - 1].parse().unwrap(), + }) + } + + Ok(results) + } + /// Returns the channel descendants, /// Structured as a map from child ids to their parent ids /// For example, the descendants of 'a' in this DAG: From 9c6f5de551ca9cf81ef08428d2ee5b24fe8e05a3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 13:17:19 -0600 Subject: [PATCH 24/60] Use new get_channel_descendants for delete --- crates/collab/src/db/queries/channels.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 625655f277..0b97569ec4 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -125,17 +125,19 @@ impl Database { .await?; // Don't remove descendant channels that have additional parents. - let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?; + let mut channels_to_remove: HashSet = HashSet::default(); + channels_to_remove.insert(channel_id); + + let graph = self.get_channel_descendants_2([channel_id], &*tx).await?; + for edge in graph.iter() { + channels_to_remove.insert(ChannelId::from_proto(edge.channel_id)); + } + { let mut channels_to_keep = channel_path::Entity::find() .filter( channel_path::Column::ChannelId - .is_in( - channels_to_remove - .keys() - .copied() - .filter(|&id| id != channel_id), - ) + .is_in(channels_to_remove.clone()) .and( channel_path::Column::IdPath .not_like(&format!("%/{}/%", channel_id)), @@ -160,7 +162,7 @@ impl Database { .await?; channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) + .filter(channel::Column::Id.is_in(channels_to_remove.clone())) .exec(&*tx) .await?; @@ -177,7 +179,7 @@ impl Database { ); tx.execute(channel_paths_stmt).await?; - Ok((channels_to_remove.into_keys().collect(), members_to_notify)) + Ok((channels_to_remove.into_iter().collect(), members_to_notify)) }) .await } From e050d168a726742dfe4e836c1bcbd758d4916ea0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 13:39:46 -0600 Subject: [PATCH 25/60] Delete some old code, reame ChannelMembers -> Members --- crates/collab/src/db/ids.rs | 8 +- crates/collab/src/db/queries/channels.rs | 183 +++-------------------- 2 files changed, 21 insertions(+), 170 deletions(-) diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index ee8c879ed3..b935c658dd 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -141,16 +141,16 @@ impl Into for ChannelRole { pub enum ChannelVisibility { #[sea_orm(string_value = "public")] Public, - #[sea_orm(string_value = "channel_members")] + #[sea_orm(string_value = "members")] #[default] - ChannelMembers, + Members, } impl From for ChannelVisibility { fn from(value: proto::ChannelVisibility) -> Self { match value { proto::ChannelVisibility::Public => ChannelVisibility::Public, - proto::ChannelVisibility::ChannelMembers => ChannelVisibility::ChannelMembers, + proto::ChannelVisibility::ChannelMembers => ChannelVisibility::Members, } } } @@ -159,7 +159,7 @@ impl Into for ChannelVisibility { fn into(self) -> proto::ChannelVisibility { match self { ChannelVisibility::Public => proto::ChannelVisibility::Public, - ChannelVisibility::ChannelMembers => proto::ChannelVisibility::ChannelMembers, + ChannelVisibility::Members => proto::ChannelVisibility::ChannelMembers, } } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0b97569ec4..74d5b797b8 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -2,9 +2,6 @@ use std::cmp::Ordering; use super::*; use rpc::proto::{channel_member::Kind, ChannelEdge}; -use smallvec::SmallVec; - -type ChannelDescendants = HashMap>; impl Database { #[cfg(test)] @@ -41,7 +38,7 @@ impl Database { let channel = channel::ActiveModel { id: ActiveValue::NotSet, name: ActiveValue::Set(name.to_string()), - visibility: ActiveValue::Set(ChannelVisibility::ChannelMembers), + visibility: ActiveValue::Set(ChannelVisibility::Members), } .insert(&*tx) .await?; @@ -349,49 +346,6 @@ impl Database { .await } - async fn get_channel_graph( - &self, - parents_by_child_id: ChannelDescendants, - trim_dangling_parents: bool, - tx: &DatabaseTransaction, - ) -> Result { - let mut channels = Vec::with_capacity(parents_by_child_id.len()); - { - let mut rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied())) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - channels.push(Channel { - id: row.id, - name: row.name, - }) - } - } - - let mut edges = Vec::with_capacity(parents_by_child_id.len()); - for (channel, parents) in parents_by_child_id.iter() { - for parent in parents.into_iter() { - if trim_dangling_parents { - if parents_by_child_id.contains_key(parent) { - edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: parent.to_proto(), - }); - } - } else { - edges.push(ChannelEdge { - channel_id: channel.to_proto(), - parent_id: parent.to_proto(), - }); - } - } - } - - Ok(ChannelGraph { channels, edges }) - } - pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -637,7 +591,7 @@ impl Database { .one(&*tx) .await? .map(|channel| channel.visibility) - .unwrap_or(ChannelVisibility::ChannelMembers); + .unwrap_or(ChannelVisibility::Members); #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { @@ -1011,79 +965,6 @@ impl Database { Ok(results) } - /// Returns the channel descendants, - /// Structured as a map from child ids to their parent ids - /// For example, the descendants of 'a' in this DAG: - /// - /// /- b -\ - /// a -- c -- d - /// - /// would be: - /// { - /// a: [], - /// b: [a], - /// c: [a], - /// d: [a, c], - /// } - async fn get_channel_descendants( - &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(HashMap::default()); - } - - let sql = format!( - r#" - SELECT - descendant_paths.* - FROM - channel_paths parent_paths, channel_paths descendant_paths - WHERE - parent_paths.channel_id IN ({values}) AND - descendant_paths.id_path LIKE (parent_paths.id_path || '%') - "# - ); - - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - - let mut parents_by_child_id: ChannelDescendants = HashMap::default(); - let mut paths = channel_path::Entity::find() - .from_raw_sql(stmt) - .stream(tx) - .await?; - - while let Some(path) = paths.next().await { - let path = path?; - let ids = path.id_path.trim_matches('/').split('/'); - let mut parent_id = None; - for id in ids { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if id == path.channel_id { - break; - } - parent_id = Some(id); - } - } - let entry = parents_by_child_id.entry(path.channel_id).or_default(); - if let Some(parent_id) = parent_id { - entry.insert(parent_id); - } - } - - Ok(parents_by_child_id) - } - /// Returns the channel with the given ID and: /// - true if the user is a member /// - false if the user hasn't accepted the invitation yet @@ -1242,18 +1123,23 @@ impl Database { .await?; } - let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?; - if let Some(channel) = channel_descendants.get_mut(&channel) { - // Remove the other parents - channel.clear(); - channel.insert(new_parent); - } - - let channels = self - .get_channel_graph(channel_descendants, false, &*tx) + let membership = channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel) + .and(channel_member::Column::UserId.eq(user)), + ) + .all(tx) .await?; - Ok(channels) + let mut channel_info = self.get_user_channels(user, membership, &*tx).await?; + + channel_info.channels.edges.push(ChannelEdge { + channel_id: channel.to_proto(), + parent_id: new_parent.to_proto(), + }); + + Ok(channel_info.channels) } /// Unlink a channel from a given parent. This will add in a root edge if @@ -1405,38 +1291,3 @@ impl PartialEq for ChannelGraph { self.channels == other.channels && self.edges == other.edges } } - -struct SmallSet(SmallVec<[T; 1]>); - -impl Deref for SmallSet { - type Target = [T]; - - fn deref(&self) -> &Self::Target { - self.0.deref() - } -} - -impl Default for SmallSet { - fn default() -> Self { - Self(SmallVec::new()) - } -} - -impl SmallSet { - fn insert(&mut self, value: T) -> bool - where - T: Ord, - { - match self.binary_search(&value) { - Ok(_) => false, - Err(ix) => { - self.0.insert(ix, value); - true - } - } - } - - fn clear(&mut self) { - self.0.clear(); - } -} From bb408936e9aed78c73bf9345746439327e876b24 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 14:08:40 -0600 Subject: [PATCH 26/60] Ignore old admin column --- crates/collab/src/db/ids.rs | 3 +- crates/collab/src/db/queries/channels.rs | 62 ++++--------------- crates/collab/src/db/tables/channel_member.rs | 4 +- 3 files changed, 14 insertions(+), 55 deletions(-) diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index b935c658dd..6dd1f2f596 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -82,12 +82,13 @@ id_type!(UserId); id_type!(ChannelBufferCollaboratorId); id_type!(FlagId); -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelRole { #[sea_orm(string_value = "admin")] Admin, #[sea_orm(string_value = "member")] + #[default] Member, #[sea_orm(string_value = "guest")] Guest, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 74d5b797b8..e7db0d4cfc 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -78,8 +78,7 @@ impl Database { channel_id: ActiveValue::Set(channel.id), user_id: ActiveValue::Set(creator_id), accepted: ActiveValue::Set(true), - admin: ActiveValue::Set(true), - role: ActiveValue::Set(Some(ChannelRole::Admin)), + role: ActiveValue::Set(ChannelRole::Admin), } .insert(&*tx) .await?; @@ -197,8 +196,7 @@ impl Database { channel_id: ActiveValue::Set(channel_id), user_id: ActiveValue::Set(invitee_id), accepted: ActiveValue::Set(false), - admin: ActiveValue::Set(role == ChannelRole::Admin), - role: ActiveValue::Set(Some(role)), + role: ActiveValue::Set(role), } .insert(&*tx) .await?; @@ -402,14 +400,7 @@ impl Database { let mut role_for_channel: HashMap = HashMap::default(); for membership in channel_memberships.iter() { - role_for_channel.insert( - membership.channel_id, - membership.role.unwrap_or(if membership.admin { - ChannelRole::Admin - } else { - ChannelRole::Member - }), - ); + role_for_channel.insert(membership.channel_id, membership.role); } for ChannelEdge { @@ -561,8 +552,7 @@ impl Database { .and(channel_member::Column::UserId.eq(for_user)), ) .set(channel_member::ActiveModel { - admin: ActiveValue::set(role == ChannelRole::Admin), - role: ActiveValue::set(Some(role)), + role: ActiveValue::set(role), ..Default::default() }) .exec(&*tx) @@ -596,7 +586,6 @@ impl Database { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { UserId, - Admin, Role, IsDirectMember, Accepted, @@ -610,7 +599,6 @@ impl Database { .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) .select_only() .column(channel_member::Column::UserId) - .column(channel_member::Column::Admin) .column(channel_member::Column::Role) .column_as( channel_member::Column::ChannelId.eq(channel_id), @@ -629,17 +617,9 @@ impl Database { let mut user_details: HashMap = HashMap::default(); while let Some(row) = stream.next().await { - let ( - user_id, - is_admin, - channel_role, - is_direct_member, - is_invite_accepted, - visibility, - ): ( + let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( UserId, - bool, - Option, + ChannelRole, bool, bool, ChannelVisibility, @@ -650,11 +630,6 @@ impl Database { (false, true) => proto::channel_member::Kind::AncestorMember, (false, false) => continue, }; - let channel_role = channel_role.unwrap_or(if is_admin { - ChannelRole::Admin - } else { - ChannelRole::Member - }); if channel_role == ChannelRole::Guest && visibility != ChannelVisibility::Public @@ -797,7 +772,6 @@ impl Database { enum QueryChannelMembership { ChannelId, Role, - Admin, Visibility, } @@ -811,7 +785,6 @@ impl Database { .select_only() .column(channel_member::Column::ChannelId) .column(channel_member::Column::Role) - .column(channel_member::Column::Admin) .column(channel::Column::Visibility) .into_values::<_, QueryChannelMembership>() .stream(&*tx) @@ -826,29 +799,16 @@ impl Database { // note these channels are not iterated in any particular order, // our current logic takes the highest permission available. while let Some(row) = rows.next().await { - let (ch_id, role, admin, visibility): ( - ChannelId, - Option, - bool, - ChannelVisibility, - ) = row?; + let (ch_id, role, visibility): (ChannelId, ChannelRole, ChannelVisibility) = row?; match role { - Some(ChannelRole::Admin) => is_admin = true, - Some(ChannelRole::Member) => is_member = true, - Some(ChannelRole::Guest) => { + ChannelRole::Admin => is_admin = true, + ChannelRole::Member => is_member = true, + ChannelRole::Guest => { if visibility == ChannelVisibility::Public { is_participant = true } } - Some(ChannelRole::Banned) => is_banned = true, - None => { - // rows created from pre-role collab server. - if admin { - is_admin = true - } else { - is_member = true - } - } + ChannelRole::Banned => is_banned = true, } if channel_id == ch_id { current_channel_visibility = Some(visibility); diff --git a/crates/collab/src/db/tables/channel_member.rs b/crates/collab/src/db/tables/channel_member.rs index e8162bfcbd..5498a00856 100644 --- a/crates/collab/src/db/tables/channel_member.rs +++ b/crates/collab/src/db/tables/channel_member.rs @@ -9,9 +9,7 @@ pub struct Model { pub channel_id: ChannelId, pub user_id: UserId, pub accepted: bool, - pub admin: bool, - // only optional while migrating - pub role: Option, + pub role: ChannelRole, } impl ActiveModelBehavior for ActiveModel {} From e20bc87152291ee37f967a72103cb2bb39bcf9a5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 14:30:20 -0600 Subject: [PATCH 27/60] Add some sanity checks for new user channel graph --- crates/collab/src/db/tests/channel_tests.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 2044310d8e..b969711232 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -895,6 +895,18 @@ async fn test_user_is_channel_participant(db: &Arc) { ] ); + db.respond_to_channel_invite(vim_channel, guest, true) + .await + .unwrap(); + + let channels = db.get_channels_for_user(guest).await.unwrap().channels; + assert_dag(channels, &[(vim_channel, None)]); + let channels = db.get_channels_for_user(member).await.unwrap().channels; + assert_dag( + channels, + &[(active_channel, None), (vim_channel, Some(active_channel))], + ); + db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned) .await .unwrap(); @@ -926,7 +938,7 @@ async fn test_user_is_channel_participant(db: &Arc) { }, proto::ChannelMember { user_id: guest.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), + kind: proto::channel_member::Kind::Member.into(), role: proto::ChannelRole::Banned.into(), }, ] @@ -1015,6 +1027,12 @@ async fn test_user_is_channel_participant(db: &Arc) { }, ] ); + + let channels = db.get_channels_for_user(guest).await.unwrap().channels; + assert_dag( + channels, + &[(zed_channel, None), (vim_channel, Some(zed_channel))], + ) } #[track_caller] From af11cc6cfdd4d16e29e62864f749710ffecf4006 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 15:07:49 -0600 Subject: [PATCH 28/60] show warnings by default --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 2eb7de20fb..3f42c3a967 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ web: cd ../zed.dev && PORT=3000 npm run dev -collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve +collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf From f8fd77b83e80743d12a38a47e960fd98e0bfddf4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 15:08:09 -0600 Subject: [PATCH 29/60] fix migration --- crates/collab/migrations/20231011214412_add_guest_role.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/migrations/20231011214412_add_guest_role.sql b/crates/collab/migrations/20231011214412_add_guest_role.sql index bd178ec63d..1713547158 100644 --- a/crates/collab/migrations/20231011214412_add_guest_role.sql +++ b/crates/collab/migrations/20231011214412_add_guest_role.sql @@ -1,4 +1,4 @@ ALTER TABLE channel_members ADD COLUMN role TEXT; UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END; -ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'channel_members'; +ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members'; From f6f9b5c8cbe153c63e0bfb6431f2ea62318011d3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 16:59:30 -0600 Subject: [PATCH 30/60] Wire through public access toggle --- crates/channel/src/channel_store.rs | 23 ++++- .../src/channel_store/channel_index.rs | 2 + crates/collab/src/db.rs | 1 + crates/collab/src/db/ids.rs | 6 +- crates/collab/src/db/queries/channels.rs | 19 +++-- crates/collab/src/db/tests.rs | 1 + crates/collab/src/rpc.rs | 69 ++++++++++----- .../collab/src/tests/channel_buffer_tests.rs | 6 +- .../src/collab_panel/channel_modal.rs | 83 ++++++++++++++++++- crates/rpc/proto/zed.proto | 10 ++- crates/rpc/src/proto.rs | 2 + crates/theme/src/theme.rs | 2 + styles/src/style_tree/collab_modals.ts | 23 ++++- 13 files changed, 209 insertions(+), 38 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 64c76a0a39..3e8fbafb6a 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -9,7 +9,7 @@ use db::RELEASE_CHANNEL; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{ - proto::{self, ChannelEdge, ChannelPermission, ChannelRole}, + proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility}, TypedEnvelope, }; use serde_derive::{Deserialize, Serialize}; @@ -49,6 +49,7 @@ pub type ChannelData = (Channel, ChannelPath); pub struct Channel { pub id: ChannelId, pub name: String, + pub visibility: proto::ChannelVisibility, pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_message_id: Option, } @@ -508,6 +509,25 @@ impl ChannelStore { }) } + pub fn set_channel_visibility( + &mut self, + channel_id: ChannelId, + visibility: ChannelVisibility, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.spawn(|_, _| async move { + let _ = client + .request(proto::SetChannelVisibility { + channel_id, + visibility: visibility.into(), + }) + .await?; + + Ok(()) + }) + } + pub fn invite_member( &mut self, channel_id: ChannelId, @@ -869,6 +889,7 @@ impl ChannelStore { ix, Arc::new(Channel { id: channel.id, + visibility: channel.visibility(), name: channel.name, unseen_note_version: None, unseen_message_id: None, diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index bf0de1b644..7b54d5dcd9 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -123,12 +123,14 @@ impl<'a> ChannelPathsInsertGuard<'a> { 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).visibility = channel_proto.visibility(); Arc::make_mut(existing_channel).name = channel_proto.name; } else { self.channels_by_id.insert( channel_proto.id, Arc::new(Channel { id: channel_proto.id, + visibility: channel_proto.visibility(), name: channel_proto.name, unseen_note_version: None, unseen_message_id: None, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e60b7cc33d..08f78c685d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -432,6 +432,7 @@ pub struct NewUserResult { pub struct Channel { pub id: ChannelId, pub name: String, + pub visibility: ChannelVisibility, } #[derive(Debug, PartialEq)] diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 6dd1f2f596..970d66d4cb 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -137,7 +137,7 @@ impl Into for ChannelRole { } } -#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelVisibility { #[sea_orm(string_value = "public")] @@ -151,7 +151,7 @@ impl From for ChannelVisibility { fn from(value: proto::ChannelVisibility) -> Self { match value { proto::ChannelVisibility::Public => ChannelVisibility::Public, - proto::ChannelVisibility::ChannelMembers => ChannelVisibility::Members, + proto::ChannelVisibility::Members => ChannelVisibility::Members, } } } @@ -160,7 +160,7 @@ impl Into for ChannelVisibility { fn into(self) -> proto::ChannelVisibility { match self { ChannelVisibility::Public => proto::ChannelVisibility::Public, - ChannelVisibility::Members => proto::ChannelVisibility::ChannelMembers, + ChannelVisibility::Members => proto::ChannelVisibility::Members, } } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index e7db0d4cfc..0b7e9eb2d8 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -93,12 +93,12 @@ impl Database { channel_id: ChannelId, visibility: ChannelVisibility, user_id: UserId, - ) -> Result<()> { + ) -> Result { self.transaction(move |tx| async move { self.check_user_is_channel_admin(channel_id, user_id, &*tx) .await?; - channel::ActiveModel { + let channel = channel::ActiveModel { id: ActiveValue::Unchanged(channel_id), visibility: ActiveValue::Set(visibility), ..Default::default() @@ -106,7 +106,7 @@ impl Database { .update(&*tx) .await?; - Ok(()) + Ok(channel) }) .await } @@ -219,14 +219,14 @@ impl Database { channel_id: ChannelId, user_id: UserId, new_name: &str, - ) -> Result { + ) -> Result { self.transaction(move |tx| async move { let new_name = Self::sanitize_channel_name(new_name)?.to_string(); self.check_user_is_channel_admin(channel_id, user_id, &*tx) .await?; - channel::ActiveModel { + let channel = channel::ActiveModel { id: ActiveValue::Unchanged(channel_id), name: ActiveValue::Set(new_name.clone()), ..Default::default() @@ -234,7 +234,11 @@ impl Database { .update(&*tx) .await?; - Ok(new_name) + Ok(Channel { + id: channel.id, + name: channel.name, + visibility: channel.visibility, + }) }) .await } @@ -336,6 +340,7 @@ impl Database { .map(|channel| Channel { id: channel.id, name: channel.name, + visibility: channel.visibility, }) .collect(); @@ -443,6 +448,7 @@ impl Database { channels.push(Channel { id: channel.id, name: channel.name, + visibility: channel.visibility, }); if role == ChannelRole::Admin { @@ -963,6 +969,7 @@ impl Database { Ok(Some(( Channel { id: channel.id, + visibility: channel.visibility, name: channel.name, }, is_accepted, diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 6a91fd6ffe..99a605106e 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -159,6 +159,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId) graph.channels.push(Channel { id: *id, name: name.to_string(), + visibility: ChannelVisibility::Members, }) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f8ac77325c..c3d8a25ab7 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, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, - ServerId, User, UserId, + self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId, + ProjectId, RoomId, ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -38,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, ChannelEdge, ChannelVisibility, EntityMessage, - EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, + self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -255,6 +255,7 @@ impl Server { .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_role) + .add_request_handler(set_channel_visibility) .add_request_handler(rename_channel) .add_request_handler(join_channel_buffer) .add_request_handler(leave_channel_buffer) @@ -2210,8 +2211,7 @@ async fn create_channel( let channel = proto::Channel { id: id.to_proto(), name: request.name, - // TODO: Visibility - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }; response.send(proto::CreateChannelResponse { @@ -2300,9 +2300,8 @@ async fn invite_channel_member( let mut update = proto::UpdateChannels::default(); update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, - // TODO: Visibility - visibility: proto::ChannelVisibility::ChannelMembers as i32, }); for connection_id in session .connection_pool() @@ -2343,6 +2342,39 @@ async fn remove_channel_member( Ok(()) } +async fn set_channel_visibility( + request: proto::SetChannelVisibility, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let visibility = request.visibility().into(); + + let channel = db + .set_channel_visibility(channel_id, visibility, session.user_id) + .await?; + + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + visibility: channel.visibility.into(), + }); + + let member_ids = db.get_channel_members(channel_id).await?; + + let connection_pool = session.connection_pool().await; + for member_id in member_ids { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; + } + } + + response.send(proto::Ack {})?; + Ok(()) +} + async fn set_channel_member_role( request: proto::SetChannelMemberRole, response: Response, @@ -2391,15 +2423,14 @@ async fn rename_channel( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let new_name = db + let channel = db .rename_channel(channel_id, session.user_id, &request.name) .await?; let channel = proto::Channel { - id: request.channel_id, - name: new_name, - // TODO: Visibility - visibility: proto::ChannelVisibility::ChannelMembers as i32, + id: channel.id.to_proto(), + name: channel.name, + visibility: channel.visibility.into(), }; response.send(proto::RenameChannelResponse { channel: Some(channel.clone()), @@ -2437,9 +2468,8 @@ async fn link_channel( .into_iter() .map(|channel| proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, - // TODO: Visibility - visibility: proto::ChannelVisibility::ChannelMembers as i32, }) .collect(), insert_edge: channels_to_send.edges, @@ -2530,9 +2560,8 @@ async fn move_channel( .into_iter() .map(|channel| proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, - // TODO: Visibility - visibility: proto::ChannelVisibility::ChannelMembers as i32, }) .collect(), insert_edge: channels_to_send.edges, @@ -2588,9 +2617,8 @@ async fn respond_to_channel_invite( .into_iter() .map(|channel| proto::Channel { id: channel.id.to_proto(), + visibility: channel.visibility.into(), name: channel.name, - // TODO: Visibility - visibility: ChannelVisibility::ChannelMembers.into(), }), ); update.unseen_channel_messages = result.channel_messages; @@ -3094,8 +3122,7 @@ fn build_initial_channels_update( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - // TODO: Visibility - visibility: ChannelVisibility::Public.into(), + visibility: channel.visibility.into(), }); } diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index a0b9b52484..14ae159ab8 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -11,7 +11,10 @@ use collections::HashMap; use editor::{Anchor, Editor, ToOffset}; use futures::future; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; -use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; +use rpc::{ + proto::{self, PeerId}, + RECEIVE_TIMEOUT, +}; use serde_json::json; use std::{ops::Range, sync::Arc}; @@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel { Channel { id, name: name.to_string(), + visibility: proto::ChannelVisibility::Members, unseen_note_version: None, unseen_message_id: None, } diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 16d5e48f45..bf04e4f7e6 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,6 +1,6 @@ -use channel::{ChannelId, ChannelMembership, ChannelStore}; +use channel::{Channel, ChannelId, ChannelMembership, ChannelStore}; use client::{ - proto::{self, ChannelRole}, + proto::{self, ChannelRole, ChannelVisibility}, User, UserId, UserStore, }; use context_menu::{ContextMenu, ContextMenuItem}; @@ -9,7 +9,8 @@ use gpui::{ actions, elements::*, platform::{CursorStyle, MouseButton}, - AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, + AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext, + ViewHandle, }; use picker::{Picker, PickerDelegate, PickerEvent}; use std::sync::Arc; @@ -185,6 +186,81 @@ impl View for ChannelModal { .into_any() } + fn render_visibility( + channel_id: ChannelId, + visibility: ChannelVisibility, + theme: &theme::TabbedModal, + cx: &mut ViewContext, + ) -> AnyElement { + enum TogglePublic {} + + if visibility == ChannelVisibility::Members { + return Flex::row() + .with_child( + MouseEventHandler::new::(0, cx, move |state, _| { + let style = theme.visibility_toggle.style_for(state); + Label::new(format!("{}", "Public access: OFF"), style.text.clone()) + .contained() + .with_style(style.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.set_channel_visibility( + channel_id, + ChannelVisibility::Public, + cx, + ) + }) + .detach_and_log_err(cx); + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .into_any(); + } + + Flex::row() + .with_child( + MouseEventHandler::new::(0, cx, move |state, _| { + let style = theme.visibility_toggle.style_for(state); + Label::new(format!("{}", "Public access: ON"), style.text.clone()) + .contained() + .with_style(style.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.set_channel_visibility( + channel_id, + ChannelVisibility::Members, + cx, + ) + }) + .detach_and_log_err(cx); + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .with_spacing(14.0) + .with_child( + MouseEventHandler::new::(1, cx, move |state, _| { + let style = theme.channel_link.style_for(state); + Label::new(format!("{}", "copy link"), style.text.clone()) + .contained() + .with_style(style.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(channel) = + this.channel_store.read(cx).channel_for_id(channel_id) + { + let item = ClipboardItem::new(channel.link()); + cx.write_to_clipboard(item); + } + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .into_any() + } + Flex::column() .with_child( Flex::column() @@ -193,6 +269,7 @@ impl View for ChannelModal { .contained() .with_style(theme.title.container.clone()), ) + .with_child(render_visibility(channel.id, channel.visibility, theme, cx)) .with_child(Flex::row().with_children([ render_mode_button::( Mode::InviteMembers, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 90e425a39f..f6d0dfa5d9 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -170,7 +170,8 @@ message Envelope { LinkChannel link_channel = 140; UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 145 + MoveChannel move_channel = 142; + SetChannelVisibility set_channel_visibility = 146; // current max: 146 } } @@ -1049,6 +1050,11 @@ message SetChannelMemberRole { ChannelRole role = 3; } +message SetChannelVisibility { + uint64 channel_id = 1; + ChannelVisibility visibility = 2; +} + message RenameChannel { uint64 channel_id = 1; string name = 2; @@ -1542,7 +1548,7 @@ message Nonce { enum ChannelVisibility { Public = 0; - ChannelMembers = 1; + Members = 1; } message Channel { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 57292a52ca..c60e99602e 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -231,6 +231,7 @@ messages!( (RenameChannel, Foreground), (RenameChannelResponse, Foreground), (SetChannelMemberRole, Foreground), + (SetChannelVisibility, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (ShareProject, Foreground), @@ -327,6 +328,7 @@ request_messages!( (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), (SetChannelMemberRole, Ack), + (SetChannelVisibility, Ack), (SendChannelMessage, SendChannelMessageResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannelMembers, GetChannelMembersResponse), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e534ba4260..fa3db61328 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -286,6 +286,8 @@ pub struct TabbedModal { pub header: ContainerStyle, pub body: ContainerStyle, pub title: ContainedText, + pub visibility_toggle: Interactive, + pub channel_link: Interactive, pub picker: Picker, pub max_height: f32, pub max_width: f32, diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index f9b22b6867..586e7be3f0 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -1,10 +1,11 @@ -import { useTheme } from "../theme" +import { StyleSet, StyleSets, Styles, useTheme } from "../theme" import { background, border, foreground, text } from "./components" import picker from "./picker" import { input } from "../component/input" import contact_finder from "./contact_finder" import { tab } from "../component/tab" import { icon_button } from "../component/icon_button" +import { interactive } from "../element/interactive" export default function channel_modal(): any { const theme = useTheme() @@ -27,6 +28,24 @@ export default function channel_modal(): any { const picker_input = input() + const interactive_text = (styleset: StyleSets) => + interactive({ + base: { + padding: { + left: 8, + top: 8 + }, + ...text(theme.middle, "sans", styleset, "default"), + }, state: { + hovered: { + ...text(theme.middle, "sans", styleset, "hovered"), + }, + clicked: { + ...text(theme.middle, "sans", styleset, "active"), + } + } + }); + const member_icon_style = icon_button({ variant: "ghost", size: "sm", @@ -88,6 +107,8 @@ export default function channel_modal(): any { left: BUTTON_OFFSET, }, }, + visibility_toggle: interactive_text("base"), + channel_link: interactive_text("accent"), picker: { empty_container: {}, item: { From 5e1e0b475936872077126419b29418c0e51231ff Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 16 Oct 2023 09:55:45 -0400 Subject: [PATCH 31/60] remove print from prompts --- crates/assistant/src/prompts.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 3550c4223c..2fdca046ad 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -245,8 +245,8 @@ pub fn generate_content_prompt( )); } prompts.push("Never make remarks about the output.".to_string()); - prompts.push("DO NOT return any text, except the generated code.".to_string()); - prompts.push("DO NOT wrap your text in a Markdown block".to_string()); + prompts.push("Do not return any text, except the generated code.".to_string()); + prompts.push("Do not wrap your text in a Markdown block".to_string()); let current_messages = [ChatCompletionRequestMessage { role: "user".to_string(), @@ -300,9 +300,7 @@ pub fn generate_content_prompt( } } - let prompt = prompts.join("\n"); - println!("PROMPT: {:?}", prompt); - prompt + prompts.join("\n") } #[cfg(test)] From 29f45a2e384e4eaf5d43e099f4d75c4a84e4adb4 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 16 Oct 2023 10:02:11 -0400 Subject: [PATCH 32/60] clean up warnings --- crates/assistant/src/prompts.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 2fdca046ad..7aafe75920 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,13 +1,11 @@ use crate::codegen::CodegenKind; -use gpui::{AppContext, AsyncAppContext}; -use language::{BufferSnapshot, Language, OffsetRangeExt, ToOffset}; +use gpui::AsyncAppContext; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; use semantic_index::SearchResult; -use std::borrow::Cow; use std::cmp::{self, Reverse}; use std::fmt::Write; use std::ops::Range; use std::path::PathBuf; -use std::sync::Arc; use tiktoken_rs::ChatCompletionRequestMessage; pub struct PromptCodeSnippet { @@ -19,7 +17,7 @@ pub struct PromptCodeSnippet { impl PromptCodeSnippet { pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self { let (content, language_name, file_path) = - search_result.buffer.read_with(cx, |buffer, cx| { + search_result.buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); let content = snapshot .text_for_range(search_result.range.clone()) @@ -29,7 +27,6 @@ impl PromptCodeSnippet { .language() .and_then(|language| Some(language.name().to_string())); - let language = buffer.language(); let file_path = buffer .file() .and_then(|file| Some(file.path().to_path_buf())); From 247728b723d752ed1b2e00dcbd79f8bf8bb356c2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 16 Oct 2023 15:53:29 -0400 Subject: [PATCH 33/60] Update indexing icon Co-Authored-By: Kyle Caverly <22121886+KCaverly@users.noreply.github.com> --- assets/icons/update.svg | 8 ++++++++ crates/assistant/src/assistant_panel.rs | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 assets/icons/update.svg diff --git a/assets/icons/update.svg b/assets/icons/update.svg new file mode 100644 index 0000000000..b529b2b08b --- /dev/null +++ b/assets/icons/update.svg @@ -0,0 +1,8 @@ + + + diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e8edf70498..65edb1832f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -3223,7 +3223,7 @@ impl InlineAssistant { } } Some( - Svg::new("icons/bolt.svg") + Svg::new("icons/update.svg") .with_color(theme.assistant.inline.context_status.in_progress_icon.color) .constrained() .with_width(theme.assistant.inline.context_status.in_progress_icon.width) @@ -3241,7 +3241,7 @@ impl InlineAssistant { ) } SemanticIndexStatus::Indexed {} => Some( - Svg::new("icons/circle_check.svg") + Svg::new("icons/check.svg") .with_color(theme.assistant.inline.context_status.complete_icon.color) .constrained() .with_width(theme.assistant.inline.context_status.complete_icon.width) @@ -3249,7 +3249,7 @@ impl InlineAssistant { .with_style(theme.assistant.inline.context_status.complete_icon.container) .with_tooltip::( self.id, - "Indexing Complete", + "Index up to date", None, theme.tooltip.clone(), cx, From 4e7b35c917745e8946bed4052351d93b45f0d3f8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 16 Oct 2023 13:27:05 -0600 Subject: [PATCH 34/60] Make joining a channel as a guest always succeed --- crates/channel/src/channel_store.rs | 1 + crates/collab/src/db/queries/channels.rs | 129 +++++++++--- crates/collab/src/db/queries/rooms.rs | 184 +++++++++++------- crates/collab/src/db/tests/channel_tests.rs | 15 +- crates/collab/src/rpc.rs | 160 ++++++++------- crates/collab/src/tests/channel_tests.rs | 52 +++++ .../src/collab_panel/channel_modal.rs | 2 +- 7 files changed, 371 insertions(+), 172 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 3e8fbafb6a..57b183f7de 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -972,6 +972,7 @@ impl ChannelStore { let mut all_user_ids = Vec::new(); let channel_participants = payload.channel_participants; + dbg!(&channel_participants); for entry in &channel_participants { for user_id in entry.participant_user_ids.iter() { if let Err(ix) = all_user_ids.binary_search(user_id) { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0b7e9eb2d8..d4276603f9 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -88,6 +88,84 @@ impl Database { .await } + pub async fn join_channel_internal( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + environment: &str, + tx: &DatabaseTransaction, + ) -> Result<(JoinRoom, bool)> { + let mut joined = false; + + let channel = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await?; + + let mut role = self + .channel_role_for_user(channel_id, user_id, &*tx) + .await?; + + if role.is_none() { + if channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) { + channel_member::Entity::insert(channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(user_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Guest), + }) + .on_conflict( + OnConflict::columns([ + channel_member::Column::UserId, + channel_member::Column::ChannelId, + ]) + .update_columns([channel_member::Column::Accepted]) + .to_owned(), + ) + .exec(&*tx) + .await?; + + debug_assert!( + self.channel_role_for_user(channel_id, user_id, &*tx) + .await? + == Some(ChannelRole::Guest) + ); + + role = Some(ChannelRole::Guest); + joined = true; + } + } + + if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) { + Err(anyhow!("no such channel, or not allowed"))? + } + + let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); + let room_id = self + .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) + .await?; + + self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) + .await + .map(|jr| (jr, joined)) + } + + pub async fn join_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + environment: &str, + ) -> Result<(JoinRoom, bool)> { + self.transaction(move |tx| async move { + self.join_channel_internal(channel_id, user_id, connection, environment, &*tx) + .await + }) + .await + } + pub async fn set_channel_visibility( &self, channel_id: ChannelId, @@ -981,38 +1059,39 @@ impl Database { .await } - pub async fn get_or_create_channel_room( + pub(crate) async fn get_or_create_channel_room( &self, channel_id: ChannelId, live_kit_room: &str, - enviroment: &str, + environment: &str, + tx: &DatabaseTransaction, ) -> Result { - self.transaction(|tx| async move { - let tx = tx; + let room = room::Entity::find() + .filter(room::Column::ChannelId.eq(channel_id)) + .one(&*tx) + .await?; - let room = room::Entity::find() - .filter(room::Column::ChannelId.eq(channel_id)) - .one(&*tx) - .await?; + let room_id = if let Some(room) = room { + if let Some(env) = room.enviroment { + if &env != environment { + Err(anyhow!("must join using the {} release", env))?; + } + } + room.id + } else { + let result = room::Entity::insert(room::ActiveModel { + channel_id: ActiveValue::Set(Some(channel_id)), + live_kit_room: ActiveValue::Set(live_kit_room.to_string()), + enviroment: ActiveValue::Set(Some(environment.to_string())), + ..Default::default() + }) + .exec(&*tx) + .await?; - let room_id = if let Some(room) = room { - room.id - } else { - let result = room::Entity::insert(room::ActiveModel { - channel_id: ActiveValue::Set(Some(channel_id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - enviroment: ActiveValue::Set(Some(enviroment.to_string())), - ..Default::default() - }) - .exec(&*tx) - .await?; + result.last_insert_id + }; - result.last_insert_id - }; - - Ok(room_id) - }) - .await + Ok(room_id) } // Insert an edge from the given channel to the given other channel. diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 625615db5f..d2120495b0 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -300,99 +300,139 @@ impl Database { } } - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryParticipantIndices { - ParticipantIndex, + if channel_id.is_some() { + Err(anyhow!("tried to join channel call directly"))? } - let existing_participant_indices: Vec = room_participant::Entity::find() - .filter( - room_participant::Column::RoomId - .eq(room_id) - .and(room_participant::Column::ParticipantIndex.is_not_null()), - ) - .select_only() - .column(room_participant::Column::ParticipantIndex) - .into_values::<_, QueryParticipantIndices>() - .all(&*tx) + + let participant_index = self + .get_next_participant_index_internal(room_id, &*tx) .await?; - let mut participant_index = 0; - while existing_participant_indices.contains(&participant_index) { - participant_index += 1; - } - - if let Some(channel_id) = channel_id { - self.check_user_is_channel_member(channel_id, user_id, &*tx) - .await?; - - room_participant::Entity::insert_many([room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(user_id), + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(user_id)) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .set(room_participant::ActiveModel { + participant_index: ActiveValue::Set(Some(participant_index)), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(user_id), - calling_connection_id: ActiveValue::set(connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - participant_index: ActiveValue::Set(Some(participant_index)), ..Default::default() - }]) - .on_conflict( - OnConflict::columns([room_participant::Column::UserId]) - .update_columns([ - room_participant::Column::AnsweringConnectionId, - room_participant::Column::AnsweringConnectionServerId, - room_participant::Column::AnsweringConnectionLost, - room_participant::Column::ParticipantIndex, - ]) - .to_owned(), - ) + }) .exec(&*tx) .await?; - } else { - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .set(room_participant::ActiveModel { - participant_index: ActiveValue::Set(Some(participant_index)), - answering_connection_id: ActiveValue::set(Some(connection.id as i32)), - answering_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - answering_connection_lost: ActiveValue::set(false), - ..Default::default() - }) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("room does not exist or was already joined"))?; - } + if result.rows_affected == 0 { + Err(anyhow!("room does not exist or was already joined"))?; } let room = self.get_room(room_id, &tx).await?; - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_participants_internal(channel_id, &tx) - .await? - } else { - Vec::new() - }; Ok(JoinRoom { room, - channel_id, - channel_members, + channel_id: None, + channel_members: vec![], }) }) .await } + async fn get_next_participant_index_internal( + &self, + room_id: RoomId, + tx: &DatabaseTransaction, + ) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryParticipantIndices { + ParticipantIndex, + } + let existing_participant_indices: Vec = room_participant::Entity::find() + .filter( + room_participant::Column::RoomId + .eq(room_id) + .and(room_participant::Column::ParticipantIndex.is_not_null()), + ) + .select_only() + .column(room_participant::Column::ParticipantIndex) + .into_values::<_, QueryParticipantIndices>() + .all(&*tx) + .await?; + + let mut participant_index = 0; + while existing_participant_indices.contains(&participant_index) { + participant_index += 1; + } + + Ok(participant_index) + } + + pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result> { + self.transaction(|tx| async move { + let room: Option = room::Entity::find() + .filter(room::Column::Id.eq(room_id)) + .one(&*tx) + .await?; + + Ok(room.and_then(|room| room.channel_id)) + }) + .await + } + + pub(crate) async fn join_channel_room_internal( + &self, + channel_id: ChannelId, + room_id: RoomId, + user_id: UserId, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result { + let participant_index = self + .get_next_participant_index_internal(room_id, &*tx) + .await?; + + room_participant::Entity::insert_many([room_participant::ActiveModel { + room_id: ActiveValue::set(room_id), + user_id: ActiveValue::set(user_id), + answering_connection_id: ActiveValue::set(Some(connection.id as i32)), + answering_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + answering_connection_lost: ActiveValue::set(false), + calling_user_id: ActiveValue::set(user_id), + calling_connection_id: ActiveValue::set(connection.id as i32), + calling_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + participant_index: ActiveValue::Set(Some(participant_index)), + ..Default::default() + }]) + .on_conflict( + OnConflict::columns([room_participant::Column::UserId]) + .update_columns([ + room_participant::Column::AnsweringConnectionId, + room_participant::Column::AnsweringConnectionServerId, + room_participant::Column::AnsweringConnectionLost, + room_participant::Column::ParticipantIndex, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + let channel_members = self + .get_channel_participants_internal(channel_id, &tx) + .await?; + Ok(JoinRoom { + room, + channel_id: Some(channel_id), + channel_members, + }) + } + pub async fn rejoin_room( &self, rejoin_room: proto::RejoinRoom, diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index b969711232..9b6d8d1525 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -8,7 +8,7 @@ use crate::{ db::{ queries::channels::ChannelGraph, tests::{graph, TEST_RELEASE_CHANNEL}, - ChannelId, ChannelRole, Database, NewUserParams, UserId, + ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId, }, test_both_dbs, }; @@ -207,15 +207,11 @@ async fn test_joining_channels(db: &Arc) { .user_id; let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); - let room_1 = db - .get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL) - .await - .unwrap(); // can join a room with membership to its channel - let joined_room = db - .join_room( - room_1, + let (joined_room, _) = db + .join_channel( + channel_1, user_1, ConnectionId { owner_id, id: 1 }, TEST_RELEASE_CHANNEL, @@ -224,11 +220,12 @@ async fn test_joining_channels(db: &Arc) { .unwrap(); assert_eq!(joined_room.room.participants.len(), 1); + let room_id = RoomId::from_proto(joined_room.room.id); drop(joined_room); // cannot join a room without membership to its channel assert!(db .join_room( - room_1, + room_id, user_2, ConnectionId { owner_id, id: 1 }, TEST_RELEASE_CHANNEL diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c3d8a25ab7..26ad2f281a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -38,7 +38,7 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, + self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, JoinRoom, LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, @@ -977,6 +977,13 @@ async fn join_room( session: Session, ) -> Result<()> { let room_id = RoomId::from_proto(request.id); + + let channel_id = session.db().await.channel_id_for_room(room_id).await?; + + if let Some(channel_id) = channel_id { + return join_channel_internal(channel_id, Box::new(response), session).await; + } + let joined_room = { let room = session .db() @@ -992,16 +999,6 @@ async fn join_room( room.into_inner() }; - if let Some(channel_id) = joined_room.channel_id { - channel_updated( - channel_id, - &joined_room.room, - &joined_room.channel_members, - &session.peer, - &*session.connection_pool().await, - ) - } - for connection_id in session .connection_pool() .await @@ -1039,7 +1036,7 @@ async fn join_room( response.send(proto::JoinRoomResponse { room: Some(joined_room.room), - channel_id: joined_room.channel_id.map(|id| id.to_proto()), + channel_id: None, live_kit_connection_info, })?; @@ -2602,54 +2599,68 @@ async fn respond_to_channel_invite( db.respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; + if request.accept { + channel_membership_updated(db, channel_id, &session).await?; + } else { + let mut update = proto::UpdateChannels::default(); + update + .remove_channel_invitations + .push(channel_id.to_proto()); + session.peer.send(session.connection_id, update)?; + } + response.send(proto::Ack {})?; + + Ok(()) +} + +async fn channel_membership_updated( + db: tokio::sync::MutexGuard<'_, DbHandle>, + channel_id: ChannelId, + session: &Session, +) -> Result<(), crate::Error> { let mut update = proto::UpdateChannels::default(); update .remove_channel_invitations .push(channel_id.to_proto()); - if request.accept { - let result = db.get_channel_for_user(channel_id, session.user_id).await?; - update - .channels - .extend( - result - .channels - .channels - .into_iter() - .map(|channel| proto::Channel { - id: channel.id.to_proto(), - visibility: channel.visibility.into(), - name: channel.name, - }), - ); - 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 - .extend( - result - .channel_participants - .into_iter() - .map(|(channel_id, user_ids)| proto::ChannelParticipants { - channel_id: channel_id.to_proto(), - participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), - }), - ); - update - .channel_permissions - .extend( - result - .channels_with_admin_privileges - .into_iter() - .map(|channel_id| proto::ChannelPermission { - channel_id: channel_id.to_proto(), - role: proto::ChannelRole::Admin.into(), - }), - ); - } - session.peer.send(session.connection_id, update)?; - response.send(proto::Ack {})?; + let result = db.get_channel_for_user(channel_id, session.user_id).await?; + update.channels.extend( + result + .channels + .channels + .into_iter() + .map(|channel| proto::Channel { + id: channel.id.to_proto(), + visibility: channel.visibility.into(), + name: channel.name, + }), + ); + 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 + .extend( + result + .channel_participants + .into_iter() + .map(|(channel_id, user_ids)| proto::ChannelParticipants { + channel_id: channel_id.to_proto(), + participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(), + }), + ); + update + .channel_permissions + .extend( + result + .channels_with_admin_privileges + .into_iter() + .map(|channel_id| proto::ChannelPermission { + channel_id: channel_id.to_proto(), + role: proto::ChannelRole::Admin.into(), + }), + ); + session.peer.send(session.connection_id, update)?; Ok(()) } @@ -2659,19 +2670,35 @@ async fn join_channel( session: Session, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); - let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); + join_channel_internal(channel_id, Box::new(response), session).await +} +trait JoinChannelInternalResponse { + fn send(self, result: proto::JoinRoomResponse) -> Result<()>; +} +impl JoinChannelInternalResponse for Response { + fn send(self, result: proto::JoinRoomResponse) -> Result<()> { + Response::::send(self, result) + } +} +impl JoinChannelInternalResponse for Response { + fn send(self, result: proto::JoinRoomResponse) -> Result<()> { + Response::::send(self, result) + } +} + +async fn join_channel_internal( + channel_id: ChannelId, + response: Box, + session: Session, +) -> Result<()> { let joined_room = { leave_room_for_session(&session).await?; let db = session.db().await; - let room_id = db - .get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME) - .await?; - - let joined_room = db - .join_room( - room_id, + let (joined_room, joined_channel) = db + .join_channel( + channel_id, session.user_id, session.connection_id, RELEASE_CHANNEL_NAME.as_str(), @@ -2698,9 +2725,13 @@ async fn join_channel( live_kit_connection_info, })?; + if joined_channel { + channel_membership_updated(db, channel_id, &session).await? + } + room_updated(&joined_room.room, &session.peer); - joined_room.into_inner() + joined_room }; channel_updated( @@ -2712,7 +2743,6 @@ async fn join_channel( ); update_user_contacts(session.user_id, &session).await?; - Ok(()) } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 95a672e76c..1700dfc5d3 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -912,6 +912,58 @@ async fn test_lost_channel_creation( ], ); } +#[gpui::test] +async fn test_guest_access( + 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 channels = server + .make_channel_tree(&[("channel-a", None)], (&client_a, cx_a)) + .await; + let channel_a_id = channels[0]; + + let active_call_b = cx_b.read(ActiveCall::global); + + // should not be allowed to join + assert!(active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .await + .is_err()); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_a_id, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert!(client_b + .channel_store() + .update(cx_b, |channel_store, _| channel_store + .channel_for_id(channel_a_id) + .is_some())); + + client_a.channel_store().update(cx_a, |channel_store, _| { + let participants = channel_store.channel_participants(channel_a_id); + assert_eq!(participants.len(), 1); + assert_eq!(participants[0].id, client_b.user_id().unwrap()); + }) +} #[gpui::test] async fn test_channel_moving( diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index bf04e4f7e6..da6edbde69 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,4 @@ -use channel::{Channel, ChannelId, ChannelMembership, ChannelStore}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::{ proto::{self, ChannelRole, ChannelVisibility}, User, UserId, UserStore, From 2feb091961b2c0b719cb546c39cd1752590aea38 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 16 Oct 2023 16:11:00 -0600 Subject: [PATCH 35/60] Ensure that invitees do not have permissions They have to accept the invite, (which joining the channel will do), first. --- crates/collab/src/db/queries/channels.rs | 264 +++++++++++--------- crates/collab/src/db/tests/channel_tests.rs | 72 +++--- crates/collab/src/rpc.rs | 35 ++- crates/collab/src/tests/channel_tests.rs | 63 ++++- 4 files changed, 256 insertions(+), 178 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index d4276603f9..e3a6170452 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -88,80 +88,87 @@ impl Database { .await } - pub async fn join_channel_internal( - &self, - channel_id: ChannelId, - user_id: UserId, - connection: ConnectionId, - environment: &str, - tx: &DatabaseTransaction, - ) -> Result<(JoinRoom, bool)> { - let mut joined = false; - - let channel = channel::Entity::find() - .filter(channel::Column::Id.eq(channel_id)) - .one(&*tx) - .await?; - - let mut role = self - .channel_role_for_user(channel_id, user_id, &*tx) - .await?; - - if role.is_none() { - if channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) { - channel_member::Entity::insert(channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(user_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Guest), - }) - .on_conflict( - OnConflict::columns([ - channel_member::Column::UserId, - channel_member::Column::ChannelId, - ]) - .update_columns([channel_member::Column::Accepted]) - .to_owned(), - ) - .exec(&*tx) - .await?; - - debug_assert!( - self.channel_role_for_user(channel_id, user_id, &*tx) - .await? - == Some(ChannelRole::Guest) - ); - - role = Some(ChannelRole::Guest); - joined = true; - } - } - - if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) { - Err(anyhow!("no such channel, or not allowed"))? - } - - let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); - let room_id = self - .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) - .await?; - - self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) - .await - .map(|jr| (jr, joined)) - } - pub async fn join_channel( &self, channel_id: ChannelId, user_id: UserId, connection: ConnectionId, environment: &str, - ) -> Result<(JoinRoom, bool)> { + ) -> Result<(JoinRoom, Option)> { self.transaction(move |tx| async move { - self.join_channel_internal(channel_id, user_id, connection, environment, &*tx) + let mut joined_channel_id = None; + + let channel = channel::Entity::find() + .filter(channel::Column::Id.eq(channel_id)) + .one(&*tx) + .await?; + + let mut role = self + .channel_role_for_user(channel_id, user_id, &*tx) + .await?; + + if role.is_none() && channel.is_some() { + if let Some(invitation) = self + .pending_invite_for_channel(channel_id, user_id, &*tx) + .await? + { + // note, this may be a parent channel + joined_channel_id = Some(invitation.channel_id); + role = Some(invitation.role); + + channel_member::Entity::update(channel_member::ActiveModel { + accepted: ActiveValue::Set(true), + ..invitation.into_active_model() + }) + .exec(&*tx) + .await?; + + debug_assert!( + self.channel_role_for_user(channel_id, user_id, &*tx) + .await? + == role + ); + } + } + if role.is_none() + && channel.as_ref().map(|c| c.visibility) == Some(ChannelVisibility::Public) + { + let channel_id_to_join = self + .most_public_ancestor_for_channel(channel_id, &*tx) + .await? + .unwrap_or(channel_id); + role = Some(ChannelRole::Guest); + joined_channel_id = Some(channel_id_to_join); + + channel_member::Entity::insert(channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id_to_join), + user_id: ActiveValue::Set(user_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Guest), + }) + .exec(&*tx) + .await?; + + debug_assert!( + self.channel_role_for_user(channel_id, user_id, &*tx) + .await? + == role + ); + } + + if channel.is_none() || role.is_none() || role == Some(ChannelRole::Banned) { + Err(anyhow!("no such channel, or not allowed"))? + } + + let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); + let room_id = self + .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) + .await?; + + self.join_channel_room_internal(channel_id, room_id, user_id, connection, &*tx) .await + .map(|jr| (jr, joined_channel_id)) }) .await } @@ -624,29 +631,29 @@ impl Database { admin_id: UserId, for_user: UserId, role: ChannelRole, - ) -> Result<()> { + ) -> Result { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel_id, admin_id, &*tx) .await?; - let result = channel_member::Entity::update_many() + let membership = channel_member::Entity::find() .filter( channel_member::Column::ChannelId .eq(channel_id) .and(channel_member::Column::UserId.eq(for_user)), ) - .set(channel_member::ActiveModel { - role: ActiveValue::set(role), - ..Default::default() - }) - .exec(&*tx) + .one(&*tx) .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } + let Some(membership) = membership else { + Err(anyhow!("no such member"))? + }; - Ok(()) + let mut update = membership.into_active_model(); + update.role = ActiveValue::Set(role); + let updated = channel_member::Entity::update(update).exec(&*tx).await?; + + Ok(updated) }) .await } @@ -844,6 +851,52 @@ impl Database { } } + pub async fn pending_invite_for_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result> { + let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; + + let row = channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channel_ids)) + .filter(channel_member::Column::UserId.eq(user_id)) + .filter(channel_member::Column::Accepted.eq(false)) + .one(&*tx) + .await?; + + Ok(row) + } + + pub async fn most_public_ancestor_for_channel( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; + + let rows = channel::Entity::find() + .filter(channel::Column::Id.is_in(channel_ids.clone())) + .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) + .all(&*tx) + .await?; + + let mut visible_channels: HashSet = HashSet::default(); + + for row in rows { + visible_channels.insert(row.id); + } + + for ancestor in channel_ids.into_iter().rev() { + if visible_channels.contains(&ancestor) { + return Ok(Some(ancestor)); + } + } + + Ok(None) + } + pub async fn channel_role_for_user( &self, channel_id: ChannelId, @@ -864,7 +917,8 @@ impl Database { .filter( channel_member::Column::ChannelId .is_in(channel_ids) - .and(channel_member::Column::UserId.eq(user_id)), + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Accepted.eq(true)), ) .select_only() .column(channel_member::Column::ChannelId) @@ -1009,52 +1063,22 @@ impl Database { Ok(results) } - /// Returns the channel with the given ID and: - /// - true if the user is a member - /// - false if the user hasn't accepted the invitation yet - pub async fn get_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { + /// Returns the channel with the given ID + pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { - let tx = tx; + self.check_user_is_channel_participant(channel_id, user_id, &*tx) + .await?; let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + let Some(channel) = channel else { + Err(anyhow!("no such channel"))? + }; - if let Some(channel) = channel { - if self - .check_user_is_channel_member(channel_id, user_id, &*tx) - .await - .is_err() - { - return Ok(None); - } - - let channel_membership = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)), - ) - .one(&*tx) - .await?; - - let is_accepted = channel_membership - .map(|membership| membership.accepted) - .unwrap_or(false); - - Ok(Some(( - Channel { - id: channel.id, - visibility: channel.visibility, - name: channel.name, - }, - is_accepted, - ))) - } else { - Ok(None) - } + Ok(Channel { + id: channel.id, + visibility: channel.visibility, + name: channel.name, + }) }) .await } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 9b6d8d1525..f08b1554bc 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -51,7 +51,7 @@ async fn test_channels(db: &Arc) { let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); // Make sure that people cannot read channels they haven't been invited to - assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none()); + assert!(db.get_channel(zed_id, b_id).await.is_err()); db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member) .await @@ -157,7 +157,7 @@ async fn test_channels(db: &Arc) { // Remove a single channel db.delete_channel(crdb_id, a_id).await.unwrap(); - assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(crdb_id, a_id).await.is_err()); // Remove a channel tree let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap(); @@ -165,9 +165,9 @@ async fn test_channels(db: &Arc) { assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); assert_eq!(user_ids, &[a_id]); - assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); + assert!(db.get_channel(rust_id, a_id).await.is_err()); + assert!(db.get_channel(cargo_id, a_id).await.is_err()); + assert!(db.get_channel(cargo_ra_id, a_id).await.is_err()); } test_both_dbs!( @@ -381,11 +381,7 @@ async fn test_channel_renames(db: &Arc) { let zed_archive_id = zed_id; - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); + let channel = db.get_channel(zed_archive_id, user_1).await.unwrap(); assert_eq!(channel.name, "zed-archive"); let non_permissioned_rename = db @@ -860,12 +856,6 @@ async fn test_user_is_channel_participant(db: &Arc) { }) .await .unwrap(); - db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await - }) - .await - .unwrap(); let members = db .get_channel_participant_details(vim_channel, admin) @@ -896,6 +886,13 @@ async fn test_user_is_channel_participant(db: &Arc) { .await .unwrap(); + db.transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, guest, &*tx) + .await + }) + .await + .unwrap(); + let channels = db.get_channels_for_user(guest).await.unwrap().channels; assert_dag(channels, &[(vim_channel, None)]); let channels = db.get_channels_for_user(member).await.unwrap().channels; @@ -953,29 +950,7 @@ async fn test_user_is_channel_participant(db: &Arc) { .await .unwrap(); - db.transaction(|tx| async move { - db.check_user_is_channel_participant(zed_channel, guest, &*tx) - .await - }) - .await - .unwrap(); - assert!(db - .transaction(|tx| async move { - db.check_user_is_channel_participant(active_channel, guest, &*tx) - .await - }) - .await - .is_err(),); - - db.transaction(|tx| async move { - db.check_user_is_channel_participant(vim_channel, guest, &*tx) - .await - }) - .await - .unwrap(); - // currently people invited to parent channels are not shown here - // (though they *do* have permissions!) let members = db .get_channel_participant_details(vim_channel, admin) .await @@ -1000,6 +975,27 @@ async fn test_user_is_channel_participant(db: &Arc) { .await .unwrap(); + db.transaction(|tx| async move { + db.check_user_is_channel_participant(zed_channel, guest, &*tx) + .await + }) + .await + .unwrap(); + assert!(db + .transaction(|tx| async move { + db.check_user_is_channel_participant(active_channel, guest, &*tx) + .await + }) + .await + .is_err(),); + + db.transaction(|tx| async move { + db.check_user_is_channel_participant(vim_channel, guest, &*tx) + .await + }) + .await + .unwrap(); + let members = db .get_channel_participant_details(vim_channel, admin) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 26ad2f281a..4b33550c39 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -38,7 +38,7 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, JoinRoom, + self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, @@ -2289,10 +2289,7 @@ async fn invite_channel_member( ) .await?; - let (channel, _) = db - .get_channel(channel_id, session.user_id) - .await? - .ok_or_else(|| anyhow!("channel not found"))?; + let channel = db.get_channel(channel_id, session.user_id).await?; let mut update = proto::UpdateChannels::default(); update.channel_invitations.push(proto::Channel { @@ -2380,21 +2377,19 @@ async fn set_channel_member_role( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - db.set_channel_member_role( - channel_id, - session.user_id, - member_id, - request.role().into(), - ) - .await?; + let channel_member = db + .set_channel_member_role( + channel_id, + session.user_id, + member_id, + request.role().into(), + ) + .await?; - let (channel, has_accepted) = db - .get_channel(channel_id, member_id) - .await? - .ok_or_else(|| anyhow!("channel not found"))?; + let channel = db.get_channel(channel_id, session.user_id).await?; let mut update = proto::UpdateChannels::default(); - if has_accepted { + if channel_member.accepted { update.channel_permissions.push(proto::ChannelPermission { channel_id: channel.id.to_proto(), role: request.role, @@ -2724,9 +2719,11 @@ async fn join_channel_internal( channel_id: joined_room.channel_id.map(|id| id.to_proto()), live_kit_connection_info, })?; + dbg!("Joined channel", &joined_channel); - if joined_channel { - channel_membership_updated(db, channel_id, &session).await? + if let Some(joined_channel) = joined_channel { + dbg!("CMU"); + channel_membership_updated(db, joined_channel, &session).await? } room_updated(&joined_room.room, &session.peer); diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 1700dfc5d3..1bb8c92ac8 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -7,7 +7,7 @@ use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{ - proto::{self}, + proto::{self, ChannelRole}, RECEIVE_TIMEOUT, }; use std::sync::Arc; @@ -965,6 +965,67 @@ async fn test_guest_access( }) } +#[gpui::test] +async fn test_invite_access( + 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 channels = server + .make_channel_tree( + &[("channel-a", None), ("channel-b", Some("channel-a"))], + (&client_a, cx_a), + ) + .await; + let channel_a_id = channels[0]; + let channel_b_id = channels[0]; + + let active_call_b = cx_b.read(ActiveCall::global); + + // should not be allowed to join + assert!(active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) + .await + .is_err()); + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.invite_member( + channel_a_id, + client_b.user_id().unwrap(), + ChannelRole::Member, + cx, + ) + }) + .await + .unwrap(); + + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b.channel_store().update(cx_b, |channel_store, _| { + assert!(channel_store.channel_for_id(channel_b_id).is_some()); + assert!(channel_store.channel_for_id(channel_a_id).is_some()); + }); + + client_a.channel_store().update(cx_a, |channel_store, _| { + let participants = channel_store.channel_participants(channel_b_id); + assert_eq!(participants.len(), 1); + assert_eq!(participants[0].id, client_b.user_id().unwrap()); + }) +} + #[gpui::test] async fn test_channel_moving( deterministic: Arc, From 6ffbc3a0f52bd94751393ad1f0217b9692cfa230 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 16 Oct 2023 20:03:44 -0600 Subject: [PATCH 36/60] Allow pasting ZED urls in the command palette in development --- Cargo.lock | 2 + crates/collab/src/db/queries/channels.rs | 2 +- crates/command_palette/Cargo.toml | 1 + crates/command_palette/src/command_palette.rs | 17 +- crates/workspace/src/workspace.rs | 1 + crates/zed-actions/Cargo.toml | 1 + crates/zed-actions/src/lib.rs | 15 +- crates/zed/src/main.rs | 206 +----------------- crates/zed/src/open_listener.rs | 202 ++++++++++++++++- crates/zed/src/zed.rs | 6 + 10 files changed, 245 insertions(+), 208 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72ee771f5d..f68cd22ae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1623,6 +1623,7 @@ dependencies = [ "theme", "util", "workspace", + "zed-actions", ] [[package]] @@ -10213,6 +10214,7 @@ name = "zed-actions" version = "0.1.0" dependencies = [ "gpui", + "serde", ] [[package]] diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index e3a6170452..b10cbd14f1 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -979,7 +979,7 @@ impl Database { }) } - /// Returns the channel ancestors, include itself, deepest first + /// Returns the channel ancestors in arbitrary order pub async fn get_channel_ancestors( &self, channel_id: ChannelId, diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 95ba452c14..b42a3b5f41 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -19,6 +19,7 @@ settings = { path = "../settings" } util = { path = "../util" } theme = { path = "../theme" } workspace = { path = "../workspace" } +zed-actions = { path = "../zed-actions" } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 10c9ba7b86..9b74c13a71 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -6,8 +6,12 @@ use gpui::{ }; use picker::{Picker, PickerDelegate, PickerEvent}; use std::cmp::{self, Reverse}; -use util::ResultExt; +use util::{ + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME}, + ResultExt, +}; use workspace::Workspace; +use zed_actions::OpenZedURL; pub fn init(cx: &mut AppContext) { cx.add_action(toggle_command_palette); @@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate { ) .await }; - let intercept_result = cx.read(|cx| { + let mut intercept_result = cx.read(|cx| { if cx.has_global::() { cx.global::()(&query, cx) } else { None } }); + if *RELEASE_CHANNEL == ReleaseChannel::Dev { + if parse_zed_link(&query).is_some() { + intercept_result = Some(CommandInterceptResult { + action: OpenZedURL { url: query.clone() }.boxed_clone(), + string: query.clone(), + positions: vec![], + }) + } + } if let Some(CommandInterceptResult { action, string, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8b068fa10c..710883d7cc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -288,6 +288,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_global_action(restart); cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::add_folder_to_project); + cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); diff --git a/crates/zed-actions/Cargo.toml b/crates/zed-actions/Cargo.toml index b3fe3cbb53..353041264a 100644 --- a/crates/zed-actions/Cargo.toml +++ b/crates/zed-actions/Cargo.toml @@ -8,3 +8,4 @@ publish = false [dependencies] gpui = { path = "../gpui" } +serde.workspace = true diff --git a/crates/zed-actions/src/lib.rs b/crates/zed-actions/src/lib.rs index bcd086924d..df6405a4b1 100644 --- a/crates/zed-actions/src/lib.rs +++ b/crates/zed-actions/src/lib.rs @@ -1,4 +1,7 @@ -use gpui::actions; +use std::sync::Arc; + +use gpui::{actions, impl_actions}; +use serde::Deserialize; actions!( zed, @@ -26,3 +29,13 @@ actions!( ResetDatabase, ] ); + +#[derive(Deserialize, Clone, PartialEq)] +pub struct OpenBrowser { + pub url: Arc, +} +#[derive(Deserialize, Clone, PartialEq)] +pub struct OpenZedURL { + pub url: String, +} +impl_actions!(zed, [OpenBrowser, OpenZedURL]); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f89a880c71..0e3bb6ef43 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,22 +3,16 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; -use cli::{ - ipc::{self, IpcSender}, - CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, -}; +use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{ self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, }; use db::kvp::KEY_VALUE_STORE; -use editor::{scroll::autoscroll::Autoscroll, Editor}; -use futures::{ - channel::{mpsc, oneshot}, - FutureExt, SinkExt, StreamExt, -}; +use editor::Editor; +use futures::StreamExt; use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task}; use isahc::{config::Configurable, Request}; -use language::{LanguageRegistry, Point}; +use language::LanguageRegistry; use log::LevelFilter; use node_runtime::RealNodeRuntime; use parking_lot::Mutex; @@ -28,7 +22,6 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file use simplelog::ConfigBuilder; use smol::process::Command; use std::{ - collections::HashMap, env, ffi::OsStr, fs::OpenOptions, @@ -42,11 +35,9 @@ use std::{ thread, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use sum_tree::Bias; use util::{ channel::{parse_zed_link, ReleaseChannel}, http::{self, HttpClient}, - paths::PathLikeWithPosition, }; use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; @@ -58,12 +49,9 @@ use zed::{ assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, only_instance::{ensure_only_instance, IsOnlyInstance}, + open_listener::{handle_cli_connection, OpenListener, OpenRequest}, }; -use crate::open_listener::{OpenListener, OpenRequest}; - -mod open_listener; - fn main() { let http = http::client(); init_paths(); @@ -113,6 +101,7 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); + cx.set_global(listener.clone()); let mut store = SettingsStore::default(); store @@ -729,189 +718,6 @@ async fn watch_languages(_: Arc, _: Arc) -> Option<()> #[cfg(not(debug_assertions))] fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} -fn connect_to_cli( - server_name: &str, -) -> Result<(mpsc::Receiver, IpcSender)> { - let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) - .context("error connecting to cli")?; - let (request_tx, request_rx) = ipc::channel::()?; - let (response_tx, response_rx) = ipc::channel::()?; - - handshake_tx - .send(IpcHandshake { - requests: request_tx, - responses: response_rx, - }) - .context("error sending ipc handshake")?; - - let (mut async_request_tx, async_request_rx) = - futures::channel::mpsc::channel::(16); - thread::spawn(move || { - while let Ok(cli_request) = request_rx.recv() { - if smol::block_on(async_request_tx.send(cli_request)).is_err() { - break; - } - } - Ok::<_, anyhow::Error>(()) - }); - - Ok((async_request_rx, response_tx)) -} - -async fn handle_cli_connection( - (mut requests, responses): (mpsc::Receiver, IpcSender), - app_state: Arc, - mut cx: AsyncAppContext, -) { - if let Some(request) = requests.next().await { - match request { - CliRequest::Open { paths, wait } => { - let mut caret_positions = HashMap::new(); - - let paths = if paths.is_empty() { - workspace::last_opened_workspace_paths() - .await - .map(|location| location.paths().to_vec()) - .unwrap_or_default() - } else { - paths - .into_iter() - .filter_map(|path_with_position_string| { - let path_with_position = PathLikeWithPosition::parse_str( - &path_with_position_string, - |path_str| { - Ok::<_, std::convert::Infallible>( - Path::new(path_str).to_path_buf(), - ) - }, - ) - .expect("Infallible"); - let path = path_with_position.path_like; - if let Some(row) = path_with_position.row { - if path.is_file() { - let row = row.saturating_sub(1); - let col = - path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } - } - Some(path) - }) - .collect() - }; - - let mut errored = false; - match cx - .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .await - { - Ok((workspace, items)) => { - let mut item_release_futures = Vec::new(); - - for (item, path) in items.into_iter().zip(&paths) { - match item { - Some(Ok(item)) => { - if let Some(point) = caret_positions.remove(path) { - if let Some(active_editor) = item.downcast::() { - active_editor - .downgrade() - .update(&mut cx, |editor, cx| { - let snapshot = - editor.snapshot(cx).display_snapshot; - let point = snapshot - .buffer_snapshot - .clip_point(point, Bias::Left); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }) - .log_err(); - } - } - - let released = oneshot::channel(); - cx.update(|cx| { - item.on_release( - cx, - Box::new(move |_| { - let _ = released.0.send(()); - }), - ) - .detach(); - }); - item_release_futures.push(released.1); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: format!("error opening {:?}: {}", path, err), - }) - .log_err(); - errored = true; - } - None => {} - } - } - - if wait { - let background = cx.background(); - let wait = async move { - if paths.is_empty() { - let (done_tx, done_rx) = oneshot::channel(); - if let Some(workspace) = workspace.upgrade(&cx) { - let _subscription = cx.update(|cx| { - cx.observe_release(&workspace, move |_, _| { - let _ = done_tx.send(()); - }) - }); - drop(workspace); - let _ = done_rx.await; - } - } else { - let _ = - futures::future::try_join_all(item_release_futures).await; - }; - } - .fuse(); - futures::pin_mut!(wait); - - loop { - // Repeatedly check if CLI is still open to avoid wasting resources - // waiting for files or workspaces to close. - let mut timer = background.timer(Duration::from_secs(1)).fuse(); - futures::select_biased! { - _ = wait => break, - _ = timer => { - if responses.send(CliResponse::Ping).is_err() { - break; - } - } - } - } - } - } - Err(error) => { - errored = true; - responses - .send(CliResponse::Stderr { - message: format!("error opening {:?}: {}", paths, error), - }) - .log_err(); - } - } - - responses - .send(CliResponse::Exit { - status: i32::from(errored), - }) - .log_err(); - } - } - } -} - pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { &[ ("Go to file", &file_finder::Toggle), diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index 9b416e14be..578d8cd69f 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -1,15 +1,26 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Context, Result}; +use cli::{ipc, IpcHandshake}; use cli::{ipc::IpcSender, CliRequest, CliResponse}; -use futures::channel::mpsc; +use editor::scroll::autoscroll::Autoscroll; +use editor::Editor; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::channel::{mpsc, oneshot}; +use futures::{FutureExt, SinkExt, StreamExt}; +use gpui::AsyncAppContext; +use language::{Bias, Point}; +use std::collections::HashMap; use std::ffi::OsStr; use std::os::unix::prelude::OsStrExt; +use std::path::Path; use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; use std::{path::PathBuf, sync::atomic::AtomicBool}; use util::channel::parse_zed_link; +use util::paths::PathLikeWithPosition; use util::ResultExt; - -use crate::connect_to_cli; +use workspace::AppState; pub enum OpenRequest { Paths { @@ -96,3 +107,186 @@ impl OpenListener { Some(OpenRequest::Paths { paths }) } } + +fn connect_to_cli( + server_name: &str, +) -> Result<(mpsc::Receiver, IpcSender)> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + handshake_tx + .send(IpcHandshake { + requests: request_tx, + responses: response_rx, + }) + .context("error sending ipc handshake")?; + + let (mut async_request_tx, async_request_rx) = + futures::channel::mpsc::channel::(16); + thread::spawn(move || { + while let Ok(cli_request) = request_rx.recv() { + if smol::block_on(async_request_tx.send(cli_request)).is_err() { + break; + } + } + Ok::<_, anyhow::Error>(()) + }); + + Ok((async_request_rx, response_tx)) +} + +pub async fn handle_cli_connection( + (mut requests, responses): (mpsc::Receiver, IpcSender), + app_state: Arc, + mut cx: AsyncAppContext, +) { + if let Some(request) = requests.next().await { + match request { + CliRequest::Open { paths, wait } => { + let mut caret_positions = HashMap::new(); + + let paths = if paths.is_empty() { + workspace::last_opened_workspace_paths() + .await + .map(|location| location.paths().to_vec()) + .unwrap_or_default() + } else { + paths + .into_iter() + .filter_map(|path_with_position_string| { + let path_with_position = PathLikeWithPosition::parse_str( + &path_with_position_string, + |path_str| { + Ok::<_, std::convert::Infallible>( + Path::new(path_str).to_path_buf(), + ) + }, + ) + .expect("Infallible"); + let path = path_with_position.path_like; + if let Some(row) = path_with_position.row { + if path.is_file() { + let row = row.saturating_sub(1); + let col = + path_with_position.column.unwrap_or(0).saturating_sub(1); + caret_positions.insert(path.clone(), Point::new(row, col)); + } + } + Some(path) + }) + .collect() + }; + + let mut errored = false; + match cx + .update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .await + { + Ok((workspace, items)) => { + let mut item_release_futures = Vec::new(); + + for (item, path) in items.into_iter().zip(&paths) { + match item { + Some(Ok(item)) => { + if let Some(point) = caret_positions.remove(path) { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = + editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(point, Bias::Left); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } + } + + let released = oneshot::channel(); + cx.update(|cx| { + item.on_release( + cx, + Box::new(move |_| { + let _ = released.0.send(()); + }), + ) + .detach(); + }); + item_release_futures.push(released.1); + } + Some(Err(err)) => { + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", path, err), + }) + .log_err(); + errored = true; + } + None => {} + } + } + + if wait { + let background = cx.background(); + let wait = async move { + if paths.is_empty() { + let (done_tx, done_rx) = oneshot::channel(); + if let Some(workspace) = workspace.upgrade(&cx) { + let _subscription = cx.update(|cx| { + cx.observe_release(&workspace, move |_, _| { + let _ = done_tx.send(()); + }) + }); + drop(workspace); + let _ = done_rx.await; + } + } else { + let _ = + futures::future::try_join_all(item_release_futures).await; + }; + } + .fuse(); + futures::pin_mut!(wait); + + loop { + // Repeatedly check if CLI is still open to avoid wasting resources + // waiting for files or workspaces to close. + let mut timer = background.timer(Duration::from_secs(1)).fuse(); + futures::select_biased! { + _ = wait => break, + _ = timer => { + if responses.send(CliResponse::Ping).is_err() { + break; + } + } + } + } + } + } + Err(error) => { + errored = true; + responses + .send(CliResponse::Stderr { + message: format!("error opening {:?}: {}", paths, error), + }) + .log_err(); + } + } + + responses + .send(CliResponse::Exit { + status: i32::from(errored), + }) + .log_err(); + } + } + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4e9a34c269..c2a218acae 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2,6 +2,7 @@ pub mod assets; pub mod languages; pub mod menus; pub mod only_instance; +pub mod open_listener; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -28,6 +29,7 @@ use gpui::{ AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle, }; pub use lsp; +use open_listener::OpenListener; pub use project; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; @@ -87,6 +89,10 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { }, ); cx.add_global_action(quit); + cx.add_global_action(move |action: &OpenZedURL, cx| { + cx.global::>() + .open_urls(vec![action.url.clone()]) + }); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { theme::adjust_font_size(cx, |size| *size += 1.0) From c12f0d26978420479c47c6e2e35463323aa88c75 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 15:33:59 -0600 Subject: [PATCH 37/60] Provisioning profiles for stable and preview --- .../{ => dev}/embedded.provisionprofile | Bin .../preview/embedded.provisionprofile | Bin 0 -> 12478 bytes ...able_Provisioning_Profile.provisionprofile | Bin 0 -> 12441 bytes script/bundle | 27 +++++++++++++----- 4 files changed, 20 insertions(+), 7 deletions(-) rename crates/zed/contents/{ => dev}/embedded.provisionprofile (100%) create mode 100644 crates/zed/contents/preview/embedded.provisionprofile create mode 100644 crates/zed/contents/stable/Zed_Stable_Provisioning_Profile.provisionprofile diff --git a/crates/zed/contents/embedded.provisionprofile b/crates/zed/contents/dev/embedded.provisionprofile similarity index 100% rename from crates/zed/contents/embedded.provisionprofile rename to crates/zed/contents/dev/embedded.provisionprofile diff --git a/crates/zed/contents/preview/embedded.provisionprofile b/crates/zed/contents/preview/embedded.provisionprofile new file mode 100644 index 0000000000000000000000000000000000000000..6eea317c373c93336526bb5403001254622237bd GIT binary patch literal 12478 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 a*7NeWFDQ&(klM8L$X$DlFNp3R9ByoeQ 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 literal 0 HcmV?d00001 diff --git a/script/bundle b/script/bundle index dc5022bea5..4775e15837 100755 --- a/script/bundle +++ b/script/bundle @@ -134,7 +134,7 @@ else cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" fi -cp crates/zed/contents/embedded.provisionprofile "${app_path}/Contents/" +cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/" if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" @@ -147,17 +147,30 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain # sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514 - /usr/bin/codesign --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks" -v + /usr/bin/codesign --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v /usr/bin/codesign --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v - /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/zed" -v + /usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v security default-keychain -s login.keychain else echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD" - echo "Performing an ad-hoc signature, but this bundle should not be distributed" - echo "If you see 'The application cannot be opened for an unexpected reason,' you likely don't have the necessary entitlements to run the application in your signing keychain" - echo "You will need to download a new signing key from developer.apple.com, add it to keychain, and export MACOS_SIGNING_KEY=" - codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v + if [[ "$local_only" = false ]]; then + echo "To create a self-signed local build use ./scripts/build.sh -ldf" + exit 1 + fi + + echo "====== WARNING ======" + echo "This bundle is being signed without all entitlements, some features (e.g. universal links) will not work" + echo "====== WARNING ======" + + # NOTE: if you need to test universal links you have a few paths forward: + # - create a PR and tag it with the `run-build-dmg` label, and download the .dmg file from there. + # - get a signing key for the MQ55VZLNZQ team from Nathan. + # - create your own signing key, and update references to MQ55VZLNZQ to your own team ID + # then comment out this line. + cat crates/zed/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements" + + codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v fi if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then From 162f6257165942371cf2ee13b8b12d4594cfb74f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 17 Oct 2023 02:16:17 -0700 Subject: [PATCH 38/60] Adjust chat permisisons to allow deletion for channel admins --- crates/collab/src/db/queries/messages.rs | 16 +++++++++++++++- crates/collab_ui/src/chat_panel.rs | 20 +++++++++++++------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index a48d425d90..aee67ec943 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -337,8 +337,22 @@ impl Database { .filter(channel_message::Column::SenderId.eq(user_id)) .exec(&*tx) .await?; + if result.rows_affected == 0 { - Err(anyhow!("no such message"))?; + if self + .check_user_is_channel_admin(channel_id, user_id, &*tx) + .await + .is_ok() + { + let result = channel_message::Entity::delete_by_id(message_id) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("no such message"))?; + } + } else { + Err(anyhow!("operation could not be completed"))?; + } } Ok(participant_connection_ids) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 1a17b48f19..a8c4006cb8 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -355,8 +355,12 @@ impl ChatPanel { } fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let (message, is_continuation, is_last) = { + let (message, is_continuation, is_last, is_admin) = { let active_chat = self.active_chat.as_ref().unwrap().0.read(cx); + let is_admin = self + .channel_store + .read(cx) + .is_user_admin(active_chat.channel().id); 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 @@ -366,6 +370,7 @@ impl ChatPanel { active_chat.message(ix).clone(), is_continuation, active_chat.message_count() == ix + 1, + is_admin, ) }; @@ -386,12 +391,13 @@ impl ChatPanel { }; let belongs_to_user = Some(message.sender.id) == self.client.user_id(); - let message_id_to_remove = - if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) { - Some(id) - } else { - None - }; + let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = + (message.id, belongs_to_user || is_admin) + { + Some(id) + } else { + None + }; enum MessageBackgroundHighlight {} MouseEventHandler::new::(ix, cx, |state, cx| { From a81484f13ff56f519fd98291c11c3ef20ddfd48a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 17 Oct 2023 02:22:34 -0700 Subject: [PATCH 39/60] Update IDs on interactive elements in LSP log viewer --- crates/language_tools/src/lsp_log.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index faed37a97c..383ca94851 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -685,6 +685,7 @@ impl View for LspLogToolbarItemView { }); let server_selected = current_server.is_some(); + enum LspLogScroll {} enum Menu {} let lsp_menu = Stack::new() .with_child(Self::render_language_server_menu_header( @@ -697,7 +698,7 @@ impl View for LspLogToolbarItemView { Overlay::new( MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() - .scrollable::(0, None, cx) + .scrollable::(0, None, cx) .with_children(menu_rows.into_iter().map(|row| { Self::render_language_server_menu_item( row.server_id, @@ -876,6 +877,7 @@ impl LspLogToolbarItemView { ) -> impl Element { enum ActivateLog {} enum ActivateRpcTrace {} + enum LanguageServerCheckbox {} Flex::column() .with_child({ @@ -921,7 +923,7 @@ impl LspLogToolbarItemView { .with_height(theme.toolbar_dropdown_menu.row_height), ) .with_child( - ui::checkbox_with_label::( + ui::checkbox_with_label::( Empty::new(), &theme.welcome.checkbox, rpc_trace_enabled, From 465d726bd4508490b993689073f08a764d178eca Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 17 Oct 2023 03:05:01 -0700 Subject: [PATCH 40/60] Minor adjustments --- .cargo/config.toml | 2 +- crates/channel/src/channel_store.rs | 1 - .../src/channel_store/channel_index.rs | 5 +- crates/collab/src/db/ids.rs | 9 +++ crates/collab/src/db/queries/channels.rs | 61 +++++++++---------- 5 files changed, 42 insertions(+), 36 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index e22bdb0f2c..9da6b3be08 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ xtask = "run --package xtask --" [build] # v0 mangling scheme provides more detailed backtraces around closures -rustflags = ["-C", "symbol-mangling-version=v0", "-C", "link-arg=-fuse-ld=/opt/homebrew/Cellar/llvm/16.0.6/bin/ld64.lld"] +rustflags = ["-C", "symbol-mangling-version=v0"] diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 57b183f7de..3e8fbafb6a 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -972,7 +972,6 @@ impl ChannelStore { let mut all_user_ids = Vec::new(); let channel_participants = payload.channel_participants; - dbg!(&channel_participants); for entry in &channel_participants { for user_id in entry.participant_user_ids.iter() { if let Err(ix) = all_user_ids.binary_search(user_id) { diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index 7b54d5dcd9..36379a3942 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -123,8 +123,9 @@ impl<'a> ChannelPathsInsertGuard<'a> { 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).visibility = channel_proto.visibility(); - Arc::make_mut(existing_channel).name = channel_proto.name; + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.visibility = channel_proto.visibility(); + existing_channel.name = channel_proto.name; } else { self.channels_by_id.insert( channel_proto.id, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 970d66d4cb..38240fd4c4 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -106,6 +106,15 @@ impl ChannelRole { Guest => false, } } + + pub fn max(&self, other: Self) -> Self { + match (self, other) { + (ChannelRole::Admin, _) | (_, ChannelRole::Admin) => ChannelRole::Admin, + (ChannelRole::Member, _) | (_, ChannelRole::Member) => ChannelRole::Member, + (ChannelRole::Banned, _) | (_, ChannelRole::Banned) => ChannelRole::Banned, + (ChannelRole::Guest, _) | (_, ChannelRole::Guest) => ChannelRole::Guest, + } + } } impl From for ChannelRole { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index b10cbd14f1..0dc197aa0b 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -209,7 +209,7 @@ impl Database { let mut channels_to_remove: HashSet = HashSet::default(); channels_to_remove.insert(channel_id); - let graph = self.get_channel_descendants_2([channel_id], &*tx).await?; + let graph = self.get_channel_descendants([channel_id], &*tx).await?; for edge in graph.iter() { channels_to_remove.insert(ChannelId::from_proto(edge.channel_id)); } @@ -218,7 +218,7 @@ impl Database { let mut channels_to_keep = channel_path::Entity::find() .filter( channel_path::Column::ChannelId - .is_in(channels_to_remove.clone()) + .is_in(channels_to_remove.iter().copied()) .and( channel_path::Column::IdPath .not_like(&format!("%/{}/%", channel_id)), @@ -243,7 +243,7 @@ impl Database { .await?; channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.clone())) + .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) .exec(&*tx) .await?; @@ -484,7 +484,7 @@ impl Database { tx: &DatabaseTransaction, ) -> Result { let mut edges = self - .get_channel_descendants_2(channel_memberships.iter().map(|m| m.channel_id), &*tx) + .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; let mut role_for_channel: HashMap = HashMap::default(); @@ -515,7 +515,7 @@ impl Database { let mut channels_to_remove: HashSet = HashSet::default(); let mut rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(role_for_channel.keys().cloned())) + .filter(channel::Column::Id.is_in(role_for_channel.keys().copied())) .stream(&*tx) .await?; @@ -877,7 +877,7 @@ impl Database { let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; let rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(channel_ids.clone())) + .filter(channel::Column::Id.is_in(channel_ids.iter().copied())) .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) .all(&*tx) .await?; @@ -928,40 +928,39 @@ impl Database { .stream(&*tx) .await?; - let mut is_admin = false; - let mut is_member = false; + let mut user_role: Option = None; + let max_role = |role| { + user_role + .map(|user_role| user_role.max(role)) + .get_or_insert(role); + }; + let mut is_participant = false; - let mut is_banned = false; let mut current_channel_visibility = None; // note these channels are not iterated in any particular order, // our current logic takes the highest permission available. while let Some(row) = rows.next().await { - let (ch_id, role, visibility): (ChannelId, ChannelRole, ChannelVisibility) = row?; + let (membership_channel, role, visibility): ( + ChannelId, + ChannelRole, + ChannelVisibility, + ) = row?; match role { - ChannelRole::Admin => is_admin = true, - ChannelRole::Member => is_member = true, - ChannelRole::Guest => { - if visibility == ChannelVisibility::Public { - is_participant = true - } + ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => max_role(role), + ChannelRole::Guest if visibility == ChannelVisibility::Public => { + is_participant = true } - ChannelRole::Banned => is_banned = true, + ChannelRole::Guest => {} } - if channel_id == ch_id { + if channel_id == membership_channel { current_channel_visibility = Some(visibility); } } // free up database connection drop(rows); - Ok(if is_admin { - Some(ChannelRole::Admin) - } else if is_member { - Some(ChannelRole::Member) - } else if is_banned { - Some(ChannelRole::Banned) - } else if is_participant { + if is_participant && user_role.is_none() { if current_channel_visibility.is_none() { current_channel_visibility = channel::Entity::find() .filter(channel::Column::Id.eq(channel_id)) @@ -970,13 +969,11 @@ impl Database { .map(|channel| channel.visibility); } if current_channel_visibility == Some(ChannelVisibility::Public) { - Some(ChannelRole::Guest) - } else { - None + user_role = Some(ChannelRole::Guest); } - } else { - None - }) + } + + Ok(user_role) } /// Returns the channel ancestors in arbitrary order @@ -1007,7 +1004,7 @@ impl Database { // Returns the channel desendants as a sorted list of edges for further processing. // The edges are sorted such that you will see unknown channel ids as children // before you see them as parents. - async fn get_channel_descendants_2( + async fn get_channel_descendants( &self, channel_ids: impl IntoIterator, tx: &DatabaseTransaction, From 851701cb6f1e50d0ccd272b107bad525eddf99f2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 09:41:34 -0600 Subject: [PATCH 41/60] Fix get_most_public_ancestor --- crates/channel/src/channel_store.rs | 25 ++++++ crates/collab/src/db/ids.rs | 9 +-- crates/collab/src/db/queries/channels.rs | 78 +++++++++---------- crates/collab/src/db/tests/channel_tests.rs | 48 ++++++++++++ .../src/collab_panel/channel_modal.rs | 14 +++- crates/command_palette/src/command_palette.rs | 2 +- 6 files changed, 126 insertions(+), 50 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 3e8fbafb6a..5fb7ddc72c 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -82,6 +82,31 @@ pub struct ChannelMembership { pub kind: proto::channel_member::Kind, pub role: proto::ChannelRole, } +impl ChannelMembership { + pub fn sort_key(&self) -> MembershipSortKey { + MembershipSortKey { + role_order: match self.role { + proto::ChannelRole::Admin => 0, + proto::ChannelRole::Member => 1, + proto::ChannelRole::Banned => 2, + proto::ChannelRole::Guest => 3, + }, + kind_order: match self.kind { + proto::channel_member::Kind::Member => 0, + proto::channel_member::Kind::AncestorMember => 1, + proto::channel_member::Kind::Invitee => 2, + }, + username_order: self.user.github_login.as_str(), + } + } +} + +#[derive(PartialOrd, Ord, PartialEq, Eq)] +pub struct MembershipSortKey<'a> { + role_order: u8, + kind_order: u8, + username_order: &'a str, +} pub enum ChannelEvent { ChannelCreated(ChannelId), diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 38240fd4c4..f0de4c255e 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -108,11 +108,10 @@ impl ChannelRole { } pub fn max(&self, other: Self) -> Self { - match (self, other) { - (ChannelRole::Admin, _) | (_, ChannelRole::Admin) => ChannelRole::Admin, - (ChannelRole::Member, _) | (_, ChannelRole::Member) => ChannelRole::Member, - (ChannelRole::Banned, _) | (_, ChannelRole::Banned) => ChannelRole::Banned, - (ChannelRole::Guest, _) | (_, ChannelRole::Guest) => ChannelRole::Guest, + if self.should_override(other) { + *self + } else { + other } } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 0dc197aa0b..a1a618c733 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1,5 +1,3 @@ -use std::cmp::Ordering; - use super::*; use rpc::proto::{channel_member::Kind, ChannelEdge}; @@ -544,6 +542,12 @@ impl Database { if !channels_to_remove.is_empty() { // Note: this code assumes each channel has one parent. + // If there are multiple valid public paths to a channel, + // e.g. + // If both of these paths are present (* indicating public): + // - zed* -> projects -> vim* + // - zed* -> conrad -> public-projects* -> vim* + // Users would only see one of them (based on edge sort order) let mut replacement_parent: HashMap = HashMap::default(); for ChannelEdge { parent_id, @@ -707,14 +711,14 @@ impl Database { } let mut user_details: HashMap = HashMap::default(); - while let Some(row) = stream.next().await { + while let Some(user_membership) = stream.next().await { let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): ( UserId, ChannelRole, bool, bool, ChannelVisibility, - ) = row?; + ) = user_membership?; let kind = match (is_direct_member, is_invite_accepted) { (true, true) => proto::channel_member::Kind::Member, (true, false) => proto::channel_member::Kind::Invitee, @@ -745,33 +749,7 @@ impl Database { } } - // sort by permissions descending, within each section, show members, then ancestor members, then invitees. - let mut results: Vec<(UserId, UserDetail)> = user_details.into_iter().collect(); - results.sort_by(|a, b| { - if a.1.channel_role.should_override(b.1.channel_role) { - return Ordering::Less; - } else if b.1.channel_role.should_override(a.1.channel_role) { - return Ordering::Greater; - } - - if a.1.kind == Kind::Member && b.1.kind != Kind::Member { - return Ordering::Less; - } else if b.1.kind == Kind::Member && a.1.kind != Kind::Member { - return Ordering::Greater; - } - - if a.1.kind == Kind::AncestorMember && b.1.kind != Kind::AncestorMember { - return Ordering::Less; - } else if b.1.kind == Kind::AncestorMember && a.1.kind != Kind::AncestorMember { - return Ordering::Greater; - } - - // would be nice to sort alphabetically instead of by user id. - // (or defer all sorting to the UI, but we need something to help the tests) - return a.0.cmp(&b.0); - }); - - Ok(results + Ok(user_details .into_iter() .map(|(user_id, details)| proto::ChannelMember { user_id: user_id.to_proto(), @@ -810,7 +788,7 @@ impl Database { user_id: UserId, tx: &DatabaseTransaction, ) -> Result<()> { - match self.channel_role_for_user(channel_id, user_id, tx).await? { + match dbg!(self.channel_role_for_user(channel_id, user_id, tx).await)? { Some(ChannelRole::Admin) => Ok(()), Some(ChannelRole::Member) | Some(ChannelRole::Banned) @@ -874,10 +852,26 @@ impl Database { channel_id: ChannelId, tx: &DatabaseTransaction, ) -> Result> { - let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; + // Note: if there are many paths to a channel, this will return just one + let arbitary_path = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel_id)) + .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc) + .one(tx) + .await?; + + let Some(path) = arbitary_path else { + return Ok(None); + }; + + let ancestor_ids: Vec = path + .id_path + .trim_matches('/') + .split('/') + .map(|id| ChannelId::from_proto(id.parse().unwrap())) + .collect(); let rows = channel::Entity::find() - .filter(channel::Column::Id.is_in(channel_ids.iter().copied())) + .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied())) .filter(channel::Column::Visibility.eq(ChannelVisibility::Public)) .all(&*tx) .await?; @@ -888,7 +882,7 @@ impl Database { visible_channels.insert(row.id); } - for ancestor in channel_ids.into_iter().rev() { + for ancestor in ancestor_ids { if visible_channels.contains(&ancestor) { return Ok(Some(ancestor)); } @@ -929,11 +923,6 @@ impl Database { .await?; let mut user_role: Option = None; - let max_role = |role| { - user_role - .map(|user_role| user_role.max(role)) - .get_or_insert(role); - }; let mut is_participant = false; let mut current_channel_visibility = None; @@ -946,8 +935,15 @@ impl Database { ChannelRole, ChannelVisibility, ) = row?; + match role { - ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => max_role(role), + ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => { + if let Some(users_role) = user_role { + user_role = Some(users_role.max(role)); + } else { + user_role = Some(role) + } + } ChannelRole::Guest if visibility == ChannelVisibility::Public => { is_participant = true } diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index f08b1554bc..ac272726da 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -1028,6 +1028,54 @@ async fn test_user_is_channel_participant(db: &Arc) { ) } +test_both_dbs!( + test_user_joins_correct_channel, + test_user_joins_correct_channel_postgres, + test_user_joins_correct_channel_sqlite +); + +async fn test_user_joins_correct_channel(db: &Arc) { + let admin = new_test_user(db, "admin@example.com").await; + + let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); + + let active_channel = db + .create_channel("active", Some(zed_channel), admin) + .await + .unwrap(); + + let vim_channel = db + .create_channel("vim", Some(active_channel), admin) + .await + .unwrap(); + + let vim2_channel = db + .create_channel("vim2", Some(vim_channel), admin) + .await + .unwrap(); + + db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin) + .await + .unwrap(); + + let most_public = db + .transaction( + |tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await }, + ) + .await + .unwrap(); + + assert_eq!(most_public, Some(zed_channel)) +} + #[track_caller] fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option)]) { let mut actual_map: HashMap> = HashMap::default(); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index da6edbde69..0ccf0894b2 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -100,11 +100,14 @@ impl ChannelModal { let channel_id = self.channel_id; cx.spawn(|this, mut cx| async move { if mode == Mode::ManageMembers { - let members = channel_store + let mut members = channel_store .update(&mut cx, |channel_store, cx| { channel_store.get_channel_member_details(channel_id, cx) }) .await?; + + members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key())); + this.update(&mut cx, |this, cx| { this.picker .update(cx, |picker, _| picker.delegate_mut().members = members); @@ -675,11 +678,16 @@ impl ChannelModalDelegate { invite_member.await?; this.update(&mut cx, |this, cx| { - this.delegate_mut().members.push(ChannelMembership { + let new_member = ChannelMembership { user, kind: proto::channel_member::Kind::Invitee, role: ChannelRole::Member, - }); + }; + let members = &mut this.delegate_mut().members; + match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) { + Ok(ix) | Err(ix) => members.insert(ix, new_member), + } + cx.notify(); }) }) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 9b74c13a71..ce762876a4 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -7,7 +7,7 @@ use gpui::{ use picker::{Picker, PickerDelegate, PickerEvent}; use std::cmp::{self, Reverse}; use util::{ - channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME}, + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, }; use workspace::Workspace; From 2456c077f698d72d411543f33fc1d3da3df14c95 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 10:01:31 -0600 Subject: [PATCH 42/60] Fix channel test ordering --- crates/collab/src/db/tests/channel_tests.rs | 33 ++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index ac272726da..40842aff5c 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -281,10 +281,12 @@ async fn test_channel_invites(db: &Arc) { assert_eq!(user_3_invites, &[channel_1_1]); - let members = db + let mut members = db .get_channel_participant_details(channel_1_1, user_1) .await .unwrap(); + + members.sort_by_key(|member| member.user_id); assert_eq!( members, &[ @@ -293,16 +295,16 @@ async fn test_channel_invites(db: &Arc) { kind: proto::channel_member::Kind::Member.into(), role: proto::ChannelRole::Admin.into(), }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - role: proto::ChannelRole::Admin.into(), - }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), role: proto::ChannelRole::Member.into(), }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + role: proto::ChannelRole::Admin.into(), + }, ] ); @@ -857,10 +859,13 @@ async fn test_user_is_channel_participant(db: &Arc) { .await .unwrap(); - let members = db + let mut members = db .get_channel_participant_details(vim_channel, admin) .await .unwrap(); + + members.sort_by_key(|member| member.user_id); + assert_eq!( members, &[ @@ -912,11 +917,13 @@ async fn test_user_is_channel_participant(db: &Arc) { .await .is_err()); - let members = db + let mut members = db .get_channel_participant_details(vim_channel, admin) .await .unwrap(); + members.sort_by_key(|member| member.user_id); + assert_eq!( members, &[ @@ -951,10 +958,13 @@ async fn test_user_is_channel_participant(db: &Arc) { .unwrap(); // currently people invited to parent channels are not shown here - let members = db + let mut members = db .get_channel_participant_details(vim_channel, admin) .await .unwrap(); + + members.sort_by_key(|member| member.user_id); + assert_eq!( members, &[ @@ -996,10 +1006,13 @@ async fn test_user_is_channel_participant(db: &Arc) { .await .unwrap(); - let members = db + let mut members = db .get_channel_participant_details(vim_channel, admin) .await .unwrap(); + + members.sort_by_key(|member| member.user_id); + assert_eq!( members, &[ From 3412becfc53ad2551412f586ef58b2c589fe3810 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 10:15:20 -0600 Subject: [PATCH 43/60] Fix some tests --- crates/channel/src/channel_store_tests.rs | 16 ++++++++-------- crates/collab/src/db/queries/channels.rs | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index ea47c7c7b7..23f2e11a03 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -18,12 +18,12 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 1, name: "b".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 2, name: "a".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, ], channel_permissions: vec![proto::ChannelPermission { @@ -51,12 +51,12 @@ fn test_update_channels(cx: &mut AppContext) { proto::Channel { id: 3, name: "x".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 4, name: "y".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, ], insert_edge: vec![ @@ -96,17 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { proto::Channel { id: 0, name: "a".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 1, name: "b".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, proto::Channel { id: 2, name: "c".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }, ], insert_edge: vec![ @@ -165,7 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) { channels: vec![proto::Channel { id: channel_id, name: "the-channel".to_string(), - visibility: proto::ChannelVisibility::ChannelMembers as i32, + visibility: proto::ChannelVisibility::Members as i32, }], ..Default::default() }); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index a1a618c733..07fe219330 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -143,7 +143,8 @@ impl Database { channel_id: ActiveValue::Set(channel_id_to_join), user_id: ActiveValue::Set(user_id), accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Guest), + // TODO: change this back to Guest. + role: ActiveValue::Set(ChannelRole::Member), }) .exec(&*tx) .await?; From 5b39fc81232f4c7ed9aa94649f0e951f292b5f6d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 10:24:47 -0600 Subject: [PATCH 44/60] Temporarily join public channels as a member --- crates/collab/src/db/queries/channels.rs | 5 +++-- crates/collab/src/rpc.rs | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 07fe219330..ee989b2ea0 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -135,7 +135,8 @@ impl Database { .most_public_ancestor_for_channel(channel_id, &*tx) .await? .unwrap_or(channel_id); - role = Some(ChannelRole::Guest); + // TODO: change this back to Guest. + role = Some(ChannelRole::Member); joined_channel_id = Some(channel_id_to_join); channel_member::Entity::insert(channel_member::ActiveModel { @@ -789,7 +790,7 @@ impl Database { user_id: UserId, tx: &DatabaseTransaction, ) -> Result<()> { - match dbg!(self.channel_role_for_user(channel_id, user_id, tx).await)? { + match self.channel_role_for_user(channel_id, user_id, tx).await? { Some(ChannelRole::Admin) => Ok(()), Some(ChannelRole::Member) | Some(ChannelRole::Banned) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 575c9d8871..15ea3b24e1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2720,10 +2720,8 @@ async fn join_channel_internal( channel_id: joined_room.channel_id.map(|id| id.to_proto()), live_kit_connection_info, })?; - dbg!("Joined channel", &joined_channel); if let Some(joined_channel) = joined_channel { - dbg!("CMU"); channel_membership_updated(db, joined_channel, &session).await? } From 31241f48bef316eacd5c0e874a96357755f12948 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:56:03 +0200 Subject: [PATCH 45/60] workspace: Do not scan for .gitignore files if a .git directory is encountered along the way (#3135) Partially fixes zed-industries/community#575 This PR will see one more fix to the case I've spotted while working on this: namely, if a project has several nested repositories, e.g for a structure: /a /a/.git/ /a/.gitignore /a/b/ /a/b/.git/ /a/b/.gitignore /b/ should not account for a's .gitignore at all - which is sort of similar to the fix in commit #c416fbb, but for the paths in the project. The release note is kinda bad, I'll try to reword it too. - [ ] Improve release note. - [x] Address the same bug for project files. Release Notes: - Fixed .gitignore files beyond the first .git directory being respected by the worktree (zed-industries/community#575). --- crates/project/src/worktree.rs | 39 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a38e43cd87..f6fae0c98b 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2027,11 +2027,16 @@ impl LocalSnapshot { fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { let mut new_ignores = Vec::new(); - for ancestor in abs_path.ancestors().skip(1) { - if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { - new_ignores.push((ancestor, Some(ignore.clone()))); - } else { - new_ignores.push((ancestor, None)); + for (index, ancestor) in abs_path.ancestors().enumerate() { + if index > 0 { + if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { + new_ignores.push((ancestor, Some(ignore.clone()))); + } else { + new_ignores.push((ancestor, None)); + } + } + if ancestor.join(&*DOT_GIT).is_dir() { + break; } } @@ -2048,7 +2053,6 @@ impl LocalSnapshot { if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { ignore_stack = IgnoreStack::all(); } - ignore_stack } @@ -3064,14 +3068,21 @@ impl BackgroundScanner { // Populate ignores above the root. let root_abs_path = self.state.lock().snapshot.abs_path.clone(); - for ancestor in root_abs_path.ancestors().skip(1) { - if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await - { - self.state - .lock() - .snapshot - .ignores_by_parent_abs_path - .insert(ancestor.into(), (ignore.into(), false)); + for (index, ancestor) in root_abs_path.ancestors().enumerate() { + if index != 0 { + if let Ok(ignore) = + build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await + { + self.state + .lock() + .snapshot + .ignores_by_parent_abs_path + .insert(ancestor.into(), (ignore.into(), false)); + } + } + if ancestor.join(&*DOT_GIT).is_dir() { + // Reached root of git repository. + break; } } From 8db389313bf993d60ecf774122eea276ef0546d2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 17 Oct 2023 13:34:51 -0400 Subject: [PATCH 46/60] Add link & public icons --- assets/icons/link.svg | 3 +++ assets/icons/public.svg | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 assets/icons/link.svg create mode 100644 assets/icons/public.svg diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 0000000000..4925bd8e00 --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/public.svg b/assets/icons/public.svg new file mode 100644 index 0000000000..55a7968485 --- /dev/null +++ b/assets/icons/public.svg @@ -0,0 +1,3 @@ + + + From 33296802fb0424863ad3a956b7b70b76afaa23f3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 12:11:39 +0300 Subject: [PATCH 47/60] Add a rough prototype --- crates/language_tools/src/lsp_log.rs | 60 ++++++++++++++++------------ 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 383ca94851..a796bc46c8 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -36,7 +36,7 @@ struct ProjectState { } struct LanguageServerState { - log_buffer: ModelHandle, + log_storage: Vec, rpc_state: Option, _io_logs_subscription: Option, _lsp_logs_subscription: Option, @@ -168,15 +168,14 @@ impl LogStore { project: &ModelHandle, id: LanguageServerId, cx: &mut ModelContext, - ) -> Option> { + ) -> Option<&mut Vec> { let project_state = self.projects.get_mut(&project.downgrade())?; let server_state = project_state.servers.entry(id).or_insert_with(|| { cx.notify(); LanguageServerState { rpc_state: None, - log_buffer: cx - .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) - .clone(), + // TODO kb move this to settings? + log_storage: Vec::with_capacity(10_000), _io_logs_subscription: None, _lsp_logs_subscription: None, } @@ -186,7 +185,7 @@ impl LogStore { if let Some(server) = server.as_deref() { if server.has_notification_handler::() { // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. - return Some(server_state.log_buffer.clone()); + return Some(&mut server_state.log_storage); } } @@ -215,7 +214,7 @@ impl LogStore { } }) }); - Some(server_state.log_buffer.clone()) + Some(&mut server_state.log_storage) } fn add_language_server_log( @@ -225,25 +224,23 @@ impl LogStore { message: &str, cx: &mut ModelContext, ) -> Option<()> { - let buffer = match self + let log_lines = match self .projects .get_mut(&project.downgrade())? .servers - .get(&id) - .map(|state| state.log_buffer.clone()) + .get_mut(&id) + .map(|state| &mut state.log_storage) { Some(existing_buffer) => existing_buffer, None => self.add_language_server(&project, id, cx)?, }; - buffer.update(cx, |buffer, cx| { - let len = buffer.len(); - let has_newline = message.ends_with("\n"); - buffer.edit([(len..len, message)], None, cx); - if !has_newline { - let len = buffer.len(); - buffer.edit([(len..len, "\n")], None, cx); - } - }); + + // TODO kb something better VecDequeue? + if log_lines.capacity() == log_lines.len() { + log_lines.drain(..log_lines.len() / 2); + } + log_lines.push(message.trim().to_string()); + cx.notify(); Some(()) } @@ -260,15 +257,15 @@ impl LogStore { Some(()) } - pub fn log_buffer_for_server( + fn server_logs( &self, project: &ModelHandle, server_id: LanguageServerId, - ) -> Option> { + ) -> Option<&[String]> { let weak_project = project.downgrade(); let project_state = self.projects.get(&weak_project)?; let server_state = project_state.servers.get(&server_id)?; - Some(server_state.log_buffer.clone()) + Some(&server_state.log_storage) } fn enable_rpc_trace_for_language_server( @@ -487,14 +484,24 @@ impl LspLogView { } fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { - let buffer = self + let log_contents = self .log_store .read(cx) - .log_buffer_for_server(&self.project, server_id); - if let Some(buffer) = buffer { + .server_logs(&self.project, server_id) + .map(|lines| lines.join("\n")); + if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = false; - self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, log_contents)); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx); + editor.set_read_only(true); + editor.move_to_end(&Default::default(), cx); + editor + }); + cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())) + .detach(); + self.editor = editor; cx.notify(); } } @@ -505,6 +512,7 @@ impl LspLogView { cx: &mut ViewContext, ) { let buffer = self.log_store.update(cx, |log_set, cx| { + // TODO kb save this buffer from overflows too log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx) }); if let Some(buffer) = buffer { From 5a4161d29385ca6fd454ca788cab3204c5f1e820 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 15:41:27 +0300 Subject: [PATCH 48/60] Do not detach subscriptions --- crates/language_tools/src/lsp_log.rs | 63 +++++++++++++++++----------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index a796bc46c8..dcdaf1df6b 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1,4 +1,4 @@ -use collections::HashMap; +use collections::{HashMap, VecDeque}; use editor::Editor; use futures::{channel::mpsc, StreamExt}; use gpui::{ @@ -36,7 +36,7 @@ struct ProjectState { } struct LanguageServerState { - log_storage: Vec, + log_storage: VecDeque, rpc_state: Option, _io_logs_subscription: Option, _lsp_logs_subscription: Option, @@ -49,6 +49,7 @@ struct LanguageServerRpcState { pub struct LspLogView { pub(crate) editor: ViewHandle, + _editor_subscription: Subscription, log_store: ModelHandle, current_server_id: Option, is_showing_rpc_trace: bool, @@ -168,14 +169,14 @@ impl LogStore { project: &ModelHandle, id: LanguageServerId, cx: &mut ModelContext, - ) -> Option<&mut Vec> { + ) -> Option<&mut LanguageServerState> { let project_state = self.projects.get_mut(&project.downgrade())?; let server_state = project_state.servers.entry(id).or_insert_with(|| { cx.notify(); LanguageServerState { rpc_state: None, // TODO kb move this to settings? - log_storage: Vec::with_capacity(10_000), + log_storage: VecDeque::with_capacity(10_000), _io_logs_subscription: None, _lsp_logs_subscription: None, } @@ -185,7 +186,7 @@ impl LogStore { if let Some(server) = server.as_deref() { if server.has_notification_handler::() { // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. - return Some(&mut server_state.log_storage); + return Some(server_state); } } @@ -214,7 +215,7 @@ impl LogStore { } }) }); - Some(&mut server_state.log_storage) + Some(server_state) } fn add_language_server_log( @@ -224,22 +225,24 @@ impl LogStore { message: &str, cx: &mut ModelContext, ) -> Option<()> { - let log_lines = match self + let language_server_state = match self .projects .get_mut(&project.downgrade())? .servers .get_mut(&id) - .map(|state| &mut state.log_storage) { - Some(existing_buffer) => existing_buffer, + Some(existing_state) => existing_state, None => self.add_language_server(&project, id, cx)?, }; - // TODO kb something better VecDequeue? + let log_lines = &mut language_server_state.log_storage; if log_lines.capacity() == log_lines.len() { - log_lines.drain(..log_lines.len() / 2); + log_lines.pop_front(); } - log_lines.push(message.trim().to_string()); + log_lines.push_back(message.trim().to_string()); + + //// TODO kb refresh editor too + //need LspLogView. cx.notify(); Some(()) @@ -261,7 +264,7 @@ impl LogStore { &self, project: &ModelHandle, server_id: LanguageServerId, - ) -> Option<&[String]> { + ) -> Option<&VecDeque> { let weak_project = project.downgrade(); let project_state = self.projects.get(&weak_project)?; let server_state = project_state.servers.get(&server_id)?; @@ -408,8 +411,10 @@ impl LspLogView { cx.notify(); }); + let (editor, _editor_subscription) = Self::editor_for_buffer(project.clone(), buffer, cx); let mut this = Self { - editor: Self::editor_for_buffer(project.clone(), buffer, cx), + editor, + _editor_subscription, project, log_store, current_server_id: None, @@ -426,16 +431,15 @@ impl LspLogView { project: ModelHandle, buffer: ModelHandle, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> (ViewHandle, Subscription) { let editor = cx.add_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); editor.set_read_only(true); editor.move_to_end(&Default::default(), cx); editor }); - cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())) - .detach(); - editor + let subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); + (editor, subscription) } pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { @@ -488,19 +492,27 @@ impl LspLogView { .log_store .read(cx) .server_logs(&self.project, server_id) - .map(|lines| lines.join("\n")); + .map(|lines| { + let (a, b) = lines.as_slices(); + let log_contents = a.join("\n"); + if b.is_empty() { + log_contents + } else { + log_contents + "\n" + &b.join("\n") + } + }); if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = false; - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, log_contents)); let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx); + let mut editor = Editor::multi_line(None, cx); editor.set_read_only(true); editor.move_to_end(&Default::default(), cx); + editor.set_text(log_contents, cx); editor }); - cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())) - .detach(); + self._editor_subscription = + cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); self.editor = editor; cx.notify(); } @@ -518,7 +530,10 @@ impl LspLogView { if let Some(buffer) = buffer { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = true; - self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx); + let (editor, _editor_subscription) = + Self::editor_for_buffer(self.project.clone(), buffer, cx); + self.editor = editor; + self._editor_subscription = _editor_subscription; cx.notify(); } } From ba5c188630d86373b119213ffdab21449eccccc6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 16:53:44 +0300 Subject: [PATCH 49/60] Update editor with current buffer logs --- crates/language_tools/src/lsp_log.rs | 41 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index dcdaf1df6b..bf75d35bb7 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -24,6 +24,7 @@ use workspace::{ const SEND_LINE: &str = "// Send:\n"; const RECEIVE_LINE: &str = "// Receive:\n"; +const MAX_STORED_LOG_ENTRIES: usize = 5000; pub struct LogStore { projects: HashMap, ProjectState>, @@ -54,7 +55,7 @@ pub struct LspLogView { current_server_id: Option, is_showing_rpc_trace: bool, project: ModelHandle, - _log_store_subscription: Subscription, + _log_store_subscriptions: Vec, } pub struct LspLogToolbarItemView { @@ -175,8 +176,7 @@ impl LogStore { cx.notify(); LanguageServerState { rpc_state: None, - // TODO kb move this to settings? - log_storage: VecDeque::with_capacity(10_000), + log_storage: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), _io_logs_subscription: None, _lsp_logs_subscription: None, } @@ -236,14 +236,16 @@ impl LogStore { }; let log_lines = &mut language_server_state.log_storage; - if log_lines.capacity() == log_lines.len() { + if log_lines.len() == MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } - log_lines.push_back(message.trim().to_string()); - - //// TODO kb refresh editor too - //need LspLogView. + let message = message.trim(); + log_lines.push_back(message.to_string()); + cx.emit(Event::NewServerLogEntry { + id, + entry: message.to_string(), + }); cx.notify(); Some(()) } @@ -375,7 +377,7 @@ impl LspLogView { .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); - let _log_store_subscription = cx.observe(&log_store, |this, store, cx| { + let model_changes_subscription = cx.observe(&log_store, |this, store, cx| { (|| -> Option<()> { let project_state = store.read(cx).projects.get(&this.project.downgrade())?; if let Some(current_lsp) = this.current_server_id { @@ -411,6 +413,18 @@ impl LspLogView { cx.notify(); }); + let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { + Event::NewServerLogEntry { id, entry } => { + if log_view.current_server_id == Some(*id) { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.handle_input(entry, cx); + editor.handle_input("\n", cx); + editor.set_read_only(true); + }) + } + } + }); let (editor, _editor_subscription) = Self::editor_for_buffer(project.clone(), buffer, cx); let mut this = Self { editor, @@ -419,7 +433,7 @@ impl LspLogView { log_store, current_server_id: None, is_showing_rpc_trace: false, - _log_store_subscription, + _log_store_subscriptions: vec![model_changes_subscription, events_subscriptions], }; if let Some(server_id) = server_id { this.show_logs_for_server(server_id, cx); @@ -524,7 +538,6 @@ impl LspLogView { cx: &mut ViewContext, ) { let buffer = self.log_store.update(cx, |log_set, cx| { - // TODO kb save this buffer from overflows too log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx) }); if let Some(buffer) = buffer { @@ -972,8 +985,12 @@ impl LspLogToolbarItemView { } } +pub enum Event { + NewServerLogEntry { id: LanguageServerId, entry: String }, +} + impl Entity for LogStore { - type Event = (); + type Event = Event; } impl Entity for LspLogView { From c872c86c4a5df3200cc1b2f621aedc5a4c8651e3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 20:53:39 +0300 Subject: [PATCH 50/60] Remove another needless log buffer --- crates/language_tools/src/lsp_log.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index bf75d35bb7..58f7d68235 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -376,7 +376,6 @@ impl LspLogView { .projects .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); let model_changes_subscription = cx.observe(&log_store, |this, store, cx| { (|| -> Option<()> { let project_state = store.read(cx).projects.get(&this.project.downgrade())?; @@ -425,7 +424,14 @@ impl LspLogView { } } }); - let (editor, _editor_subscription) = Self::editor_for_buffer(project.clone(), buffer, cx); + // TODO kb deduplicate + let editor = cx.add_view(|cx| { + let mut editor = Editor::multi_line(None, cx); + editor.set_read_only(true); + editor.move_to_end(&Default::default(), cx); + editor + }); + let _editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); let mut this = Self { editor, _editor_subscription, From 08af830fd7b6b70ea29ba55b3088acec967829cd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 21:40:25 +0300 Subject: [PATCH 51/60] Do not create buffers for rpc logs --- crates/language_tools/src/lsp_log.rs | 218 +++++++++++++++------------ 1 file changed, 118 insertions(+), 100 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 58f7d68235..ae63f84b64 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1,5 +1,5 @@ use collections::{HashMap, VecDeque}; -use editor::Editor; +use editor::{Editor, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, @@ -11,7 +11,7 @@ use gpui::{ AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakModelHandle, }; -use language::{Buffer, LanguageServerId, LanguageServerName}; +use language::{LanguageServerId, LanguageServerName}; use lsp::IoKind; use project::{search::SearchQuery, Project}; use std::{borrow::Cow, sync::Arc}; @@ -22,8 +22,8 @@ use workspace::{ ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated, }; -const SEND_LINE: &str = "// Send:\n"; -const RECEIVE_LINE: &str = "// Receive:\n"; +const SEND_LINE: &str = "// Send:"; +const RECEIVE_LINE: &str = "// Receive:"; const MAX_STORED_LOG_ENTRIES: usize = 5000; pub struct LogStore { @@ -37,20 +37,20 @@ struct ProjectState { } struct LanguageServerState { - log_storage: VecDeque, + log_messages: VecDeque, rpc_state: Option, _io_logs_subscription: Option, _lsp_logs_subscription: Option, } struct LanguageServerRpcState { - buffer: ModelHandle, + rpc_messages: VecDeque, last_message_kind: Option, } pub struct LspLogView { pub(crate) editor: ViewHandle, - _editor_subscription: Subscription, + editor_subscription: Subscription, log_store: ModelHandle, current_server_id: Option, is_showing_rpc_trace: bool, @@ -124,10 +124,9 @@ impl LogStore { io_tx, }; cx.spawn_weak(|this, mut cx| async move { - while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await { + while let Some((project, server_id, io_kind, message)) = io_rx.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - message.push('\n'); this.on_io(project, server_id, io_kind, &message, cx); }); } @@ -176,7 +175,7 @@ impl LogStore { cx.notify(); LanguageServerState { rpc_state: None, - log_storage: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), _io_logs_subscription: None, _lsp_logs_subscription: None, } @@ -235,16 +234,16 @@ impl LogStore { None => self.add_language_server(&project, id, cx)?, }; - let log_lines = &mut language_server_state.log_storage; + let log_lines = &mut language_server_state.log_messages; if log_lines.len() == MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } - let message = message.trim(); log_lines.push_back(message.to_string()); cx.emit(Event::NewServerLogEntry { id, entry: message.to_string(), + is_rpc: false, }); cx.notify(); Some(()) @@ -270,38 +269,24 @@ impl LogStore { let weak_project = project.downgrade(); let project_state = self.projects.get(&weak_project)?; let server_state = project_state.servers.get(&server_id)?; - Some(&server_state.log_storage) + Some(&server_state.log_messages) } fn enable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, - cx: &mut ModelContext, - ) -> Option> { + ) -> Option<&mut LanguageServerRpcState> { let weak_project = project.downgrade(); let project_state = self.projects.get_mut(&weak_project)?; let server_state = project_state.servers.get_mut(&server_id)?; - let rpc_state = server_state.rpc_state.get_or_insert_with(|| { - let language = project.read(cx).languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); - cx.spawn_weak({ - let buffer = buffer.clone(); - |_, mut cx| async move { - let language = language.await.ok(); - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language(language, cx); - }); - } - }) - .detach(); - - LanguageServerRpcState { - buffer, + let rpc_state = server_state + .rpc_state + .get_or_insert_with(|| LanguageServerRpcState { + rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), last_message_kind: None, - } - }); - Some(rpc_state.buffer.clone()) + }); + Some(rpc_state) } pub fn disable_rpc_trace_for_language_server( @@ -330,7 +315,7 @@ impl LogStore { IoKind::StdIn => false, IoKind::StdErr => { let project = project.upgrade(cx)?; - let message = format!("stderr: {}\n", message.trim()); + let message = format!("stderr: {}", message.trim()); self.add_language_server_log(&project, language_server_id, &message, cx); return Some(()); } @@ -343,24 +328,40 @@ impl LogStore { .get_mut(&language_server_id)? .rpc_state .as_mut()?; - state.buffer.update(cx, |buffer, cx| { - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send + let kind = if is_received { + MessageKind::Receive + } else { + MessageKind::Send + }; + + let rpc_log_lines = &mut state.rpc_messages; + if rpc_log_lines.len() == MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + if state.last_message_kind != Some(kind) { + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, }; - if state.last_message_kind != Some(kind) { - let len = buffer.len(); - let line = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - buffer.edit([(len..len, line)], None, cx); - state.last_message_kind = Some(kind); - } - let len = buffer.len(); - buffer.edit([(len..len, message)], None, cx); + rpc_log_lines.push_back(line_before_message.to_string()); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + entry: line_before_message.to_string(), + is_rpc: true, + }); + } + + if rpc_log_lines.len() == MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + let message = message.trim(); + rpc_log_lines.push_back(message.to_string()); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + entry: message.to_string(), + is_rpc: true, }); + cx.notify(); Some(()) } } @@ -413,28 +414,25 @@ impl LspLogView { cx.notify(); }); let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { - Event::NewServerLogEntry { id, entry } => { + Event::NewServerLogEntry { id, entry, is_rpc } => { if log_view.current_server_id == Some(*id) { - log_view.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.handle_input(entry, cx); - editor.handle_input("\n", cx); - editor.set_read_only(true); - }) + if (*is_rpc && log_view.is_showing_rpc_trace) + || (!*is_rpc && !log_view.is_showing_rpc_trace) + { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.handle_input(entry.trim(), cx); + editor.handle_input("\n", cx); + editor.set_read_only(true); + }); + } } } }); - // TODO kb deduplicate - let editor = cx.add_view(|cx| { - let mut editor = Editor::multi_line(None, cx); - editor.set_read_only(true); - editor.move_to_end(&Default::default(), cx); - editor - }); - let _editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); + let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx); let mut this = Self { editor, - _editor_subscription, + editor_subscription, project, log_store, current_server_id: None, @@ -447,19 +445,19 @@ impl LspLogView { this } - fn editor_for_buffer( - project: ModelHandle, - buffer: ModelHandle, + fn editor_for_logs( + log_contents: String, cx: &mut ViewContext, ) -> (ViewHandle, Subscription) { let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(buffer, Some(project), cx); + let mut editor = Editor::multi_line(None, cx); + editor.set_text(log_contents, cx); + editor.move_to_end(&MoveToEnd, cx); editor.set_read_only(true); - editor.move_to_end(&Default::default(), cx); editor }); - let subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); - (editor, subscription) + let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); + (editor, editor_subscription) } pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { @@ -512,28 +510,13 @@ impl LspLogView { .log_store .read(cx) .server_logs(&self.project, server_id) - .map(|lines| { - let (a, b) = lines.as_slices(); - let log_contents = a.join("\n"); - if b.is_empty() { - log_contents - } else { - log_contents + "\n" + &b.join("\n") - } - }); + .map(log_contents); if let Some(log_contents) = log_contents { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = false; - let editor = cx.add_view(|cx| { - let mut editor = Editor::multi_line(None, cx); - editor.set_read_only(true); - editor.move_to_end(&Default::default(), cx); - editor.set_text(log_contents, cx); - editor - }); - self._editor_subscription = - cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); + let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx); self.editor = editor; + self.editor_subscription = editor_subscription; cx.notify(); } } @@ -543,16 +526,37 @@ impl LspLogView { server_id: LanguageServerId, cx: &mut ViewContext, ) { - let buffer = self.log_store.update(cx, |log_set, cx| { - log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx) + let rpc_log = self.log_store.update(cx, |log_store, _| { + log_store + .enable_rpc_trace_for_language_server(&self.project, server_id) + .map(|state| log_contents(&state.rpc_messages)) }); - if let Some(buffer) = buffer { + if let Some(rpc_log) = rpc_log { self.current_server_id = Some(server_id); self.is_showing_rpc_trace = true; - let (editor, _editor_subscription) = - Self::editor_for_buffer(self.project.clone(), buffer, cx); + let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx); + let language = self.project.read(cx).languages().language_for_name("JSON"); + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton") + .update(cx, |_, cx| { + cx.spawn_weak({ + let buffer = cx.handle(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }); + } + }) + .detach(); + }); + self.editor = editor; - self._editor_subscription = _editor_subscription; + self.editor_subscription = editor_subscription; cx.notify(); } } @@ -565,7 +569,7 @@ impl LspLogView { ) { self.log_store.update(cx, |log_store, cx| { if enabled { - log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx); + log_store.enable_rpc_trace_for_language_server(&self.project, server_id); } else { log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx); } @@ -577,6 +581,16 @@ impl LspLogView { } } +fn log_contents(lines: &VecDeque) -> String { + let (a, b) = lines.as_slices(); + let log_contents = a.join("\n"); + if b.is_empty() { + log_contents + } else { + log_contents + "\n" + &b.join("\n") + } +} + impl View for LspLogView { fn ui_name() -> &'static str { "LspLogView" @@ -992,7 +1006,11 @@ impl LspLogToolbarItemView { } pub enum Event { - NewServerLogEntry { id: LanguageServerId, entry: String }, + NewServerLogEntry { + id: LanguageServerId, + entry: String, + is_rpc: bool, + }, } impl Entity for LogStore { From a95cce9a60716e77ecbfaa85eaa9ffaa039cfc60 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 21:47:21 +0300 Subject: [PATCH 52/60] Reduce max log lines, clean log buffers better --- crates/language_tools/src/lsp_log.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index ae63f84b64..c75fea256d 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -24,7 +24,7 @@ use workspace::{ const SEND_LINE: &str = "// Send:"; const RECEIVE_LINE: &str = "// Receive:"; -const MAX_STORED_LOG_ENTRIES: usize = 5000; +const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { projects: HashMap, ProjectState>, @@ -235,7 +235,7 @@ impl LogStore { }; let log_lines = &mut language_server_state.log_messages; - if log_lines.len() == MAX_STORED_LOG_ENTRIES { + while log_lines.len() >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } let message = message.trim(); @@ -335,9 +335,6 @@ impl LogStore { }; let rpc_log_lines = &mut state.rpc_messages; - if rpc_log_lines.len() == MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } if state.last_message_kind != Some(kind) { let line_before_message = match kind { MessageKind::Send => SEND_LINE, @@ -351,7 +348,7 @@ impl LogStore { }); } - if rpc_log_lines.len() == MAX_STORED_LOG_ENTRIES { + while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES { rpc_log_lines.pop_front(); } let message = message.trim(); From 1c5e07f4a21f0ca4f0b80881798605c51a94a6ec Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 13:19:22 -0600 Subject: [PATCH 53/60] update sidebar for public channels --- assets/icons/public.svg | 2 +- .../20221109000000_test_schema.sql | 2 +- ...rojects_room_id_fkey_on_delete_cascade.sql | 8 + crates/collab_ui/src/collab_panel.rs | 246 ++++++++++++++---- 4 files changed, 208 insertions(+), 50 deletions(-) create mode 100644 crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql diff --git a/assets/icons/public.svg b/assets/icons/public.svg index 55a7968485..38278cdaba 100644 --- a/assets/icons/public.svg +++ b/assets/icons/public.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index dcb793aa51..8eb6b52fd8 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); CREATE TABLE "projects" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "room_id" INTEGER REFERENCES rooms (id) NOT NULL, + "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL, "host_user_id" INTEGER REFERENCES users (id) NOT NULL, "host_connection_id" INTEGER, "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql new file mode 100644 index 0000000000..be535ff7fa --- /dev/null +++ b/crates/collab/migrations/20231017185833_projects_room_id_fkey_on_delete_cascade.sql @@ -0,0 +1,8 @@ +-- Add migration script here + +ALTER TABLE projects + DROP CONSTRAINT projects_room_id_fkey, + ADD CONSTRAINT projects_room_id_fkey + FOREIGN KEY (room_id) + REFERENCES rooms (id) + ON DELETE CASCADE; diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 30505b0876..2e68a1c939 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -11,7 +11,10 @@ use anyhow::Result; use call::ActiveCall; use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore}; use channel_modal::ChannelModal; -use client::{proto::PeerId, Client, Contact, User, UserStore}; +use client::{ + proto::{self, PeerId}, + Client, Contact, User, UserStore, +}; use contact_finder::ContactFinder; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; @@ -428,7 +431,7 @@ enum ListEntry { is_last: bool, }, ParticipantScreen { - peer_id: PeerId, + peer_id: Option, is_last: bool, }, IncomingRequest(Arc), @@ -442,6 +445,9 @@ enum ListEntry { ChannelNotes { channel_id: ChannelId, }, + ChannelChat { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -602,6 +608,13 @@ impl CollabPanel { ix, cx, ), + ListEntry::ChannelChat { channel_id } => this.render_channel_chat( + *channel_id, + &theme.collab_panel, + is_selected, + ix, + cx, + ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), @@ -804,7 +817,8 @@ impl CollabPanel { let room = room.read(cx); if let Some(channel_id) = room.channel_id() { - self.entries.push(ListEntry::ChannelNotes { channel_id }) + self.entries.push(ListEntry::ChannelNotes { channel_id }); + self.entries.push(ListEntry::ChannelChat { channel_id }) } // Populate the active user. @@ -836,7 +850,13 @@ impl CollabPanel { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, - is_last: projects.peek().is_none(), + is_last: projects.peek().is_none() && !room.is_screen_sharing(), + }); + } + if room.is_screen_sharing() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: None, + is_last: true, }); } } @@ -880,7 +900,7 @@ impl CollabPanel { } if !participant.video_tracks.is_empty() { self.entries.push(ListEntry::ParticipantScreen { - peer_id: participant.peer_id, + peer_id: Some(participant.peer_id), is_last: true, }); } @@ -1225,14 +1245,18 @@ impl CollabPanel { ) -> AnyElement { enum CallParticipant {} enum CallParticipantTooltip {} + enum LeaveCallButton {} + enum LeaveCallTooltip {} 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 content = MouseEventHandler::new::( + user.id as usize, + cx, + |mouse_state, cx| { let style = if is_current_user { *collab_theme .contact_row @@ -1268,14 +1292,32 @@ impl CollabPanel { Label::new("Calling", collab_theme.calling_indicator.text.clone()) .contained() .with_style(collab_theme.calling_indicator.container) - .aligned(), + .aligned() + .into_any(), ) } else if is_current_user { Some( - Label::new("You", collab_theme.calling_indicator.text.clone()) - .contained() - .with_style(collab_theme.calling_indicator.container) - .aligned(), + MouseEventHandler::new::(0, cx, |state, _| { + render_icon_button( + theme + .collab_panel + .leave_call_button + .style_for(is_selected, state), + "icons/exit.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, _, cx| { + Self::leave_call(cx); + }) + .with_tooltip::( + 0, + "Leave call", + None, + theme.tooltip.clone(), + cx, + ) + .into_any(), ) } else { None @@ -1284,7 +1326,8 @@ impl CollabPanel { .with_height(collab_theme.row_height) .contained() .with_style(style) - }); + }, + ); if is_current_user || is_pending || peer_id.is_none() { return content.into_any(); @@ -1406,7 +1449,7 @@ impl CollabPanel { } fn render_participant_screen( - peer_id: PeerId, + peer_id: Option, is_last: bool, is_selected: bool, theme: &theme::CollabPanel, @@ -1421,8 +1464,8 @@ impl CollabPanel { .unwrap_or(0.); let tree_branch = theme.tree_branch; - MouseEventHandler::new::( - peer_id.as_u64() as usize, + let handler = MouseEventHandler::new::( + peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); @@ -1460,16 +1503,20 @@ impl CollabPanel { .contained() .with_style(row.container) }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(peer_id, cx) - }); - } - }) - .into_any() + ); + if peer_id.is_none() { + return handler.into_any(); + } + handler + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(peer_id.unwrap(), cx) + }); + } + }) + .into_any() } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { @@ -1496,23 +1543,32 @@ impl CollabPanel { enum AddChannel {} let tooltip_style = &theme.tooltip; + let mut channel_link = None; + let mut channel_tooltip_text = None; + let mut channel_icon = None; + let text = match section { Section::ActiveCall => { let channel_name = iife!({ let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - let name = self - .channel_store - .read(cx) - .channel_for_id(channel_id)? - .name - .as_str(); + let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - Some(name) + channel_link = Some(channel.link()); + (channel_icon, channel_tooltip_text) = match channel.visibility { + proto::ChannelVisibility::Public => { + (Some("icons/public.svg"), Some("Copy public channel link.")) + } + proto::ChannelVisibility::Members => { + (Some("icons/hash.svg"), Some("Copy private channel link.")) + } + }; + + Some(channel.name.as_str()) }); if let Some(name) = channel_name { - Cow::Owned(format!("#{}", name)) + Cow::Owned(format!("{}", name)) } else { Cow::Borrowed("Current Call") } @@ -1527,28 +1583,30 @@ impl CollabPanel { enum AddContact {} let button = match section { - Section::ActiveCall => Some( + Section::ActiveCall => channel_link.map(|channel_link| { + let channel_link_copy = channel_link.clone(); MouseEventHandler::new::(0, cx, |state, _| { render_icon_button( theme .collab_panel .leave_call_button .style_for(is_selected, state), - "icons/exit.svg", + "icons/link.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - Self::leave_call(cx); + .on_click(MouseButton::Left, move |_, _, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) }) .with_tooltip::( 0, - "Leave call", + channel_tooltip_text.unwrap(), None, tooltip_style.clone(), cx, - ), - ), + ) + }), Section::Contacts => Some( MouseEventHandler::new::(0, cx, |state, _| { render_icon_button( @@ -1633,6 +1691,21 @@ impl CollabPanel { theme.collab_panel.contact_username.container.margin.left, ), ) + } else if let Some(channel_icon) = channel_icon { + Some( + Svg::new(channel_icon) + .with_color(header_style.text.color) + .constrained() + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size) + .contained() + .with_margin_right( + theme.collab_panel.contact_username.container.margin.left, + ), + ) } else { None }) @@ -1908,6 +1981,12 @@ impl CollabPanel { let channel_id = channel.id; let collab_theme = &theme.collab_panel; let has_children = self.channel_store.read(cx).has_children(channel_id); + let is_public = self + .channel_store + .read(cx) + .channel_for_id(channel_id) + .map(|channel| channel.visibility) + == Some(proto::ChannelVisibility::Public); let other_selected = self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); @@ -1965,12 +2044,16 @@ impl CollabPanel { Flex::::row() .with_child( - Svg::new("icons/hash.svg") - .with_color(collab_theme.channel_hash.color) - .constrained() - .with_width(collab_theme.channel_hash.width) - .aligned() - .left(), + Svg::new(if is_public { + "icons/public.svg" + } else { + "icons/hash.svg" + }) + .with_color(collab_theme.channel_hash.color) + .constrained() + .with_width(collab_theme.channel_hash.width) + .aligned() + .left(), ) .with_child({ let style = collab_theme.channel_name.inactive_state(); @@ -2275,7 +2358,7 @@ impl CollabPanel { .with_child(render_tree_branch( tree_branch, &row.name.text, - true, + false, vec2f(host_avatar_width, theme.row_height), cx.font_cache(), )) @@ -2308,6 +2391,62 @@ impl CollabPanel { .into_any() } + fn render_channel_chat( + &self, + channel_id: ChannelId, + theme: &theme::CollabPanel, + is_selected: bool, + ix: usize, + cx: &mut ViewContext, + ) -> AnyElement { + enum ChannelChat {} + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + 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); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/conversations.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new("chat", theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_padding_left(theme.channel_row.default_style().padding.left) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + fn render_channel_invite( channel: Arc, channel_store: ModelHandle, @@ -2771,6 +2910,9 @@ impl CollabPanel { } } ListEntry::ParticipantScreen { peer_id, .. } => { + let Some(peer_id) = peer_id else { + return; + }; if let Some(workspace) = self.workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { workspace.open_shared_screen(*peer_id, cx) @@ -3498,6 +3640,14 @@ impl PartialEq for ListEntry { return channel_id == other_id; } } + ListEntry::ChannelChat { channel_id } => { + if let ListEntry::ChannelChat { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; From 04a28fe831d2d044eff3405f4b034d767e07be0a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 13:32:08 -0600 Subject: [PATCH 54/60] Fix lint errors --- styles/src/style_tree/collab_modals.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index 586e7be3f0..6132ce5ff4 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -1,4 +1,4 @@ -import { StyleSet, StyleSets, Styles, useTheme } from "../theme" +import { StyleSets, useTheme } from "../theme" import { background, border, foreground, text } from "./components" import picker from "./picker" import { input } from "../component/input" @@ -44,7 +44,7 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", styleset, "active"), } } - }); + }) const member_icon_style = icon_button({ variant: "ghost", From 13c7bbbac622cf44fcaa2ee7340de016385c00d3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 17 Oct 2023 15:47:17 -0400 Subject: [PATCH 55/60] Shorten GitHub release message --- .github/workflows/release_actions.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index c1df24a8e5..550eda882b 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -20,9 +20,7 @@ jobs: id: get-content with: stringToTruncate: | - 📣 Zed ${{ github.event.release.tag_name }} was just released! - - Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it. + 📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released! ${{ github.event.release.body }} maxLength: 2000 From ed8a2c8793cb3f14bed3f32fb791f620b0667739 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 18 Oct 2023 10:35:11 -0400 Subject: [PATCH 56/60] revert change to return only the text and inside return all text inside markdown blocks --- crates/assistant/src/prompts.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 7aafe75920..18e9e18f7d 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -243,7 +243,7 @@ pub fn generate_content_prompt( } prompts.push("Never make remarks about the output.".to_string()); prompts.push("Do not return any text, except the generated code.".to_string()); - prompts.push("Do not wrap your text in a Markdown block".to_string()); + prompts.push("Always wrap your code in a Markdown block".to_string()); let current_messages = [ChatCompletionRequestMessage { role: "user".to_string(), @@ -256,7 +256,11 @@ pub fn generate_content_prompt( tiktoken_rs::num_tokens_from_messages(model, ¤t_messages) { let max_token_count = tiktoken_rs::model::get_context_size(model); - let intermediate_token_count = max_token_count - current_token_count; + let intermediate_token_count = if max_token_count > current_token_count { + max_token_count - current_token_count + } else { + 0 + }; if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION { 0 From 99121ad5cd5cb2debded3a9029383688d270d04c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:05:13 +0200 Subject: [PATCH 57/60] buffer_search: Discard empty search suggestions. (#3136) Now when buffer_search::Deploy action is triggered (with cmd-f), we'll keep the previous query in query_editor (if there was one) instead of replacing it with empty query. This addresses this bit of feedback from Jose: > If no text is selected, `cmd + f` should not delete the text in the search bar when refocusing Release Notes: - Improved buffer search by not clearing out query editor when no text is selected and "buffer search: deploy" (default keybind: cmd-f) is triggered. --- crates/search/src/buffer_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 07c1eef3ff..ef8c56f2a7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -537,6 +537,7 @@ impl BufferSearchBar { self.active_searchable_item .as_ref() .map(|searchable_item| searchable_item.query_suggestion(cx)) + .filter(|suggestion| !suggestion.is_empty()) } pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { From cf429ba284e041dfdd73a1bcb6de95afc312e626 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 18 Oct 2023 12:31:12 -0400 Subject: [PATCH 58/60] v0.110.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 ecbe076711..833e234956 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10041,7 +10041,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.109.0" +version = "0.110.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index aeabd4b453..4e2b97f4a1 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.109.0" +version = "0.110.0" publish = false [lib] From 4e68b588be4bf2107d03749bbf60b0af9ca80da2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 18 Oct 2023 13:17:17 -0400 Subject: [PATCH 59/60] collab 0.25.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 833e234956..6b92170b88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1467,7 +1467,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index b91f0e1a5f..bc6e09f3bd 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.24.0" +version = "0.25.0" publish = false [[bin]] From 655c9ece2decdb550192959175c9cd3cd88f26d1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 18 Oct 2023 11:16:14 -0700 Subject: [PATCH 60/60] Fix possibility of infinite loop in selections_with_autoclose_regions Previously, that method could loop forever if the editor's autoclose regions had unexpected selection ids. Co-authored-by: Piotr --- crates/editor/src/editor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7aca4ab98f..d755723085 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3286,8 +3286,10 @@ impl Editor { i = 0; } else if pair_state.range.start.to_offset(buffer) > range.end { break; - } else if pair_state.selection_id == selection.id { - enclosing = Some(pair_state); + } else { + if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + } i += 1; } }