From f20f096a30f02dc565475d0c873f3a0a04ce9664 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 2 Oct 2023 19:15:59 +0300 Subject: [PATCH 001/274] 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 002/274] 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 003/274] 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 004/274] 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 005/274] 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 006/274] 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 007/274] 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 008/274] 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 009/274] 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 010/274] 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 011/274] 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 012/274] 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 013/274] 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 014/274] 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 e802c072f7e3e32503355e44cbaa32b5e382e14d Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 5 Sep 2023 22:16:12 -0400 Subject: [PATCH 015/274] Start hacking in autocomplete docs --- crates/editor/src/editor.rs | 91 ++++--- crates/editor/src/hover_popover.rs | 392 ++++++++++++++++++++++++----- crates/theme/src/theme.rs | 8 +- styles/src/style_tree/editor.ts | 8 +- 4 files changed, 382 insertions(+), 117 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 24ffa64a6a..b1c5e35703 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -859,7 +859,6 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, - project: Option>, completions: Arc<[Completion]>, match_candidates: Vec, matches: Arc<[StringMatch]>, @@ -903,42 +902,17 @@ impl CompletionsMenu { fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} - let language_servers = self.project.as_ref().map(|project| { - project - .read(cx) - .language_servers_for_buffer(self.buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .map(|(adapter, server)| (server.server_id(), adapter.short_name)) - .collect::>() - }); - let needs_server_name = language_servers - .as_ref() - .map_or(false, |servers| servers.len() > 1); - - let get_server_name = - move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> { - language_servers - .iter() - .flatten() - .find_map(|(server_id, server_name)| { - if *server_id == lookup_server_id { - Some(*server_name) - } else { - None - } - }) - }; - let widest_completion_ix = self .matches .iter() .enumerate() .max_by_key(|(_, mat)| { let completion = &self.completions[mat.candidate_id]; - let mut len = completion.label.text.chars().count(); + let documentation = &completion.lsp_completion.documentation; - if let Some(server_name) = get_server_name(completion.server_id) { - len += server_name.chars().count(); + let mut len = completion.label.text.chars().count(); + if let Some(lsp::Documentation::String(text)) = documentation { + len += text.chars().count(); } len @@ -948,8 +922,16 @@ impl CompletionsMenu { let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; - let container_style = style.autocomplete.container; - UniformList::new( + + let alongside_docs_text_style = TextStyle { + soft_wrap: true, + ..style.text.clone() + }; + let alongside_docs_width = style.autocomplete.alongside_docs_width; + let alongside_docs_container_style = style.autocomplete.alongside_docs_container; + let outer_container_style = style.autocomplete.container; + + let list = UniformList::new( self.list.clone(), matches.len(), cx, @@ -957,7 +939,9 @@ impl CompletionsMenu { let start_ix = range.start; for (ix, mat) in matches[range].iter().enumerate() { let completion = &completions[mat.candidate_id]; + let documentation = &completion.lsp_completion.documentation; let item_ix = start_ix + ix; + items.push( MouseEventHandler::new::( mat.candidate_id, @@ -986,22 +970,18 @@ impl CompletionsMenu { ), ); - if let Some(server_name) = get_server_name(completion.server_id) { + if let Some(lsp::Documentation::String(text)) = documentation { Flex::row() .with_child(completion_label) .with_children((|| { - if !needs_server_name { - return None; - } - let text_style = TextStyle { - color: style.autocomplete.server_name_color, + color: style.autocomplete.inline_docs_color, font_size: style.text.font_size - * style.autocomplete.server_name_size_percent, + * style.autocomplete.inline_docs_size_percent, ..style.text.clone() }; - let label = Text::new(server_name, text_style) + let label = Text::new(text.clone(), text_style) .aligned() .constrained() .dynamically(move |constraint, _, _| { @@ -1021,7 +1001,7 @@ impl CompletionsMenu { .with_style( style .autocomplete - .server_name_container, + .inline_docs_container, ) .into_any(), ) @@ -1065,10 +1045,29 @@ impl CompletionsMenu { } }, ) - .with_width_from_item(widest_completion_ix) - .contained() - .with_style(container_style) - .into_any() + .with_width_from_item(widest_completion_ix); + + Flex::row() + .with_child(list) + .with_children({ + let completion = &self.completions[selected_item]; + let documentation = &completion.lsp_completion.documentation; + + if let Some(lsp::Documentation::MarkupContent(content)) = documentation { + Some( + Text::new(content.value.clone(), alongside_docs_text_style) + .constrained() + .with_width(alongside_docs_width) + .contained() + .with_style(alongside_docs_container_style), + ) + } else { + None + } + }) + .contained() + .with_style(outer_container_style) + .into_any() } pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { @@ -3150,7 +3149,6 @@ impl Editor { }); let id = post_inc(&mut self.next_completion_id); - let project = self.project.clone(); let task = cx.spawn(|this, mut cx| { async move { let menu = if let Some(completions) = completions.await.log_err() { @@ -3169,7 +3167,6 @@ impl Editor { }) .collect(), buffer, - project, completions: completions.into(), matches: Vec::new().into(), selected_item: 0, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 553cb321c3..69b5562c34 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, - link_go_to_definition::{InlayHighlight, RangeInEditor}, + link_go_to_definition::{DocumentRange, InlayRange}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -8,12 +8,12 @@ use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + fonts::{HighlightStyle, Underline, Weight}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; -use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -50,18 +50,19 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC pub struct InlayHover { pub excerpt: ExcerptId, - pub range: InlayHighlight, + pub triggered_from: InlayOffset, + pub range: InlayRange, pub tooltip: HoverBlock, } pub fn find_hovered_hint_part( label_parts: Vec, - hint_start: InlayOffset, + hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_start { - let mut hovered_character = (hovered_offset - hint_start).0; - let mut part_start = hint_start; + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { @@ -87,8 +88,10 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie }; if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - if let RangeInEditor::Inlay(range) = symbol_range { - if range == &inlay_hover.range { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..range.highlight_end) + .contains(&inlay_hover.triggered_from) + { // Hover triggered from same location as last time. Don't show again. return; } @@ -96,6 +99,18 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie hide_hover(editor, cx); } + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + let task = cx.spawn(|this, mut cx| { async move { cx.background() @@ -107,7 +122,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let hover_popover = InfoPopover { project: project.clone(), - symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), + symbol_range: DocumentRange::Inlay(inlay_hover.range), blocks: vec![inlay_hover.tooltip], language: None, rendered_content: None, @@ -311,7 +326,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: RangeInEditor::Text(range), + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -346,43 +361,237 @@ fn show_hover( } fn render_blocks( + theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, language: Option<&Arc>, -) -> RichText { - let mut data = RichText { - text: Default::default(), - highlights: Default::default(), - region_ranges: Default::default(), - regions: Default::default(), - }; + style: &EditorStyle, +) -> RenderedInfo { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut data.text, &mut Vec::new()); - data.text.push_str(&block.text); + new_paragraph(&mut text, &mut Vec::new()); + text.push_str(&block.text); } + HoverBlockKind::Markdown => { - render_markdown_mut(&block.text, language_registry, language, &mut data) + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&block.text, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code( + &mut text, + &mut highlights, + t.as_ref(), + language, + style, + ); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if let Some(link_url) = link_url.clone() { + region_ranges.push(prev_len..text.len()); + regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), style)); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + if link_url.is_some() { + highlights.push(( + prev_len..text.len(), + HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }, + )); + } + regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(&mut text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(&mut text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + language.cloned() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } } + HoverBlockKind::Code { language } => { if let Some(language) = language_registry .language_for_name(language) .now_or_never() .and_then(Result::ok) { - render_code(&mut data.text, &mut data.highlights, &block.text, &language); + render_code(&mut text, &mut highlights, &block.text, &language, style); } else { - data.text.push_str(&block.text); + text.push_str(&block.text); } } } } - data.text = data.text.trim().to_string(); + RenderedInfo { + theme_id, + text: text.trim().to_string(), + highlights, + region_ranges, + regions, + } +} - data +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + content: &str, + language: &Arc, + style: &EditorStyle, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if let Some(style) = highlight_id.style(&style.syntax) { + highlights.push((prev_len + range.start..prev_len + range.end, style)); + } + } +} + +fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } } #[derive(Default)] @@ -415,8 +624,8 @@ impl HoverState { self.info_popover .as_ref() .map(|info_popover| match &info_popover.symbol_range { - RangeInEditor::Text(range) => &range.start, - RangeInEditor::Inlay(range) => &range.inlay_position, + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -442,10 +651,25 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - symbol_range: RangeInEditor, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - rendered_content: Option, + rendered_content: Option, +} + +#[derive(Debug, Clone)] +struct RenderedInfo { + theme_id: usize, + text: String, + highlights: Vec<(Range, HighlightStyle)>, + region_ranges: Vec>, + regions: Vec, +} + +#[derive(Debug, Clone)] +struct RenderedRegion { + code: bool, + link_url: Option, } impl InfoPopover { @@ -454,24 +678,63 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { + if let Some(rendered) = &self.rendered_content { + if rendered.theme_id != style.theme_id { + self.rendered_content = None; + } + } + let rendered_content = self.rendered_content.get_or_insert_with(|| { render_blocks( + style.theme_id, &self.blocks, self.project.read(cx).languages(), self.language.as_ref(), + style, ) }); - MouseEventHandler::new::(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { + let mut region_id = 0; + let view_id = cx.view_id(); + let code_span_background_color = style.document_highlight_read_background; + let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) - .with_child(rendered_content.element( - style.syntax.clone(), - style.text.clone(), - code_span_background_color, - cx, - )) + .with_child( + Text::new(rendered_content.text.clone(), style.text.clone()) + .with_highlights(rendered_content.highlights.clone()) + .with_custom_runs( + rendered_content.region_ranges.clone(), + move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::( + MouseButton::Left, + move |_, _, cx| cx.platform().open_url(&url), + ), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }, + ) + .with_soft_wrap(true), + ) .contained() .with_style(style.hover_popover.container) }) @@ -564,15 +827,13 @@ mod tests { inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, link_go_to_definition::update_inlay_link_and_hover_points, test::editor_lsp_test_context::EditorLspTestContext, - InlayId, }; use collections::BTreeSet; - use gpui::fonts::{HighlightStyle, Underline, Weight}; + use gpui::fonts::Weight; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; - use rich_text::Highlight; use smol::stream::StreamExt; use unindent::Unindent; use util::test::marked_text_ranges; @@ -783,7 +1044,7 @@ mod tests { .await; cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, _| { + cx.editor(|editor, cx| { let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; assert_eq!( blocks, @@ -793,7 +1054,8 @@ mod tests { }], ); - let rendered = render_blocks(&blocks, &Default::default(), None); + let style = editor.style(cx); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); assert_eq!( rendered.text, code_str.trim(), @@ -985,7 +1247,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(&blocks, &Default::default(), None); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -996,21 +1258,8 @@ mod tests { rendered.text, expected_text, "wrong text for input {blocks:?}" ); - - let rendered_highlights: Vec<_> = rendered - .highlights - .iter() - .filter_map(|(range, highlight)| { - let style = match highlight { - Highlight::Id(id) => id.style(&style.syntax)?, - Highlight::Highlight(style) => style.clone(), - }; - Some((range.clone(), style)) - }) - .collect(); - assert_eq!( - rendered_highlights, expected_highlights, + rendered.highlights, expected_highlights, "wrong highlights for input {blocks:?}" ); } @@ -1244,16 +1493,25 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); assert_eq!( popover.symbol_range, - RangeInEditor::Inlay(InlayHighlight { - inlay: InlayId::Hint(0), + DocumentRange::Inlay(InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - range: ": ".len()..": ".len() + new_type_label.len(), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), }), "Popover range should match the new type label part" ); @@ -1301,17 +1559,23 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); assert_eq!( popover.symbol_range, - RangeInEditor::Inlay(InlayHighlight { - inlay: InlayId::Hint(0), + DocumentRange::Inlay(InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - range: ": ".len() + new_type_label.len() + "<".len() - ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), }), "Popover range should match the struct label part" ); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e534ba4260..9f7530ec18 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -867,9 +867,11 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, - pub server_name_container: ContainerStyle, - pub server_name_color: Color, - pub server_name_size_percent: f32, + pub inline_docs_container: ContainerStyle, + pub inline_docs_color: Color, + pub inline_docs_size_percent: f32, + pub alongside_docs_width: f32, + pub alongside_docs_container: ContainerStyle, } #[derive(Clone, Copy, Default, Deserialize, JsonSchema)] diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index e55a73c365..37d6c4ea1e 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -206,9 +206,11 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, - server_name_container: { padding: { left: 40 } }, - server_name_color: text(theme.middle, "sans", "disabled", {}).color, - server_name_size_percent: 0.75, + inline_docs_container: { padding: { left: 40 } }, + inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, + inline_docs_size_percent: 0.75, + alongside_docs_width: 400, + alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { background: background(theme.middle), From 1584dae9c211c65120825314e4a6b5fbda39cd1d Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 5 Sep 2023 22:23:16 -0400 Subject: [PATCH 016/274] Actually display the correct completion's doc --- crates/editor/src/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b1c5e35703..ba2b50c1c1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1050,7 +1050,8 @@ impl CompletionsMenu { Flex::row() .with_child(list) .with_children({ - let completion = &self.completions[selected_item]; + let mat = &self.matches[selected_item]; + let completion = &self.completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; if let Some(lsp::Documentation::MarkupContent(content)) = documentation { From 370a3cafd0381b088ca1ac2122ecea968c7f9839 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 14 Sep 2023 15:24:46 -0400 Subject: [PATCH 017/274] Add markdown rendering to alongside completion docs --- crates/editor/src/editor.rs | 51 ++++--- crates/editor/src/markdown.rs | 246 ++++++++++++++++++++++++++++++++ styles/src/style_tree/editor.ts | 2 +- 3 files changed, 280 insertions(+), 19 deletions(-) create mode 100644 crates/editor/src/markdown.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba2b50c1c1..2035bd35f0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,6 +9,7 @@ mod highlight_matching_bracket; mod hover_popover; pub mod items; mod link_go_to_definition; +mod markdown; mod mouse_context_menu; pub mod movement; pub mod multi_buffer; @@ -845,11 +846,12 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, + editor: &Editor, style: EditorStyle, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(editor, style, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } @@ -899,7 +901,12 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { + fn render( + &self, + editor: &Editor, + style: EditorStyle, + cx: &mut ViewContext, + ) -> AnyElement { enum CompletionTag {} let widest_completion_ix = self @@ -923,18 +930,12 @@ impl CompletionsMenu { let matches = self.matches.clone(); let selected_item = self.selected_item; - let alongside_docs_text_style = TextStyle { - soft_wrap: true, - ..style.text.clone() - }; let alongside_docs_width = style.autocomplete.alongside_docs_width; let alongside_docs_container_style = style.autocomplete.alongside_docs_container; let outer_container_style = style.autocomplete.container; - let list = UniformList::new( - self.list.clone(), - matches.len(), - cx, + let list = UniformList::new(self.list.clone(), matches.len(), cx, { + let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; for (ix, mat) in matches[range].iter().enumerate() { @@ -1043,8 +1044,8 @@ impl CompletionsMenu { .into_any(), ); } - }, - ) + } + }) .with_width_from_item(widest_completion_ix); Flex::row() @@ -1055,12 +1056,26 @@ impl CompletionsMenu { let documentation = &completion.lsp_completion.documentation; if let Some(lsp::Documentation::MarkupContent(content)) = documentation { + let registry = editor + .project + .as_ref() + .unwrap() + .read(cx) + .languages() + .clone(); + let language = self.buffer.read(cx).language().map(Arc::clone); Some( - Text::new(content.value.clone(), alongside_docs_text_style) - .constrained() - .with_width(alongside_docs_width) - .contained() - .with_style(alongside_docs_container_style), + crate::markdown::render_markdown( + &content.value, + ®istry, + &language, + &style, + cx, + ) + .constrained() + .with_width(alongside_docs_width) + .contained() + .with_style(alongside_docs_container_style), ) } else { None @@ -3985,7 +4000,7 @@ impl Editor { ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) + .map(|menu| menu.render(cursor_position, self, style, cx)) } fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs new file mode 100644 index 0000000000..3ea8db34b3 --- /dev/null +++ b/crates/editor/src/markdown.rs @@ -0,0 +1,246 @@ +use std::ops::Range; +use std::sync::Arc; + +use futures::FutureExt; +use gpui::{ + elements::Text, + fonts::{HighlightStyle, Underline, Weight}, + platform::{CursorStyle, MouseButton}, + CursorRegion, MouseRegion, ViewContext, +}; +use language::{Language, LanguageRegistry}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + +use crate::{Editor, EditorStyle}; + +#[derive(Debug, Clone)] +struct RenderedRegion { + code: bool, + link_url: Option, +} + +pub fn render_markdown( + markdown: &str, + language_registry: &Arc, + language: &Option>, + style: &EditorStyle, + cx: &mut ViewContext, +) -> Text { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut region_ranges = Vec::new(); + let mut regions = Vec::new(); + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&markdown, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code(&mut text, &mut highlights, t.as_ref(), language, style); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if let Some(link_url) = link_url.clone() { + region_ranges.push(prev_len..text.len()); + regions.push(RenderedRegion { + link_url: Some(link_url), + code: false, + }); + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), style)); + } + } + } + } + + Event::Code(t) => { + text.push_str(t.as_ref()); + region_ranges.push(prev_len..text.len()); + if link_url.is_some() { + highlights.push(( + prev_len..text.len(), + HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }, + )); + } + regions.push(RenderedRegion { + code: true, + link_url: link_url.clone(), + }); + } + + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + + Tag::Heading(_, _, _) => { + new_paragraph(&mut text, &mut list_stack); + bold_depth += 1; + } + + Tag::CodeBlock(kind) => { + new_paragraph(&mut text, &mut list_stack); + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok) + } else { + language.clone() + } + } + + Tag::Emphasis => italic_depth += 1, + + Tag::Strong => bold_depth += 1, + + Tag::Link(_, url, _) => link_url = Some(url.to_string()), + + Tag::List(number) => { + list_stack.push((number, false)); + } + + Tag::Item => { + let len = list_stack.len(); + if let Some((list_number, has_content)) = list_stack.last_mut() { + *has_content = false; + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_number { + text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + text.push_str("- "); + } + } + } + + _ => {} + }, + + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + + Event::HardBreak => text.push('\n'), + + Event::SoftBreak => text.push(' '), + + _ => {} + } + } + + let code_span_background_color = style.document_highlight_read_background; + let view_id = cx.view_id(); + let mut region_id = 0; + Text::new(text, style.text.clone()) + .with_highlights(highlights) + .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + content: &str, + language: &Arc, + style: &EditorStyle, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if let Some(style) = highlight_id.style(&style.syntax) { + highlights.push((prev_len + range.start..prev_len + range.end, style)); + } + } +} + +fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { + let mut is_subsequent_paragraph_of_list = false; + if let Some((_, has_content)) = list_stack.last_mut() { + if *has_content { + is_subsequent_paragraph_of_list = true; + } else { + *has_content = true; + return; + } + } + + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } + for _ in 0..list_stack.len().saturating_sub(1) { + text.push_str(" "); + } + if is_subsequent_paragraph_of_list { + text.push_str(" "); + } +} diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 37d6c4ea1e..e7717583a8 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -209,7 +209,7 @@ export default function editor(): any { inline_docs_container: { padding: { left: 40 } }, inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, inline_docs_size_percent: 0.75, - alongside_docs_width: 400, + alongside_docs_width: 700, alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { From e8be14e5d64d6b574ea626ff50e59e84875ebf39 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 15 Sep 2023 11:51:57 -0400 Subject: [PATCH 018/274] Merge info popover's and autocomplete docs' markdown rendering --- crates/editor/src/hover_popover.rs | 230 ++++------------------------- crates/editor/src/markdown.rs | 102 ++++++++----- 2 files changed, 87 insertions(+), 245 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 69b5562c34..16ecb2dc01 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,7 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{DocumentRange, InlayRange}, + markdown::{self, RenderedRegion}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -8,7 +9,7 @@ use futures::FutureExt; use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, - fonts::{HighlightStyle, Underline, Weight}, + fonts::HighlightStyle, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; @@ -364,7 +365,7 @@ fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, - language: Option<&Arc>, + language: &Option>, style: &EditorStyle, ) -> RenderedInfo { let mut text = String::new(); @@ -375,160 +376,20 @@ fn render_blocks( for block in blocks { match &block.kind { HoverBlockKind::PlainText => { - new_paragraph(&mut text, &mut Vec::new()); + markdown::new_paragraph(&mut text, &mut Vec::new()); text.push_str(&block.text); } - HoverBlockKind::Markdown => { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; - - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut link_url = None; - let mut current_language = None; - let mut list_stack = Vec::new(); - - for event in Parser::new_ext(&block.text, Options::all()) { - let prev_len = text.len(); - match event { - Event::Text(t) => { - if let Some(language) = ¤t_language { - render_code( - &mut text, - &mut highlights, - t.as_ref(), - language, - style, - ); - } else { - text.push_str(t.as_ref()); - - let mut style = HighlightStyle::default(); - if bold_depth > 0 { - style.weight = Some(Weight::BOLD); - } - if italic_depth > 0 { - style.italic = Some(true); - } - if let Some(link_url) = link_url.clone() { - region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { - link_url: Some(link_url), - code: false, - }); - style.underline = Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }); - } - - if style != HighlightStyle::default() { - let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { - if last_range.end == prev_len && last_style == &style { - last_range.end = text.len(); - new_highlight = false; - } - } - if new_highlight { - highlights.push((prev_len..text.len(), style)); - } - } - } - } - - Event::Code(t) => { - text.push_str(t.as_ref()); - region_ranges.push(prev_len..text.len()); - if link_url.is_some() { - highlights.push(( - prev_len..text.len(), - HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - }, - )); - } - regions.push(RenderedRegion { - code: true, - link_url: link_url.clone(), - }); - } - - Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), - - Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); - bold_depth += 1; - } - - Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); - current_language = if let CodeBlockKind::Fenced(language) = kind { - language_registry - .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) - } else { - language.cloned() - } - } - - Tag::Emphasis => italic_depth += 1, - - Tag::Strong => bold_depth += 1, - - Tag::Link(_, url, _) => link_url = Some(url.to_string()), - - Tag::List(number) => { - list_stack.push((number, false)); - } - - Tag::Item => { - let len = list_stack.len(); - if let Some((list_number, has_content)) = list_stack.last_mut() { - *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - for _ in 0..len - 1 { - text.push_str(" "); - } - if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); - *number += 1; - *has_content = false; - } else { - text.push_str("- "); - } - } - } - - _ => {} - }, - - Event::End(tag) => match tag { - Tag::Heading(_, _, _) => bold_depth -= 1, - Tag::CodeBlock(_) => current_language = None, - Tag::Emphasis => italic_depth -= 1, - Tag::Strong => bold_depth -= 1, - Tag::Link(_, _, _) => link_url = None, - Tag::List(_) => drop(list_stack.pop()), - _ => {} - }, - - Event::HardBreak => text.push('\n'), - - Event::SoftBreak => text.push(' '), - - _ => {} - } - } - } + HoverBlockKind::Markdown => markdown::render_markdown_block( + &block.text, + language_registry, + language, + style, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ), HoverBlockKind::Code { language } => { if let Some(language) = language_registry @@ -536,7 +397,13 @@ fn render_blocks( .now_or_never() .and_then(Result::ok) { - render_code(&mut text, &mut highlights, &block.text, &language, style); + markdown::render_code( + &mut text, + &mut highlights, + &block.text, + &language, + style, + ); } else { text.push_str(&block.text); } @@ -553,47 +420,6 @@ fn render_blocks( } } -fn render_code( - text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, - content: &str, - language: &Arc, - style: &EditorStyle, -) { - let prev_len = text.len(); - text.push_str(content); - for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if let Some(style) = highlight_id.style(&style.syntax) { - highlights.push((prev_len + range.start..prev_len + range.end, style)); - } - } -} - -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { - let mut is_subsequent_paragraph_of_list = false; - if let Some((_, has_content)) = list_stack.last_mut() { - if *has_content { - is_subsequent_paragraph_of_list = true; - } else { - *has_content = true; - return; - } - } - - if !text.is_empty() { - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - } - for _ in 0..list_stack.len().saturating_sub(1) { - text.push_str(" "); - } - if is_subsequent_paragraph_of_list { - text.push_str(" "); - } -} - #[derive(Default)] pub struct HoverState { pub info_popover: Option, @@ -666,12 +492,6 @@ struct RenderedInfo { regions: Vec, } -#[derive(Debug, Clone)] -struct RenderedRegion { - code: bool, - link_url: Option, -} - impl InfoPopover { pub fn render( &mut self, @@ -689,7 +509,7 @@ impl InfoPopover { style.theme_id, &self.blocks, self.project.read(cx).languages(), - self.language.as_ref(), + &self.language, style, ) }); @@ -829,7 +649,7 @@ mod tests { test::editor_lsp_test_context::EditorLspTestContext, }; use collections::BTreeSet; - use gpui::fonts::Weight; + use gpui::fonts::{Underline, Weight}; use indoc::indoc; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; @@ -1055,7 +875,7 @@ mod tests { ); let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); assert_eq!( rendered.text, code_str.trim(), @@ -1247,7 +1067,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs index 3ea8db34b3..df5041c0db 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/editor/src/markdown.rs @@ -14,9 +14,9 @@ use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use crate::{Editor, EditorStyle}; #[derive(Debug, Clone)] -struct RenderedRegion { - code: bool, - link_url: Option, +pub struct RenderedRegion { + pub code: bool, + pub link_url: Option, } pub fn render_markdown( @@ -31,6 +31,59 @@ pub fn render_markdown( let mut region_ranges = Vec::new(); let mut regions = Vec::new(); + render_markdown_block( + markdown, + language_registry, + language, + style, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ); + + let code_span_background_color = style.document_highlight_read_background; + let view_id = cx.view_id(); + let mut region_id = 0; + Text::new(text, style.text.clone()) + .with_highlights(highlights) + .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + +pub fn render_markdown_block( + markdown: &str, + language_registry: &Arc, + language: &Option>, + style: &EditorStyle, + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + region_ranges: &mut Vec>, + regions: &mut Vec, +) { let mut bold_depth = 0; let mut italic_depth = 0; let mut link_url = None; @@ -42,7 +95,7 @@ pub fn render_markdown( match event { Event::Text(t) => { if let Some(language) = ¤t_language { - render_code(&mut text, &mut highlights, t.as_ref(), language, style); + render_code(text, highlights, t.as_ref(), language, style); } else { text.push_str(t.as_ref()); @@ -102,15 +155,15 @@ pub fn render_markdown( } Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(&mut text, &mut list_stack), + Tag::Paragraph => new_paragraph(text, &mut list_stack), Tag::Heading(_, _, _) => { - new_paragraph(&mut text, &mut list_stack); + new_paragraph(text, &mut list_stack); bold_depth += 1; } Tag::CodeBlock(kind) => { - new_paragraph(&mut text, &mut list_stack); + new_paragraph(text, &mut list_stack); current_language = if let CodeBlockKind::Fenced(language) = kind { language_registry .language_for_name(language.as_ref()) @@ -171,40 +224,9 @@ pub fn render_markdown( _ => {} } } - - let code_span_background_color = style.document_highlight_read_background; - let view_id = cx.view_id(); - let mut region_id = 0; - Text::new(text, style.text.clone()) - .with_highlights(highlights) - .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) - }), - ); - } - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) } -fn render_code( +pub fn render_code( text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, content: &str, @@ -220,7 +242,7 @@ fn render_code( } } -fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { let mut is_subsequent_paragraph_of_list = false; if let Some((_, has_content)) = list_stack.last_mut() { if *has_content { From ca88717f0c3145b441f3efeb81d53d578b1a9c7e Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 15 Sep 2023 15:12:04 -0400 Subject: [PATCH 019/274] Make completion docs scrollable --- crates/editor/src/editor.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2035bd35f0..2f02ac59b0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1064,18 +1064,22 @@ impl CompletionsMenu { .languages() .clone(); let language = self.buffer.read(cx).language().map(Arc::clone); + + enum CompletionDocsMarkdown {} Some( - crate::markdown::render_markdown( - &content.value, - ®istry, - &language, - &style, - cx, - ) - .constrained() - .with_width(alongside_docs_width) - .contained() - .with_style(alongside_docs_container_style), + Flex::column() + .scrollable::(0, None, cx) + .with_child(crate::markdown::render_markdown( + &content.value, + ®istry, + &language, + &style, + cx, + )) + .constrained() + .with_width(alongside_docs_width) + .contained() + .with_style(alongside_docs_container_style), ) } else { None From 77ba25328cbeee3bfec5c3acef71fa8ff4394870 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 22 Sep 2023 15:10:48 -0400 Subject: [PATCH 020/274] Most of getting completion documentation resolved & cached MD parsing --- Cargo.lock | 2 +- crates/editor/Cargo.toml | 1 - crates/editor/src/editor.rs | 170 ++++++++++++++++---- crates/editor/src/hover_popover.rs | 6 +- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 2 + crates/language/src/language.rs | 1 + crates/{editor => language}/src/markdown.rs | 90 ++++++----- crates/language/src/proto.rs | 1 + crates/lsp/src/lsp.rs | 14 +- crates/project/src/lsp_command.rs | 1 + crates/zed/src/languages.rs | 4 +- 12 files changed, 217 insertions(+), 76 deletions(-) rename crates/{editor => language}/src/markdown.rs (79%) diff --git a/Cargo.lock b/Cargo.lock index c971846a5d..147760ab14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2404,7 +2404,6 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", - "pulldown-cmark", "rand 0.8.5", "rich_text", "rpc", @@ -3990,6 +3989,7 @@ dependencies = [ "lsp", "parking_lot 0.11.2", "postage", + "pulldown-cmark", "rand 0.8.5", "regex", "rpc", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 2c3d6227a9..d03e1c1106 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -57,7 +57,6 @@ log.workspace = true ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true -pulldown-cmark = { version = "0.9.2", default-features = false } rand.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2f02ac59b0..c0d2b4ee0b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,7 +9,6 @@ mod highlight_matching_bracket; mod hover_popover; pub mod items; mod link_go_to_definition; -mod markdown; mod mouse_context_menu; pub mod movement; pub mod multi_buffer; @@ -78,6 +77,7 @@ pub use multi_buffer::{ ToPoint, }; use ordered_float::OrderedFloat; +use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; use rpc::proto::PeerId; @@ -788,10 +788,14 @@ enum ContextMenu { } impl ContextMenu { - fn select_first(&mut self, cx: &mut ViewContext) -> bool { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(cx), + ContextMenu::Completions(menu) => menu.select_first(project, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -800,10 +804,14 @@ impl ContextMenu { } } - fn select_prev(&mut self, cx: &mut ViewContext) -> bool { + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(cx), + ContextMenu::Completions(menu) => menu.select_prev(project, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -812,10 +820,14 @@ impl ContextMenu { } } - fn select_next(&mut self, cx: &mut ViewContext) -> bool { + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(cx), + ContextMenu::Completions(menu) => menu.select_next(project, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -824,10 +836,14 @@ impl ContextMenu { } } - fn select_last(&mut self, cx: &mut ViewContext) -> bool { + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(cx), + ContextMenu::Completions(menu) => menu.select_last(project, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -861,7 +877,7 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, - completions: Arc<[Completion]>, + completions: Arc>>, match_candidates: Vec, matches: Arc<[StringMatch]>, selected_item: usize, @@ -869,34 +885,115 @@ struct CompletionsMenu { } impl CompletionsMenu { - fn select_first(&mut self, cx: &mut ViewContext) { + fn select_first( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } - fn select_prev(&mut self, cx: &mut ViewContext) { + fn select_prev( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } - fn select_next(&mut self, cx: &mut ViewContext) { + fn select_next( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } - fn select_last(&mut self, cx: &mut ViewContext) { + fn select_last( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } + fn attempt_resolve_selected_completion( + &mut self, + project: Option<&ModelHandle>, + cx: &mut ViewContext, + ) { + println!("attempt_resolve_selected_completion"); + let index = self.matches[dbg!(self.selected_item)].candidate_id; + dbg!(index); + let Some(project) = project else { + println!("no project"); + return; + }; + + let completions = self.completions.clone(); + let completions_guard = completions.read(); + let completion = &completions_guard[index]; + if completion.lsp_completion.documentation.is_some() { + println!("has existing documentation"); + return; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let Some(server) = project.read(cx).language_server_for_id(server_id) else { + println!("no server"); + return; + }; + + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !dbg!(can_resolve) { + return; + } + + cx.spawn(|this, mut cx| async move { + println!("in spawn"); + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + println!("errored"); + return; + }; + + if completion_item.documentation.is_some() { + println!("got new documentation"); + let mut completions = completions.write(); + completions[index].lsp_completion.documentation = completion_item.documentation; + println!("notifying"); + _ = this.update(&mut cx, |_, cx| cx.notify()); + } else { + println!("did not get anything"); + } + }) + .detach(); + } + fn visible(&self) -> bool { !self.matches.is_empty() } @@ -914,7 +1011,8 @@ impl CompletionsMenu { .iter() .enumerate() .max_by_key(|(_, mat)| { - let completion = &self.completions[mat.candidate_id]; + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; let mut len = completion.label.text.chars().count(); @@ -938,6 +1036,7 @@ impl CompletionsMenu { let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; + let completions = completions.read(); for (ix, mat) in matches[range].iter().enumerate() { let completion = &completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; @@ -1052,7 +1151,8 @@ impl CompletionsMenu { .with_child(list) .with_children({ let mat = &self.matches[selected_item]; - let completion = &self.completions[mat.candidate_id]; + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; let documentation = &completion.lsp_completion.documentation; if let Some(lsp::Documentation::MarkupContent(content)) = documentation { @@ -1069,13 +1169,12 @@ impl CompletionsMenu { Some( Flex::column() .scrollable::(0, None, cx) - .with_child(crate::markdown::render_markdown( - &content.value, - ®istry, - &language, - &style, - cx, - )) + // .with_child(language::markdown::render_markdown( + // &content.value, + // ®istry, + // &language, + // &style, + // )) .constrained() .with_width(alongside_docs_width) .contained() @@ -1130,17 +1229,20 @@ impl CompletionsMenu { } } + let completions = self.completions.read(); matches.sort_unstable_by_key(|mat| { - let completion = &self.completions[mat.candidate_id]; + let completion = &completions[mat.candidate_id]; ( completion.lsp_completion.sort_text.as_ref(), Reverse(OrderedFloat(mat.score)), completion.sort_key(), ) }); + drop(completions); for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].label.filter_range.start; + let completions = self.completions.read(); + let filter_start = completions[mat.candidate_id].label.filter_range.start; for position in &mut mat.positions { *position += filter_start; } @@ -3187,7 +3289,7 @@ impl Editor { }) .collect(), buffer, - completions: completions.into(), + completions: Arc::new(RwLock::new(completions.into())), matches: Vec::new().into(), selected_item: 0, list: Default::default(), @@ -3196,6 +3298,9 @@ impl Editor { if menu.matches.is_empty() { None } else { + _ = this.update(&mut cx, |editor, cx| { + menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); + }); Some(menu) } } else { @@ -3252,7 +3357,8 @@ impl Editor { .matches .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; let buffer_handle = completions_menu.buffer; - let completion = completions_menu.completions.get(mat.candidate_id)?; + let completions = completions_menu.completions.read(); + let completion = completions.get(mat.candidate_id)?; let snippet; let text; @@ -5372,7 +5478,7 @@ impl Editor { if self .context_menu .as_mut() - .map(|menu| menu.select_last(cx)) + .map(|menu| menu.select_last(self.project.as_ref(), cx)) .unwrap_or(false) { return; @@ -5416,25 +5522,25 @@ impl Editor { pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(cx); + context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(cx); + context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(cx); + context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(cx); + context_menu.select_last(self.project.as_ref(), cx); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 16ecb2dc01..ea6eac3a66 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,7 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{DocumentRange, InlayRange}, - markdown::{self, RenderedRegion}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -13,7 +12,10 @@ use gpui::{ platform::{CursorStyle, MouseButton}, AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; -use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; +use language::{ + markdown::{self, RenderedRegion}, + Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, +}; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4771fc7083..d5d5bdd1af 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -46,6 +46,7 @@ lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } regex.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 207c41e7cd..10585633ae 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,6 +1,7 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, + markdown::RenderedMarkdown, proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ @@ -148,6 +149,7 @@ pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, + pub alongside_documentation: Option, pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7d113a88af..12f76f1df3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2,6 +2,7 @@ mod buffer; mod diagnostic_set; mod highlight_map; pub mod language_settings; +pub mod markdown; mod outline; pub mod proto; mod syntax_map; diff --git a/crates/editor/src/markdown.rs b/crates/language/src/markdown.rs similarity index 79% rename from crates/editor/src/markdown.rs rename to crates/language/src/markdown.rs index df5041c0db..e033820a21 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,6 +1,7 @@ use std::ops::Range; use std::sync::Arc; +use crate::{Language, LanguageRegistry}; use futures::FutureExt; use gpui::{ elements::Text, @@ -8,10 +9,50 @@ use gpui::{ platform::{CursorStyle, MouseButton}, CursorRegion, MouseRegion, ViewContext, }; -use language::{Language, LanguageRegistry}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; -use crate::{Editor, EditorStyle}; +#[derive(Debug, Clone)] +pub struct RenderedMarkdown { + text: String, + highlights: Vec<(Range, HighlightStyle)>, + region_ranges: Vec>, + regions: Vec, +} + +// impl RenderedMarkdown { +// pub fn render(&self, style: &theme::Editor, cx: &mut ViewContext) -> Text { +// let code_span_background_color = style.document_highlight_read_background; +// let view_id = cx.view_id(); +// let mut region_id = 0; +// Text::new(text, style.text.clone()) +// .with_highlights(highlights) +// .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { +// region_id += 1; +// let region = regions[ix].clone(); +// if let Some(url) = region.link_url { +// scene.push_cursor_region(CursorRegion { +// bounds, +// style: CursorStyle::PointingHand, +// }); +// scene.push_mouse_region( +// MouseRegion::new::(view_id, region_id, bounds) +// .on_click::(MouseButton::Left, move |_, _, cx| { +// cx.platform().open_url(&url) +// }), +// ); +// } +// if region.code { +// scene.push_quad(gpui::Quad { +// bounds, +// background: Some(code_span_background_color), +// border: Default::default(), +// corner_radii: (2.0).into(), +// }); +// } +// }) +// .with_soft_wrap(true) +// } +// } #[derive(Debug, Clone)] pub struct RenderedRegion { @@ -23,9 +64,8 @@ pub fn render_markdown( markdown: &str, language_registry: &Arc, language: &Option>, - style: &EditorStyle, - cx: &mut ViewContext, -) -> Text { + style: &theme::Editor, +) -> RenderedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); @@ -42,43 +82,19 @@ pub fn render_markdown( &mut regions, ); - let code_span_background_color = style.document_highlight_read_background; - let view_id = cx.view_id(); - let mut region_id = 0; - Text::new(text, style.text.clone()) - .with_highlights(highlights) - .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) - }), - ); - } - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) + RenderedMarkdown { + text, + highlights, + region_ranges, + regions, + } } pub fn render_markdown_block( markdown: &str, language_registry: &Arc, language: &Option>, - style: &EditorStyle, + style: &theme::Editor, text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, region_ranges: &mut Vec>, @@ -231,7 +247,7 @@ pub fn render_code( highlights: &mut Vec<(Range, HighlightStyle)>, content: &str, language: &Arc, - style: &EditorStyle, + style: &theme::Editor, ) { let prev_len = text.len(); text.push_str(content); diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c4abe39d47..49b332b4fb 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -482,6 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + alongside_documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 33581721ae..b4099e2f6e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -466,7 +466,10 @@ impl LanguageServer { completion_item: Some(CompletionItemCapability { snippet_support: Some(true), resolve_support: Some(CompletionItemCapabilityResolveSupport { - properties: vec!["additionalTextEdits".to_string()], + properties: vec![ + "documentation".to_string(), + "additionalTextEdits".to_string(), + ], }), ..Default::default() }), @@ -748,6 +751,15 @@ impl LanguageServer { ) } + // some child of string literal (be it "" or ``) which is the child of an attribute + + // + // + // + // + // const classes = "awesome "; + // + fn request_internal( next_id: &AtomicUsize, response_handlers: &Mutex>>, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8beaea5031..000fd3928c 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1462,6 +1462,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + alongside_documentation: None, server_id, lsp_completion, } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 04e5292a7d..3b65255a3d 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -128,8 +128,8 @@ pub fn init( "tsx", tree_sitter_typescript::language_tsx(), vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + // Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + // Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); From fcaf48eb4965349bedc7daab6561060686c8c757 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 22 Sep 2023 17:03:40 -0400 Subject: [PATCH 021/274] Use completion item default `data` when provided --- crates/project/src/lsp_command.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 000fd3928c..16ebb7467b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1358,7 +1358,7 @@ impl LspCommand for GetCompletions { } } } else { - Default::default() + Vec::new() }; let completions = buffer.read_with(&cx, |buffer, _| { @@ -1370,6 +1370,14 @@ impl LspCommand for GetCompletions { completions .into_iter() .filter_map(move |mut lsp_completion| { + if let Some(response_list) = &response_list { + if let Some(item_defaults) = &response_list.item_defaults { + if let Some(data) = &item_defaults.data { + lsp_completion.data = Some(data.clone()); + } + } + } + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { // If the language server provides a range to overwrite, then // check that the range is valid. From fe62423344fedb81c17a5f6aa81b56805fc8d2bc Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 28 Sep 2023 14:16:30 -0400 Subject: [PATCH 022/274] Asynchronously request completion documentation if not present --- crates/editor/src/editor.rs | 78 +++++++++++++++++++++++---------- crates/language/src/markdown.rs | 50 +++------------------ 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c0d2b4ee0b..d35a9dd30e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, - Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, + CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -119,6 +119,46 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub fn render_rendered_markdown( + md: &language::RenderedMarkdown, + style: &EditorStyle, + cx: &mut ViewContext, +) -> Text { + enum RenderedRenderedMarkdown {} + + let md = md.clone(); + let code_span_background_color = style.document_highlight_read_background; + let view_id = cx.view_id(); + let mut region_id = 0; + Text::new(md.text, style.text.clone()) + .with_highlights(md.highlights) + .with_custom_runs(md.region_ranges, move |ix, bounds, scene, _| { + region_id += 1; + let region = md.regions[ix].clone(); + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) +} + #[derive(Clone, Deserialize, PartialEq, Default)] pub struct SelectNext { #[serde(default)] @@ -938,11 +978,8 @@ impl CompletionsMenu { project: Option<&ModelHandle>, cx: &mut ViewContext, ) { - println!("attempt_resolve_selected_completion"); - let index = self.matches[dbg!(self.selected_item)].candidate_id; - dbg!(index); + let index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { - println!("no project"); return; }; @@ -950,7 +987,6 @@ impl CompletionsMenu { let completions_guard = completions.read(); let completion = &completions_guard[index]; if completion.lsp_completion.documentation.is_some() { - println!("has existing documentation"); return; } @@ -959,7 +995,6 @@ impl CompletionsMenu { drop(completions_guard); let Some(server) = project.read(cx).language_server_for_id(server_id) else { - println!("no server"); return; }; @@ -969,26 +1004,21 @@ impl CompletionsMenu { .as_ref() .and_then(|options| options.resolve_provider) .unwrap_or(false); - if !dbg!(can_resolve) { + if !can_resolve { return; } cx.spawn(|this, mut cx| async move { - println!("in spawn"); let request = server.request::(completion); let Some(completion_item) = request.await.log_err() else { - println!("errored"); return; }; if completion_item.documentation.is_some() { - println!("got new documentation"); let mut completions = completions.write(); completions[index].lsp_completion.documentation = completion_item.documentation; - println!("notifying"); + drop(completions); _ = this.update(&mut cx, |_, cx| cx.notify()); - } else { - println!("did not get anything"); } }) .detach(); @@ -1169,12 +1199,16 @@ impl CompletionsMenu { Some( Flex::column() .scrollable::(0, None, cx) - // .with_child(language::markdown::render_markdown( - // &content.value, - // ®istry, - // &language, - // &style, - // )) + .with_child(render_rendered_markdown( + &language::markdown::render_markdown( + &content.value, + ®istry, + &language, + &style.theme, + ), + &style, + cx, + )) .constrained() .with_width(alongside_docs_width) .contained() diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index e033820a21..4ccd2955b6 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -3,57 +3,17 @@ use std::sync::Arc; use crate::{Language, LanguageRegistry}; use futures::FutureExt; -use gpui::{ - elements::Text, - fonts::{HighlightStyle, Underline, Weight}, - platform::{CursorStyle, MouseButton}, - CursorRegion, MouseRegion, ViewContext, -}; +use gpui::fonts::{HighlightStyle, Underline, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] pub struct RenderedMarkdown { - text: String, - highlights: Vec<(Range, HighlightStyle)>, - region_ranges: Vec>, - regions: Vec, + pub text: String, + pub highlights: Vec<(Range, HighlightStyle)>, + pub region_ranges: Vec>, + pub regions: Vec, } -// impl RenderedMarkdown { -// pub fn render(&self, style: &theme::Editor, cx: &mut ViewContext) -> Text { -// let code_span_background_color = style.document_highlight_read_background; -// let view_id = cx.view_id(); -// let mut region_id = 0; -// Text::new(text, style.text.clone()) -// .with_highlights(highlights) -// .with_custom_runs(region_ranges, move |ix, bounds, scene, _| { -// region_id += 1; -// let region = regions[ix].clone(); -// if let Some(url) = region.link_url { -// scene.push_cursor_region(CursorRegion { -// bounds, -// style: CursorStyle::PointingHand, -// }); -// scene.push_mouse_region( -// MouseRegion::new::(view_id, region_id, bounds) -// .on_click::(MouseButton::Left, move |_, _, cx| { -// cx.platform().open_url(&url) -// }), -// ); -// } -// if region.code { -// scene.push_quad(gpui::Quad { -// bounds, -// background: Some(code_span_background_color), -// border: Default::default(), -// corner_radii: (2.0).into(), -// }); -// } -// }) -// .with_soft_wrap(true) -// } -// } - #[derive(Debug, Clone)] pub struct RenderedRegion { pub code: bool, From b8876f2b17307917bf24b20ffcdd090f9dc5126e Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 3 Oct 2023 10:58:08 -0400 Subject: [PATCH 023/274] Preparse documentation markdown when resolving completion --- crates/editor/src/editor.rs | 131 ++++++++++++++++------------- crates/editor/src/hover_popover.rs | 26 +++--- crates/language/src/buffer.rs | 44 +++++++++- crates/language/src/markdown.rs | 30 +++---- crates/language/src/proto.rs | 2 +- crates/project/src/lsp_command.rs | 2 +- 6 files changed, 144 insertions(+), 91 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d35a9dd30e..0f117cde1b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -119,12 +119,12 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub fn render_rendered_markdown( - md: &language::RenderedMarkdown, +pub fn render_parsed_markdown( + md: &language::ParsedMarkdown, style: &EditorStyle, cx: &mut ViewContext, ) -> Text { - enum RenderedRenderedMarkdown {} + enum RenderedMarkdown {} let md = md.clone(); let code_span_background_color = style.document_highlight_read_background; @@ -141,7 +141,7 @@ pub fn render_rendered_markdown( style: CursorStyle::PointingHand, }); scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) + MouseRegion::new::(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), @@ -831,11 +831,12 @@ impl ContextMenu { fn select_first( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(project, cx), + ContextMenu::Completions(menu) => menu.select_first(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -847,11 +848,12 @@ impl ContextMenu { fn select_prev( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(project, cx), + ContextMenu::Completions(menu) => menu.select_prev(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -863,11 +865,12 @@ impl ContextMenu { fn select_next( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(project, cx), + ContextMenu::Completions(menu) => menu.select_next(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -879,11 +882,12 @@ impl ContextMenu { fn select_last( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(project, cx), + ContextMenu::Completions(menu) => menu.select_last(project, style, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -928,60 +932,66 @@ impl CompletionsMenu { fn select_first( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn select_prev( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn select_next( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn select_last( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion(project, style, cx); cx.notify(); } fn attempt_resolve_selected_completion( &mut self, project: Option<&ModelHandle>, + style: theme::Editor, cx: &mut ViewContext, ) { let index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { return; }; + let language_registry = project.read(cx).languages().clone(); let completions = self.completions.clone(); let completions_guard = completions.read(); @@ -1008,16 +1018,27 @@ impl CompletionsMenu { return; } + // TODO: Do on background cx.spawn(|this, mut cx| async move { let request = server.request::(completion); let Some(completion_item) = request.await.log_err() else { return; }; - if completion_item.documentation.is_some() { + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + &style, + ); + let mut completions = completions.write(); - completions[index].lsp_completion.documentation = completion_item.documentation; + let completion = &mut completions[index]; + completion.documentation = documentation; + completion.lsp_completion.documentation = Some(lsp_documentation); drop(completions); + _ = this.update(&mut cx, |_, cx| cx.notify()); } }) @@ -1069,7 +1090,7 @@ impl CompletionsMenu { let completions = completions.read(); for (ix, mat) in matches[range].iter().enumerate() { let completion = &completions[mat.candidate_id]; - let documentation = &completion.lsp_completion.documentation; + let documentation = &completion.documentation; let item_ix = start_ix + ix; items.push( @@ -1100,7 +1121,9 @@ impl CompletionsMenu { ), ); - if let Some(lsp::Documentation::String(text)) = documentation { + if let Some(language::Documentation::SingleLine(text)) = + documentation + { Flex::row() .with_child(completion_label) .with_children((|| { @@ -1183,39 +1206,18 @@ impl CompletionsMenu { let mat = &self.matches[selected_item]; let completions = self.completions.read(); let completion = &completions[mat.candidate_id]; - let documentation = &completion.lsp_completion.documentation; + let documentation = &completion.documentation; - if let Some(lsp::Documentation::MarkupContent(content)) = documentation { - let registry = editor - .project - .as_ref() - .unwrap() - .read(cx) - .languages() - .clone(); - let language = self.buffer.read(cx).language().map(Arc::clone); + match documentation { + Some(language::Documentation::MultiLinePlainText(text)) => { + Some(Text::new(text.clone(), style.text.clone())) + } - enum CompletionDocsMarkdown {} - Some( - Flex::column() - .scrollable::(0, None, cx) - .with_child(render_rendered_markdown( - &language::markdown::render_markdown( - &content.value, - ®istry, - &language, - &style.theme, - ), - &style, - cx, - )) - .constrained() - .with_width(alongside_docs_width) - .contained() - .with_style(alongside_docs_container_style), - ) - } else { - None + Some(language::Documentation::MultiLineMarkdown(parsed)) => { + Some(render_parsed_markdown(parsed, &style, cx)) + } + + _ => None, } }) .contained() @@ -3333,7 +3335,11 @@ impl Editor { None } else { _ = this.update(&mut cx, |editor, cx| { - menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); + menu.attempt_resolve_selected_completion( + editor.project.as_ref(), + editor.style(cx).theme, + cx, + ); }); Some(menu) } @@ -5509,13 +5515,16 @@ impl Editor { return; } - if self - .context_menu - .as_mut() - .map(|menu| menu.select_last(self.project.as_ref(), cx)) - .unwrap_or(false) - { - return; + if self.context_menu.is_some() { + let style = self.style(cx).theme; + if self + .context_menu + .as_mut() + .map(|menu| menu.select_last(self.project.as_ref(), style, cx)) + .unwrap_or(false) + { + return; + } } if matches!(self.mode, EditorMode::SingleLine) { @@ -5555,26 +5564,30 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(self.project.as_ref(), cx); + context_menu.select_first(self.project.as_ref(), style, cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(self.project.as_ref(), cx); + context_menu.select_prev(self.project.as_ref(), style, cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(self.project.as_ref(), cx); + context_menu.select_next(self.project.as_ref(), style, cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(self.project.as_ref(), cx); + context_menu.select_last(self.project.as_ref(), style, cx); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index ea6eac3a66..9341fa2da3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -13,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{ - markdown::{self, RenderedRegion}, + markdown::{self, ParsedRegion}, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, }; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; @@ -367,9 +367,9 @@ fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, - language: &Option>, + language: Option>, style: &EditorStyle, -) -> RenderedInfo { +) -> ParsedInfo { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); @@ -382,10 +382,10 @@ fn render_blocks( text.push_str(&block.text); } - HoverBlockKind::Markdown => markdown::render_markdown_block( + HoverBlockKind::Markdown => markdown::parse_markdown_block( &block.text, language_registry, - language, + language.clone(), style, &mut text, &mut highlights, @@ -399,7 +399,7 @@ fn render_blocks( .now_or_never() .and_then(Result::ok) { - markdown::render_code( + markdown::highlight_code( &mut text, &mut highlights, &block.text, @@ -413,7 +413,7 @@ fn render_blocks( } } - RenderedInfo { + ParsedInfo { theme_id, text: text.trim().to_string(), highlights, @@ -482,16 +482,16 @@ pub struct InfoPopover { symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - rendered_content: Option, + rendered_content: Option, } #[derive(Debug, Clone)] -struct RenderedInfo { +struct ParsedInfo { theme_id: usize, text: String, highlights: Vec<(Range, HighlightStyle)>, region_ranges: Vec>, - regions: Vec, + regions: Vec, } impl InfoPopover { @@ -511,7 +511,7 @@ impl InfoPopover { style.theme_id, &self.blocks, self.project.read(cx).languages(), - &self.language, + self.language.clone(), style, ) }); @@ -877,7 +877,7 @@ mod tests { ); let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); assert_eq!( rendered.text, code_str.trim(), @@ -1069,7 +1069,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 10585633ae..344b470aa9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,12 +1,13 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, - markdown::RenderedMarkdown, + markdown::ParsedMarkdown, proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, + markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -144,12 +145,51 @@ pub struct Diagnostic { pub is_unnecessary: bool, } +pub fn prepare_completion_documentation( + documentation: &lsp::Documentation, + language_registry: &Arc, + language: Option>, + style: &theme::Editor, +) -> Option { + match documentation { + lsp::Documentation::String(text) => { + if text.lines().count() <= 1 { + Some(Documentation::SingleLine(text.clone())) + } else { + Some(Documentation::MultiLinePlainText(text.clone())) + } + } + + lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { + lsp::MarkupKind::PlainText => { + if value.lines().count() <= 1 { + Some(Documentation::SingleLine(value.clone())) + } else { + Some(Documentation::MultiLinePlainText(value.clone())) + } + } + + lsp::MarkupKind::Markdown => { + let parsed = markdown::parse_markdown(value, language_registry, language, style); + Some(Documentation::MultiLineMarkdown(parsed)) + } + }, + } +} + +#[derive(Clone, Debug)] +pub enum Documentation { + SingleLine(String), + MultiLinePlainText(String), + MultiLineMarkdown(ParsedMarkdown), +} + #[derive(Clone, Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, - pub alongside_documentation: Option, + pub documentation: Option, pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 4ccd2955b6..c56a676378 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -7,31 +7,31 @@ use gpui::fonts::{HighlightStyle, Underline, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] -pub struct RenderedMarkdown { +pub struct ParsedMarkdown { pub text: String, pub highlights: Vec<(Range, HighlightStyle)>, pub region_ranges: Vec>, - pub regions: Vec, + pub regions: Vec, } #[derive(Debug, Clone)] -pub struct RenderedRegion { +pub struct ParsedRegion { pub code: bool, pub link_url: Option, } -pub fn render_markdown( +pub fn parse_markdown( markdown: &str, language_registry: &Arc, - language: &Option>, + language: Option>, style: &theme::Editor, -) -> RenderedMarkdown { +) -> ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); let mut regions = Vec::new(); - render_markdown_block( + parse_markdown_block( markdown, language_registry, language, @@ -42,7 +42,7 @@ pub fn render_markdown( &mut regions, ); - RenderedMarkdown { + ParsedMarkdown { text, highlights, region_ranges, @@ -50,15 +50,15 @@ pub fn render_markdown( } } -pub fn render_markdown_block( +pub fn parse_markdown_block( markdown: &str, language_registry: &Arc, - language: &Option>, + language: Option>, style: &theme::Editor, text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, region_ranges: &mut Vec>, - regions: &mut Vec, + regions: &mut Vec, ) { let mut bold_depth = 0; let mut italic_depth = 0; @@ -71,7 +71,7 @@ pub fn render_markdown_block( match event { Event::Text(t) => { if let Some(language) = ¤t_language { - render_code(text, highlights, t.as_ref(), language, style); + highlight_code(text, highlights, t.as_ref(), language, style); } else { text.push_str(t.as_ref()); @@ -84,7 +84,7 @@ pub fn render_markdown_block( } if let Some(link_url) = link_url.clone() { region_ranges.push(prev_len..text.len()); - regions.push(RenderedRegion { + regions.push(ParsedRegion { link_url: Some(link_url), code: false, }); @@ -124,7 +124,7 @@ pub fn render_markdown_block( }, )); } - regions.push(RenderedRegion { + regions.push(ParsedRegion { code: true, link_url: link_url.clone(), }); @@ -202,7 +202,7 @@ pub fn render_markdown_block( } } -pub fn render_code( +pub fn highlight_code( text: &mut String, highlights: &mut Vec<(Range, HighlightStyle)>, content: &str, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 49b332b4fb..957f4ee7fb 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -482,7 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), - alongside_documentation: None, + documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 16ebb7467b..400dbe2abf 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1470,7 +1470,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), - alongside_documentation: None, + documentation: None, server_id, lsp_completion, } From ea6f366d2348135897fdb4a803097d4ffdfdab24 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 3 Oct 2023 11:58:49 -0400 Subject: [PATCH 024/274] If documentation exists and hasn't been parsed, do so at render and keep --- crates/editor/src/editor.rs | 46 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0f117cde1b..cabf73b581 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,10 +60,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, - LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + point_from_lsp, prepare_completion_documentation, AutoindentMode, BracketPair, Buffer, + CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, + File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -1075,21 +1075,37 @@ impl CompletionsMenu { }) .map(|(ix, _)| ix); + let project = editor.project.clone(); let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; - let alongside_docs_width = style.autocomplete.alongside_docs_width; - let alongside_docs_container_style = style.autocomplete.alongside_docs_container; - let outer_container_style = style.autocomplete.container; - let list = UniformList::new(self.list.clone(), matches.len(), cx, { let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; - let completions = completions.read(); + let mut completions = completions.write(); + for (ix, mat) in matches[range].iter().enumerate() { - let completion = &completions[mat.candidate_id]; + let completion = &mut completions[mat.candidate_id]; + + if completion.documentation.is_none() { + if let Some(lsp_docs) = &completion.lsp_completion.documentation { + let project = project + .as_ref() + .expect("It is impossible have LSP servers without a project"); + + let language_registry = project.read(cx).languages(); + + completion.documentation = prepare_completion_documentation( + lsp_docs, + language_registry, + None, + &style.theme, + ); + } + } + let documentation = &completion.documentation; let item_ix = start_ix + ix; @@ -1121,9 +1137,7 @@ impl CompletionsMenu { ), ); - if let Some(language::Documentation::SingleLine(text)) = - documentation - { + if let Some(Documentation::SingleLine(text)) = documentation { Flex::row() .with_child(completion_label) .with_children((|| { @@ -1209,11 +1223,11 @@ impl CompletionsMenu { let documentation = &completion.documentation; match documentation { - Some(language::Documentation::MultiLinePlainText(text)) => { + Some(Documentation::MultiLinePlainText(text)) => { Some(Text::new(text.clone(), style.text.clone())) } - Some(language::Documentation::MultiLineMarkdown(parsed)) => { + Some(Documentation::MultiLineMarkdown(parsed)) => { Some(render_parsed_markdown(parsed, &style, cx)) } @@ -1221,7 +1235,7 @@ impl CompletionsMenu { } }) .contained() - .with_style(outer_container_style) + .with_style(style.autocomplete.container) .into_any() } From a881b1f5fb4b2c28a48581a90e16dba1201afe7e Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 4 Oct 2023 17:36:51 -0400 Subject: [PATCH 025/274] Wait for language to load when parsing markdown --- crates/editor/src/editor.rs | 42 ++++--- crates/editor/src/hover_popover.rs | 169 +++++++++++++++++------------ crates/language/src/buffer.rs | 6 +- crates/language/src/markdown.rs | 12 +- 4 files changed, 137 insertions(+), 92 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cabf73b581..257abad41b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1018,7 +1018,6 @@ impl CompletionsMenu { return; } - // TODO: Do on background cx.spawn(|this, mut cx| async move { let request = server.request::(completion); let Some(completion_item) = request.await.log_err() else { @@ -1031,7 +1030,8 @@ impl CompletionsMenu { &language_registry, None, // TODO: Try to reasonably work out which language the completion is for &style, - ); + ) + .await; let mut completions = completions.write(); let completion = &mut completions[index]; @@ -1084,30 +1084,46 @@ impl CompletionsMenu { let style = style.clone(); move |_, range, items, cx| { let start_ix = range.start; - let mut completions = completions.write(); + let completions_guard = completions.read(); for (ix, mat) in matches[range].iter().enumerate() { - let completion = &mut completions[mat.candidate_id]; + let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; - if completion.documentation.is_none() { + if item_ix == selected_item && completion.documentation.is_none() { if let Some(lsp_docs) = &completion.lsp_completion.documentation { let project = project .as_ref() .expect("It is impossible have LSP servers without a project"); - let language_registry = project.read(cx).languages(); + let lsp_docs = lsp_docs.clone(); + let lsp_docs = lsp_docs.clone(); + let language_registry = project.read(cx).languages().clone(); + let style = style.theme.clone(); + let completions = completions.clone(); - completion.documentation = prepare_completion_documentation( - lsp_docs, - language_registry, - None, - &style.theme, - ); + cx.spawn(|this, mut cx| async move { + let documentation = prepare_completion_documentation( + &lsp_docs, + &language_registry, + None, + &style, + ) + .await; + + this.update(&mut cx, |_, cx| { + let mut completions = completions.write(); + completions[candidate_id].documentation = documentation; + drop(completions); + cx.notify(); + }) + }) + .detach(); } } let documentation = &completion.documentation; - let item_ix = start_ix + ix; items.push( MouseEventHandler::new::( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9341fa2da3..585a335bd6 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -7,7 +7,7 @@ use crate::{ use futures::FutureExt; use gpui::{ actions, - elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + elements::{Empty, Flex, MouseEventHandler, Padding, ParentElement, Text}, fonts::HighlightStyle, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, @@ -128,7 +128,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie symbol_range: DocumentRange::Inlay(inlay_hover.range), blocks: vec![inlay_hover.tooltip], language: None, - rendered_content: None, + parsed_content: None, }; this.update(&mut cx, |this, cx| { @@ -332,7 +332,7 @@ fn show_hover( symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, - rendered_content: None, + parsed_content: None, }) }); @@ -363,12 +363,12 @@ fn show_hover( editor.hover_state.info_task = Some(task); } -fn render_blocks( +async fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, language: Option>, - style: &EditorStyle, + style: &theme::Editor, ) -> ParsedInfo { let mut text = String::new(); let mut highlights = Vec::new(); @@ -382,16 +382,19 @@ fn render_blocks( text.push_str(&block.text); } - HoverBlockKind::Markdown => markdown::parse_markdown_block( - &block.text, - language_registry, - language.clone(), - style, - &mut text, - &mut highlights, - &mut region_ranges, - &mut regions, - ), + HoverBlockKind::Markdown => { + markdown::parse_markdown_block( + &block.text, + language_registry, + language.clone(), + style, + &mut text, + &mut highlights, + &mut region_ranges, + &mut regions, + ) + .await + } HoverBlockKind::Code { language } => { if let Some(language) = language_registry @@ -482,7 +485,7 @@ pub struct InfoPopover { symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - rendered_content: Option, + parsed_content: Option, } #[derive(Debug, Clone)] @@ -500,63 +503,87 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - if let Some(rendered) = &self.rendered_content { - if rendered.theme_id != style.theme_id { - self.rendered_content = None; + if let Some(parsed) = &self.parsed_content { + if parsed.theme_id != style.theme_id { + self.parsed_content = None; } } - let rendered_content = self.rendered_content.get_or_insert_with(|| { - render_blocks( - style.theme_id, - &self.blocks, - self.project.read(cx).languages(), - self.language.clone(), - style, - ) - }); + let rendered = if let Some(parsed) = &self.parsed_content { + let view_id = cx.view_id(); + let regions = parsed.regions.clone(); + let code_span_background_color = style.document_highlight_read_background; + + let mut region_id = 0; + + Text::new(parsed.text.clone(), style.text.clone()) + .with_highlights(parsed.highlights.clone()) + .with_custom_runs(parsed.region_ranges.clone(), move |ix, bounds, scene, _| { + region_id += 1; + let region = regions[ix].clone(); + + if let Some(url) = region.link_url { + scene.push_cursor_region(CursorRegion { + bounds, + style: CursorStyle::PointingHand, + }); + scene.push_mouse_region( + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + cx.platform().open_url(&url) + }), + ); + } + + if region.code { + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radii: (2.0).into(), + }); + } + }) + .with_soft_wrap(true) + .into_any() + } else { + let theme_id = style.theme_id; + let language_registry = self.project.read(cx).languages().clone(); + let blocks = self.blocks.clone(); + let language = self.language.clone(); + let style = style.theme.clone(); + cx.spawn(|this, mut cx| async move { + let blocks = + render_blocks(theme_id, &blocks, &language_registry, language, &style).await; + _ = this.update(&mut cx, |_, cx| cx.notify()); + blocks + }) + .detach(); + + Empty::new().into_any() + }; + + // let rendered_content = self.parsed_content.get_or_insert_with(|| { + // let language_registry = self.project.read(cx).languages().clone(); + // cx.spawn(|this, mut cx| async move { + // let blocks = render_blocks( + // style.theme_id, + // &self.blocks, + // &language_registry, + // self.language.clone(), + // style, + // ) + // .await; + // this.update(&mut cx, |_, cx| cx.notify()); + // blocks + // }) + // .shared() + // }); MouseEventHandler::new::(0, cx, |_, cx| { - let mut region_id = 0; - let view_id = cx.view_id(); - - let code_span_background_color = style.document_highlight_read_background; - let regions = rendered_content.regions.clone(); Flex::column() .scrollable::(1, None, cx) - .with_child( - Text::new(rendered_content.text.clone(), style.text.clone()) - .with_highlights(rendered_content.highlights.clone()) - .with_custom_runs( - rendered_content.region_ranges.clone(), - move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::( - MouseButton::Left, - move |_, _, cx| cx.platform().open_url(&url), - ), - ); - } - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }, - ) - .with_soft_wrap(true), - ) + .with_child(rendered) .contained() .with_style(style.hover_popover.container) }) @@ -877,7 +904,8 @@ mod tests { ); let style = editor.style(cx); - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = + smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); assert_eq!( rendered.text, code_str.trim(), @@ -1069,7 +1097,8 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + let rendered = + smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges @@ -1339,7 +1368,7 @@ mod tests { ); assert_eq!( popover - .rendered_content + .parsed_content .as_ref() .expect("should have label text for new type hint") .text, @@ -1403,7 +1432,7 @@ mod tests { ); assert_eq!( popover - .rendered_content + .parsed_content .as_ref() .expect("should have label text for struct hint") .text, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 344b470aa9..971494ea4f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -7,7 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, - markdown, + markdown::parse_markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -145,7 +145,7 @@ pub struct Diagnostic { pub is_unnecessary: bool, } -pub fn prepare_completion_documentation( +pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, language: Option>, @@ -170,7 +170,7 @@ pub fn prepare_completion_documentation( } lsp::MarkupKind::Markdown => { - let parsed = markdown::parse_markdown(value, language_registry, language, style); + let parsed = parse_markdown(value, language_registry, language, style).await; Some(Documentation::MultiLineMarkdown(parsed)) } }, diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index c56a676378..de5c7e8b09 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -2,7 +2,6 @@ use std::ops::Range; use std::sync::Arc; use crate::{Language, LanguageRegistry}; -use futures::FutureExt; use gpui::fonts::{HighlightStyle, Underline, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; @@ -20,7 +19,7 @@ pub struct ParsedRegion { pub link_url: Option, } -pub fn parse_markdown( +pub async fn parse_markdown( markdown: &str, language_registry: &Arc, language: Option>, @@ -40,7 +39,8 @@ pub fn parse_markdown( &mut highlights, &mut region_ranges, &mut regions, - ); + ) + .await; ParsedMarkdown { text, @@ -50,7 +50,7 @@ pub fn parse_markdown( } } -pub fn parse_markdown_block( +pub async fn parse_markdown_block( markdown: &str, language_registry: &Arc, language: Option>, @@ -143,8 +143,8 @@ pub fn parse_markdown_block( current_language = if let CodeBlockKind::Fenced(language) = kind { language_registry .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) + .await + .ok() } else { language.clone() } From 8dca4c3f9ac9b923fd9bef415fb0c6de19f5becb Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 5 Oct 2023 14:40:41 -0400 Subject: [PATCH 026/274] Don't need editor style to parse markdown --- crates/editor/src/editor.rs | 118 ++++++++++++---------- crates/editor/src/hover_popover.rs | 154 ++++++----------------------- crates/language/src/buffer.rs | 3 +- crates/language/src/markdown.rs | 63 ++++++------ 4 files changed, 133 insertions(+), 205 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 257abad41b..6b2be7c719 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,6 +60,7 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, + markdown::MarkdownHighlight, point_from_lsp, prepare_completion_documentation, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, @@ -120,21 +121,57 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( - md: &language::ParsedMarkdown, - style: &EditorStyle, + parsed: &language::ParsedMarkdown, + editor_style: &EditorStyle, cx: &mut ViewContext, ) -> Text { enum RenderedMarkdown {} - let md = md.clone(); - let code_span_background_color = style.document_highlight_read_background; + let parsed = parsed.clone(); let view_id = cx.view_id(); + let code_span_background_color = editor_style.document_highlight_read_background; + let mut region_id = 0; - Text::new(md.text, style.text.clone()) - .with_highlights(md.highlights) - .with_custom_runs(md.region_ranges, move |ix, bounds, scene, _| { + + Text::new(parsed.text, editor_style.text.clone()) + .with_highlights( + parsed + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = match highlight { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.italic = Some(true); + } + + if style.underline { + highlight.underline = Some(fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style.weight != fonts::Weight::default() { + highlight.weight = Some(style.weight); + } + + highlight + } + + MarkdownHighlight::Code(id) => id.style(&editor_style.syntax)?, + }; + + Some((range.clone(), highlight)) + }) + .collect::>(), + ) + .with_custom_runs(parsed.region_ranges, move |ix, bounds, scene, _| { region_id += 1; - let region = md.regions[ix].clone(); + let region = parsed.regions[ix].clone(); + if let Some(url) = region.link_url { scene.push_cursor_region(CursorRegion { bounds, @@ -147,6 +184,7 @@ pub fn render_parsed_markdown( }), ); } + if region.code { scene.push_quad(gpui::Quad { bounds, @@ -831,12 +869,11 @@ impl ContextMenu { fn select_first( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_first(project, style, cx), + ContextMenu::Completions(menu) => menu.select_first(project, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true @@ -848,12 +885,11 @@ impl ContextMenu { fn select_prev( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_prev(project, style, cx), + ContextMenu::Completions(menu) => menu.select_prev(project, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true @@ -865,12 +901,11 @@ impl ContextMenu { fn select_next( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_next(project, style, cx), + ContextMenu::Completions(menu) => menu.select_next(project, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true @@ -882,12 +917,11 @@ impl ContextMenu { fn select_last( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { - ContextMenu::Completions(menu) => menu.select_last(project, style, cx), + ContextMenu::Completions(menu) => menu.select_last(project, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true @@ -932,59 +966,54 @@ impl CompletionsMenu { fn select_first( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn select_prev( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item > 0 { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn select_next( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn select_last( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, style, cx); + self.attempt_resolve_selected_completion(project, cx); cx.notify(); } fn attempt_resolve_selected_completion( &mut self, project: Option<&ModelHandle>, - style: theme::Editor, cx: &mut ViewContext, ) { let index = self.matches[self.selected_item].candidate_id; @@ -1029,7 +1058,6 @@ impl CompletionsMenu { &lsp_documentation, &language_registry, None, // TODO: Try to reasonably work out which language the completion is for - &style, ) .await; @@ -1097,10 +1125,8 @@ impl CompletionsMenu { .as_ref() .expect("It is impossible have LSP servers without a project"); - let lsp_docs = lsp_docs.clone(); let lsp_docs = lsp_docs.clone(); let language_registry = project.read(cx).languages().clone(); - let style = style.theme.clone(); let completions = completions.clone(); cx.spawn(|this, mut cx| async move { @@ -1108,7 +1134,6 @@ impl CompletionsMenu { &lsp_docs, &language_registry, None, - &style, ) .await; @@ -3365,11 +3390,7 @@ impl Editor { None } else { _ = this.update(&mut cx, |editor, cx| { - menu.attempt_resolve_selected_completion( - editor.project.as_ref(), - editor.style(cx).theme, - cx, - ); + menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); }); Some(menu) } @@ -5545,16 +5566,13 @@ impl Editor { return; } - if self.context_menu.is_some() { - let style = self.style(cx).theme; - if self - .context_menu - .as_mut() - .map(|menu| menu.select_last(self.project.as_ref(), style, cx)) - .unwrap_or(false) - { - return; - } + if self + .context_menu + .as_mut() + .map(|menu| menu.select_last(self.project.as_ref(), cx)) + .unwrap_or(false) + { + return; } if matches!(self.mode, EditorMode::SingleLine) { @@ -5594,30 +5612,26 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_first(self.project.as_ref(), style, cx); + context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_prev(self.project.as_ref(), style, cx); + context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_next(self.project.as_ref(), style, cx); + context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - let style = self.style(cx).theme; if let Some(context_menu) = self.context_menu.as_mut() { - context_menu.select_last(self.project.as_ref(), style, cx); + context_menu.select_last(self.project.as_ref(), cx); } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 585a335bd6..51fe27e58a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -8,13 +8,11 @@ use futures::FutureExt; use gpui::{ actions, elements::{Empty, Flex, MouseEventHandler, Padding, ParentElement, Text}, - fonts::HighlightStyle, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, }; use language::{ - markdown::{self, ParsedRegion}, - Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, + markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, }; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; @@ -363,13 +361,11 @@ fn show_hover( editor.hover_state.info_task = Some(task); } -async fn render_blocks( - theme_id: usize, +async fn parse_blocks( blocks: &[HoverBlock], language_registry: &Arc, language: Option>, - style: &theme::Editor, -) -> ParsedInfo { +) -> markdown::ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); @@ -387,7 +383,6 @@ async fn render_blocks( &block.text, language_registry, language.clone(), - style, &mut text, &mut highlights, &mut region_ranges, @@ -402,13 +397,7 @@ async fn render_blocks( .now_or_never() .and_then(Result::ok) { - markdown::highlight_code( - &mut text, - &mut highlights, - &block.text, - &language, - style, - ); + markdown::highlight_code(&mut text, &mut highlights, &block.text, &language); } else { text.push_str(&block.text); } @@ -416,8 +405,7 @@ async fn render_blocks( } } - ParsedInfo { - theme_id, + ParsedMarkdown { text: text.trim().to_string(), highlights, region_ranges, @@ -485,16 +473,7 @@ pub struct InfoPopover { symbol_range: DocumentRange, pub blocks: Vec, language: Option>, - parsed_content: Option, -} - -#[derive(Debug, Clone)] -struct ParsedInfo { - theme_id: usize, - text: String, - highlights: Vec<(Range, HighlightStyle)>, - region_ranges: Vec>, - regions: Vec, + parsed_content: Option, } impl InfoPopover { @@ -503,58 +482,14 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - if let Some(parsed) = &self.parsed_content { - if parsed.theme_id != style.theme_id { - self.parsed_content = None; - } - } - let rendered = if let Some(parsed) = &self.parsed_content { - let view_id = cx.view_id(); - let regions = parsed.regions.clone(); - let code_span_background_color = style.document_highlight_read_background; - - let mut region_id = 0; - - Text::new(parsed.text.clone(), style.text.clone()) - .with_highlights(parsed.highlights.clone()) - .with_custom_runs(parsed.region_ranges.clone(), move |ix, bounds, scene, _| { - region_id += 1; - let region = regions[ix].clone(); - - if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { - bounds, - style: CursorStyle::PointingHand, - }); - scene.push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) - }), - ); - } - - if region.code { - scene.push_quad(gpui::Quad { - bounds, - background: Some(code_span_background_color), - border: Default::default(), - corner_radii: (2.0).into(), - }); - } - }) - .with_soft_wrap(true) - .into_any() + crate::render_parsed_markdown(parsed, style, cx).into_any() } else { - let theme_id = style.theme_id; let language_registry = self.project.read(cx).languages().clone(); let blocks = self.blocks.clone(); let language = self.language.clone(); - let style = style.theme.clone(); cx.spawn(|this, mut cx| async move { - let blocks = - render_blocks(theme_id, &blocks, &language_registry, language, &style).await; + let blocks = parse_blocks(&blocks, &language_registry, language).await; _ = this.update(&mut cx, |_, cx| cx.notify()); blocks }) @@ -563,23 +498,6 @@ impl InfoPopover { Empty::new().into_any() }; - // let rendered_content = self.parsed_content.get_or_insert_with(|| { - // let language_registry = self.project.read(cx).languages().clone(); - // cx.spawn(|this, mut cx| async move { - // let blocks = render_blocks( - // style.theme_id, - // &self.blocks, - // &language_registry, - // self.language.clone(), - // style, - // ) - // .await; - // this.update(&mut cx, |_, cx| cx.notify()); - // blocks - // }) - // .shared() - // }); - MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .scrollable::(1, None, cx) @@ -678,9 +596,12 @@ mod tests { test::editor_lsp_test_context::EditorLspTestContext, }; use collections::BTreeSet; - use gpui::fonts::{Underline, Weight}; + use gpui::fonts::Weight; use indoc::indoc; - use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; + use language::{ + language_settings::InlayHintSettings, markdown::MarkdownHighlightStyle, Diagnostic, + DiagnosticSet, + }; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -893,7 +814,7 @@ mod tests { .await; cx.condition(|editor, _| editor.hover_state.visible()).await; - cx.editor(|editor, cx| { + cx.editor(|editor, _| { let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; assert_eq!( blocks, @@ -903,9 +824,7 @@ mod tests { }], ); - let style = editor.style(cx); - let rendered = - smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); assert_eq!( rendered.text, code_str.trim(), @@ -984,16 +903,17 @@ mod tests { #[gpui::test] fn test_render_blocks(cx: &mut gpui::TestAppContext) { + use markdown::MarkdownHighlight; + init_test(cx, |_| {}); cx.add_window(|cx| { let editor = Editor::single_line(None, cx); - let style = editor.style(cx); struct Row { blocks: Vec, expected_marked_text: String, - expected_styles: Vec, + expected_styles: Vec, } let rows = &[ @@ -1004,10 +924,10 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![HighlightStyle { - weight: Some(Weight::BOLD), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + weight: Weight::BOLD, ..Default::default() - }], + })], }, // Links Row { @@ -1016,13 +936,10 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }], + })], }, // Lists Row { @@ -1047,13 +964,10 @@ mod tests { - «c» - d" .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }], + })], }, // Multi-paragraph list items Row { @@ -1081,13 +995,10 @@ mod tests { - ten - six" .unindent(), - expected_styles: vec![HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }], + })], }, ]; @@ -1097,8 +1008,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = - smol::block_on(render_blocks(0, &blocks, &Default::default(), None, &style)); + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 971494ea4f..d318a87b40 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -149,7 +149,6 @@ pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, language: Option>, - style: &theme::Editor, ) -> Option { match documentation { lsp::Documentation::String(text) => { @@ -170,7 +169,7 @@ pub async fn prepare_completion_documentation( } lsp::MarkupKind::Markdown => { - let parsed = parse_markdown(value, language_registry, language, style).await; + let parsed = parse_markdown(value, language_registry, language).await; Some(Documentation::MultiLineMarkdown(parsed)) } }, diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index de5c7e8b09..9f29e7cb88 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,18 +1,31 @@ use std::ops::Range; use std::sync::Arc; -use crate::{Language, LanguageRegistry}; -use gpui::fonts::{HighlightStyle, Underline, Weight}; +use crate::{HighlightId, Language, LanguageRegistry}; +use gpui::fonts::Weight; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] pub struct ParsedMarkdown { pub text: String, - pub highlights: Vec<(Range, HighlightStyle)>, + pub highlights: Vec<(Range, MarkdownHighlight)>, pub region_ranges: Vec>, pub regions: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MarkdownHighlight { + Style(MarkdownHighlightStyle), + Code(HighlightId), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct MarkdownHighlightStyle { + pub italic: bool, + pub underline: bool, + pub weight: Weight, +} + #[derive(Debug, Clone)] pub struct ParsedRegion { pub code: bool, @@ -23,7 +36,6 @@ pub async fn parse_markdown( markdown: &str, language_registry: &Arc, language: Option>, - style: &theme::Editor, ) -> ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); @@ -34,7 +46,6 @@ pub async fn parse_markdown( markdown, language_registry, language, - style, &mut text, &mut highlights, &mut region_ranges, @@ -54,9 +65,8 @@ pub async fn parse_markdown_block( markdown: &str, language_registry: &Arc, language: Option>, - style: &theme::Editor, text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, + highlights: &mut Vec<(Range, MarkdownHighlight)>, region_ranges: &mut Vec>, regions: &mut Vec, ) { @@ -71,16 +81,16 @@ pub async fn parse_markdown_block( match event { Event::Text(t) => { if let Some(language) = ¤t_language { - highlight_code(text, highlights, t.as_ref(), language, style); + highlight_code(text, highlights, t.as_ref(), language); } else { text.push_str(t.as_ref()); - let mut style = HighlightStyle::default(); + let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { - style.weight = Some(Weight::BOLD); + style.weight = Weight::BOLD; } if italic_depth > 0 { - style.italic = Some(true); + style.italic = true; } if let Some(link_url) = link_url.clone() { region_ranges.push(prev_len..text.len()); @@ -88,22 +98,22 @@ pub async fn parse_markdown_block( link_url: Some(link_url), code: false, }); - style.underline = Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }); + style.underline = true; } - if style != HighlightStyle::default() { + if style != MarkdownHighlightStyle::default() { let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() { + if let Some((last_range, MarkdownHighlight::Style(last_style))) = + highlights.last_mut() + { if last_range.end == prev_len && last_style == &style { last_range.end = text.len(); new_highlight = false; } } if new_highlight { - highlights.push((prev_len..text.len(), style)); + let range = prev_len..text.len(); + highlights.push((range, MarkdownHighlight::Style(style))); } } } @@ -115,13 +125,10 @@ pub async fn parse_markdown_block( if link_url.is_some() { highlights.push(( prev_len..text.len(), - HighlightStyle { - underline: Some(Underline { - thickness: 1.0.into(), - ..Default::default() - }), + MarkdownHighlight::Style(MarkdownHighlightStyle { + underline: true, ..Default::default() - }, + }), )); } regions.push(ParsedRegion { @@ -204,17 +211,15 @@ pub async fn parse_markdown_block( pub fn highlight_code( text: &mut String, - highlights: &mut Vec<(Range, HighlightStyle)>, + highlights: &mut Vec<(Range, MarkdownHighlight)>, content: &str, language: &Arc, - style: &theme::Editor, ) { let prev_len = text.len(); text.push_str(content); for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if let Some(style) = highlight_id.style(&style.syntax) { - highlights.push((prev_len + range.start..prev_len + range.end, style)); - } + let highlight = MarkdownHighlight::Code(highlight_id); + highlights.push((prev_len + range.start..prev_len + range.end, highlight)); } } From 32a29cd4d32d447c00af9157c99f45d57d219cda Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 5 Oct 2023 23:57:01 -0400 Subject: [PATCH 027/274] Unbork info popover parsing/rendering and make better --- crates/editor/src/hover_popover.rs | 105 +++++++++++++---------------- 1 file changed, 45 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 51fe27e58a..7917d57865 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -7,7 +7,7 @@ use crate::{ use futures::FutureExt; use gpui::{ actions, - elements::{Empty, Flex, MouseEventHandler, Padding, ParentElement, Text}, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, }; @@ -121,12 +121,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie this.hover_state.diagnostic_popover = None; })?; + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + let hover_popover = InfoPopover { project: project.clone(), symbol_range: DocumentRange::Inlay(inlay_hover.range), - blocks: vec![inlay_hover.tooltip], - language: None, - parsed_content: None, + blocks, + parsed_content, }; this.update(&mut cx, |this, cx| { @@ -304,35 +307,38 @@ fn show_hover( }); })?; - // Construct new hover popover from hover request - let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { - if hover_result.is_empty() { - return None; + let hover_result = hover_request.await.ok().flatten(); + let hover_popover = match hover_result { + Some(hover_result) if !hover_result.is_empty() => { + // Create symbol range of anchors for highlighting and filtering of future requests. + let range = if let Some(range) = hover_result.range { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), range.end); + + start..end + } else { + anchor..anchor + }; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone()); + let blocks = hover_result.contents; + let language = hover_result.language; + let parsed_content = parse_blocks(&blocks, &language_registry, language).await; + + Some(InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Text(range), + blocks, + parsed_content, + }) } - // Create symbol range of anchors for highlighting and filtering - // of future requests. - let range = if let Some(range) = hover_result.range { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), range.end); - - start..end - } else { - anchor..anchor - }; - - Some(InfoPopover { - project: project.clone(), - symbol_range: DocumentRange::Text(range), - blocks: hover_result.contents, - language: hover_result.language, - parsed_content: None, - }) - }); + _ => None, + }; this.update(&mut cx, |this, cx| { if let Some(symbol_range) = hover_popover @@ -472,8 +478,7 @@ pub struct InfoPopover { pub project: ModelHandle, symbol_range: DocumentRange, pub blocks: Vec, - language: Option>, - parsed_content: Option, + parsed_content: ParsedMarkdown, } impl InfoPopover { @@ -482,26 +487,14 @@ impl InfoPopover { style: &EditorStyle, cx: &mut ViewContext, ) -> AnyElement { - let rendered = if let Some(parsed) = &self.parsed_content { - crate::render_parsed_markdown(parsed, style, cx).into_any() - } else { - let language_registry = self.project.read(cx).languages().clone(); - let blocks = self.blocks.clone(); - let language = self.language.clone(); - cx.spawn(|this, mut cx| async move { - let blocks = parse_blocks(&blocks, &language_registry, language).await; - _ = this.update(&mut cx, |_, cx| cx.notify()); - blocks - }) - .detach(); - - Empty::new().into_any() - }; - MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .scrollable::(1, None, cx) - .with_child(rendered) + .with_child(crate::render_parsed_markdown( + &self.parsed_content, + style, + cx, + )) .contained() .with_style(style.hover_popover.container) }) @@ -1277,11 +1270,7 @@ mod tests { "Popover range should match the new type label part" ); assert_eq!( - popover - .parsed_content - .as_ref() - .expect("should have label text for new type hint") - .text, + popover.parsed_content.text, format!("A tooltip for `{new_type_label}`"), "Rendered text should not anyhow alter backticks" ); @@ -1341,11 +1330,7 @@ mod tests { "Popover range should match the struct label part" ); assert_eq!( - popover - .parsed_content - .as_ref() - .expect("should have label text for struct hint") - .text, + popover.parsed_content.text, format!("A tooltip for {struct_label}"), "Rendered markdown element should remove backticks from text" ); From 9d8cff1275e8bfa6a6f08d48c286cdc9a8a5ab6e Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 6 Oct 2023 00:17:36 -0400 Subject: [PATCH 028/274] If documentation included in original completion then parse up front --- crates/editor/src/editor.rs | 51 +++++-------------------------- crates/project/src/lsp_command.rs | 27 ++++++++++++---- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6b2be7c719..6585611040 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -61,10 +61,10 @@ pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, markdown::MarkdownHighlight, - point_from_lsp, prepare_completion_documentation, AutoindentMode, BracketPair, Buffer, - CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, - File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, - Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, + Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -940,12 +940,11 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, - editor: &Editor, style: EditorStyle, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(editor, style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } @@ -1077,12 +1076,7 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render( - &self, - editor: &Editor, - style: EditorStyle, - cx: &mut ViewContext, - ) -> AnyElement { + fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} let widest_completion_ix = self @@ -1103,7 +1097,6 @@ impl CompletionsMenu { }) .map(|(ix, _)| ix); - let project = editor.project.clone(); let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; @@ -1118,36 +1111,6 @@ impl CompletionsMenu { let item_ix = start_ix + ix; let candidate_id = mat.candidate_id; let completion = &completions_guard[candidate_id]; - - if item_ix == selected_item && completion.documentation.is_none() { - if let Some(lsp_docs) = &completion.lsp_completion.documentation { - let project = project - .as_ref() - .expect("It is impossible have LSP servers without a project"); - - let lsp_docs = lsp_docs.clone(); - let language_registry = project.read(cx).languages().clone(); - let completions = completions.clone(); - - cx.spawn(|this, mut cx| async move { - let documentation = prepare_completion_documentation( - &lsp_docs, - &language_registry, - None, - ) - .await; - - this.update(&mut cx, |_, cx| { - let mut completions = completions.write(); - completions[candidate_id].documentation = documentation; - drop(completions); - cx.notify(); - }) - }) - .detach(); - } - } - let documentation = &completion.documentation; items.push( @@ -4201,7 +4164,7 @@ impl Editor { ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu .as_ref() - .map(|menu| menu.render(cursor_position, self, style, cx)) + .map(|menu| menu.render(cursor_position, style, cx)) } fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 400dbe2abf..c71b378da6 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -10,7 +10,7 @@ use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, - point_from_lsp, point_to_lsp, + point_from_lsp, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, @@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions { async fn response_from_lsp( self, completions: Option, - _: ModelHandle, + project: ModelHandle, buffer: ModelHandle, server_id: LanguageServerId, cx: AsyncAppContext, @@ -1361,7 +1361,8 @@ impl LspCommand for GetCompletions { Vec::new() }; - let completions = buffer.read_with(&cx, |buffer, _| { + let completions = buffer.read_with(&cx, |buffer, cx| { + let language_registry = project.read(cx).languages().clone(); let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -1453,14 +1454,28 @@ impl LspCommand for GetCompletions { } }; - let language = language.clone(); LineEnding::normalize(&mut new_text); + let language_registry = language_registry.clone(); + let language = language.clone(); + Some(async move { let mut label = None; - if let Some(language) = language { + if let Some(language) = language.as_ref() { language.process_completion(&mut lsp_completion).await; label = language.label_for_completion(&lsp_completion).await; } + + let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await + } else { + None + }; + Completion { old_range, new_text, @@ -1470,7 +1485,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), - documentation: None, + documentation, server_id, lsp_completion, } From f18f870206bf274a0e9a9dfd92efd8d4ed4b5c82 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 6 Oct 2023 11:56:55 -0400 Subject: [PATCH 029/274] Re-enable language servers --- crates/zed/src/languages.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 3b65255a3d..04e5292a7d 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -128,8 +128,8 @@ pub fn init( "tsx", tree_sitter_typescript::language_tsx(), vec![ - // Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - // Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); From 7020050b069474ab9145cef6578a95d68f217f43 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 9 Oct 2023 14:28:53 -0400 Subject: [PATCH 030/274] Fix `hover_popover.rs` after bad rebase --- crates/editor/src/editor.rs | 35 ++------ crates/editor/src/hover_popover.rs | 128 +++++++++++++---------------- crates/language/src/markdown.rs | 31 ++++++- 3 files changed, 92 insertions(+), 102 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6585611040..b8c9690b90 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,7 +60,6 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - markdown::MarkdownHighlight, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, @@ -139,45 +138,21 @@ pub fn render_parsed_markdown( .highlights .iter() .filter_map(|(range, highlight)| { - let highlight = match highlight { - MarkdownHighlight::Style(style) => { - let mut highlight = HighlightStyle::default(); - - if style.italic { - highlight.italic = Some(true); - } - - if style.underline { - highlight.underline = Some(fonts::Underline { - thickness: 1.0.into(), - ..Default::default() - }); - } - - if style.weight != fonts::Weight::default() { - highlight.weight = Some(style.weight); - } - - highlight - } - - MarkdownHighlight::Code(id) => id.style(&editor_style.syntax)?, - }; - + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; Some((range.clone(), highlight)) }) .collect::>(), ) - .with_custom_runs(parsed.region_ranges, move |ix, bounds, scene, _| { + .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { region_id += 1; let region = parsed.regions[ix].clone(); if let Some(url) = region.link_url { - scene.push_cursor_region(CursorRegion { + cx.scene().push_cursor_region(CursorRegion { bounds, style: CursorStyle::PointingHand, }); - scene.push_mouse_region( + cx.scene().push_mouse_region( MouseRegion::new::(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) @@ -186,7 +161,7 @@ pub fn render_parsed_markdown( } if region.code { - scene.push_quad(gpui::Quad { + cx.scene().push_quad(gpui::Quad { bounds, background: Some(code_span_background_color), border: Default::default(), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7917d57865..d5ccb481b2 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, - link_go_to_definition::{DocumentRange, InlayRange}, + link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -51,19 +51,18 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC pub struct InlayHover { pub excerpt: ExcerptId, - pub triggered_from: InlayOffset, - pub range: InlayRange, + pub range: InlayHighlight, pub tooltip: HoverBlock, } pub fn find_hovered_hint_part( label_parts: Vec, - hint_range: Range, + hint_start: InlayOffset, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; + if hovered_offset >= hint_start { + let mut hovered_character = (hovered_offset - hint_start).0; + let mut part_start = hint_start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { @@ -89,10 +88,8 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie }; if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..range.highlight_end) - .contains(&inlay_hover.triggered_from) - { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { // Hover triggered from same location as last time. Don't show again. return; } @@ -100,18 +97,6 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie hide_hover(editor, cx); } - let snapshot = editor.snapshot(cx); - // Don't request again if the location is the same as the previous request - if let Some(triggered_from) = editor.hover_state.triggered_from { - if inlay_hover.triggered_from - == snapshot - .display_snapshot - .anchor_to_inlay_offset(triggered_from) - { - return; - } - } - let task = cx.spawn(|this, mut cx| { async move { cx.background() @@ -127,7 +112,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let hover_popover = InfoPopover { project: project.clone(), - symbol_range: DocumentRange::Inlay(inlay_hover.range), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), blocks, parsed_content, }; @@ -331,7 +316,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: DocumentRange::Text(range), + symbol_range: RangeInEditor::Text(range), blocks, parsed_content, }) @@ -449,8 +434,8 @@ impl HoverState { self.info_popover .as_ref() .map(|info_popover| match &info_popover.symbol_range { - DocumentRange::Text(range) => &range.start, - DocumentRange::Inlay(range) => &range.inlay_position, + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -476,7 +461,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - symbol_range: DocumentRange, + symbol_range: RangeInEditor, pub blocks: Vec, parsed_content: ParsedMarkdown, } @@ -587,14 +572,12 @@ mod tests { inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, link_go_to_definition::update_inlay_link_and_hover_points, test::editor_lsp_test_context::EditorLspTestContext, + InlayId, }; use collections::BTreeSet; - use gpui::fonts::Weight; + use gpui::fonts::{HighlightStyle, Underline, Weight}; use indoc::indoc; - use language::{ - language_settings::InlayHintSettings, markdown::MarkdownHighlightStyle, Diagnostic, - DiagnosticSet, - }; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -896,17 +879,16 @@ mod tests { #[gpui::test] fn test_render_blocks(cx: &mut gpui::TestAppContext) { - use markdown::MarkdownHighlight; - init_test(cx, |_| {}); cx.add_window(|cx| { let editor = Editor::single_line(None, cx); + let style = editor.style(cx); struct Row { blocks: Vec, expected_marked_text: String, - expected_styles: Vec, + expected_styles: Vec, } let rows = &[ @@ -917,10 +899,10 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: Weight::BOLD, + expected_styles: vec![HighlightStyle { + weight: Some(Weight::BOLD), ..Default::default() - })], + }], }, // Links Row { @@ -929,10 +911,13 @@ mod tests { kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - })], + }], }, // Lists Row { @@ -957,10 +942,13 @@ mod tests { - «c» - d" .unindent(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - })], + }], }, // Multi-paragraph list items Row { @@ -988,10 +976,13 @@ mod tests { - ten - six" .unindent(), - expected_styles: vec![MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), ..Default::default() - })], + }], }, ]; @@ -1012,8 +1003,18 @@ mod tests { rendered.text, expected_text, "wrong text for input {blocks:?}" ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect(); + assert_eq!( - rendered.highlights, expected_highlights, + rendered_highlights, expected_highlights, "wrong highlights for input {blocks:?}" ); } @@ -1247,25 +1248,16 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let entire_inlay_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - - let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); assert_eq!( popover.symbol_range, - DocumentRange::Inlay(InlayRange { + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_new_type_label_start, - highlight_end: InlayOffset( - expected_new_type_label_start.0 + new_type_label.len() - ), + range: ": ".len()..": ".len() + new_type_label.len(), }), "Popover range should match the new type label part" ); @@ -1309,23 +1301,17 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let entire_inlay_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - let expected_struct_label_start = - InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); assert_eq!( popover.symbol_range, - DocumentRange::Inlay(InlayRange { + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_struct_label_start, - highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), }), "Popover range should match the struct label part" ); diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 9f29e7cb88..8be15e81f6 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -2,7 +2,7 @@ use std::ops::Range; use std::sync::Arc; use crate::{HighlightId, Language, LanguageRegistry}; -use gpui::fonts::Weight; +use gpui::fonts::{self, HighlightStyle, Weight}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; #[derive(Debug, Clone)] @@ -19,6 +19,35 @@ pub enum MarkdownHighlight { Code(HighlightId), } +impl MarkdownHighlight { + pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { + match self { + MarkdownHighlight::Style(style) => { + let mut highlight = HighlightStyle::default(); + + if style.italic { + highlight.italic = Some(true); + } + + if style.underline { + highlight.underline = Some(fonts::Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style.weight != fonts::Weight::default() { + highlight.weight = Some(style.weight); + } + + Some(highlight) + } + + MarkdownHighlight::Code(id) => id.style(theme), + } + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct MarkdownHighlightStyle { pub italic: bool, From a801a4aeef93e24d235da51a9a1fdbe9f5d1d6d1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 27 Sep 2023 13:16:32 -0600 Subject: [PATCH 031/274] Remove some unnecessary Eqs --- .cargo/config.toml | 2 +- crates/language/src/buffer.rs | 4 ++-- crates/text/src/selection.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 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/language/src/buffer.rs b/crates/language/src/buffer.rs index 207c41e7cd..19e5e290b9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -159,7 +159,7 @@ pub struct CodeAction { pub lsp_action: lsp::CodeAction, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] pub enum Operation { Buffer(text::Operation), @@ -182,7 +182,7 @@ pub enum Operation { }, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq)] pub enum Event { Operation(Operation), Edited, diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 205c27239d..60d5e2f1c4 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -2,14 +2,14 @@ use crate::{Anchor, BufferSnapshot, TextDimension}; use std::cmp::Ordering; use std::ops::Range; -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, Column(u32), ColumnRange { start: u32, end: u32 }, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Selection { pub id: usize, pub start: T, From dacc8cb5f47ae8272afaf560979c7eb6d67e3354 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 27 Sep 2023 15:10:50 -0600 Subject: [PATCH 032/274] Begin to use pixels for column selection For zed-industries/community#759 For zed-industries/community#1966 Co-Authored-By: Julia --- crates/editor/src/display_map.rs | 391 +++++++++++++++++++++++-------- crates/editor/src/editor.rs | 51 +++- crates/editor/src/element.rs | 59 +---- crates/editor/src/movement.rs | 50 +++- crates/text/src/selection.rs | 2 + 5 files changed, 387 insertions(+), 166 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index d97db9695a..3d13447fc2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,22 +5,24 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer, - MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, EditorStyle, InlayId, + MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; use gpui::{ color::Color, - fonts::{FontId, HighlightStyle}, - Entity, ModelContext, ModelHandle, + fonts::{FontId, HighlightStyle, Underline}, + text_layout::{Line, RunStyle}, + AppContext, Entity, FontCache, ModelContext, ModelHandle, TextLayoutCache, }; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, }; -use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; +use lsp::DiagnosticSeverity; +use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; use wrap_map::WrapMap; @@ -316,6 +318,12 @@ pub struct Highlights<'a> { pub suggestion_highlight_style: Option, } +pub struct HighlightedChunk<'a> { + pub chunk: &'a str, + pub style: Option, + pub is_tab: bool, +} + pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, pub fold_snapshot: fold_map::FoldSnapshot, @@ -485,7 +493,7 @@ impl DisplaySnapshot { language_aware: bool, inlay_highlight_style: Option, suggestion_highlight_style: Option, - ) -> DisplayChunks<'_> { + ) -> DisplayChunks<'a> { self.block_snapshot.chunks( display_rows, language_aware, @@ -498,6 +506,174 @@ impl DisplaySnapshot { ) } + pub fn highlighted_chunks<'a>( + &'a self, + display_rows: Range, + style: &'a EditorStyle, + ) -> impl Iterator> { + self.chunks( + display_rows, + true, + Some(style.theme.hint), + Some(style.theme.suggestion), + ) + .map(|chunk| { + let mut highlight_style = chunk + .syntax_highlight_id + .and_then(|id| id.style(&style.syntax)); + + if let Some(chunk_highlight) = chunk.highlight_style { + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(chunk_highlight); + } else { + highlight_style = Some(chunk_highlight); + } + } + + let mut diagnostic_highlight = HighlightStyle::default(); + + if chunk.is_unnecessary { + diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); + } + + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } + } + + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(diagnostic_highlight); + } else { + highlight_style = Some(diagnostic_highlight); + } + + HighlightedChunk { + chunk: chunk.text, + style: highlight_style, + is_tab: chunk.is_tab, + } + }) + } + + fn layout_line_for_row( + &self, + display_row: u32, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + ) -> Line { + let mut styles = Vec::new(); + let mut line = String::new(); + + let range = display_row..display_row + 1; + for chunk in self.highlighted_chunks(range, editor_style) { + dbg!(chunk.chunk); + line.push_str(chunk.chunk); + + let text_style = if let Some(style) = chunk.style { + editor_style + .text + .clone() + .highlight(style, font_cache) + .map(Cow::Owned) + .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text)) + } else { + Cow::Borrowed(&editor_style.text) + }; + + styles.push(( + chunk.chunk.len(), + RunStyle { + font_id: text_style.font_id, + color: text_style.color, + underline: text_style.underline, + }, + )); + } + + dbg!(&line, &editor_style.text.font_size, &styles); + text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) + } + + pub fn x_for_point( + &self, + display_point: DisplayPoint, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + ) -> f32 { + let layout_line = self.layout_line_for_row( + display_point.row(), + font_cache, + text_layout_cache, + editor_style, + ); + layout_line.x_for_index(display_point.column() as usize) + } + + pub fn column_for_x( + &self, + display_row: u32, + x_coordinate: f32, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + ) -> Option { + let layout_line = + self.layout_line_for_row(display_row, font_cache, text_layout_cache, editor_style); + layout_line.index_for_x(x_coordinate).map(|c| c as u32) + } + + // column_for_x(row, x) + + fn point( + &self, + display_point: DisplayPoint, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, + cx: &AppContext, + ) -> f32 { + let mut styles = Vec::new(); + let mut line = String::new(); + + let range = display_point.row()..display_point.row() + 1; + for chunk in self.highlighted_chunks(range, editor_style) { + dbg!(chunk.chunk); + line.push_str(chunk.chunk); + + let text_style = if let Some(style) = chunk.style { + editor_style + .text + .clone() + .highlight(style, cx.font_cache()) + .map(Cow::Owned) + .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text)) + } else { + Cow::Borrowed(&editor_style.text) + }; + + styles.push(( + chunk.chunk.len(), + RunStyle { + font_id: text_style.font_id, + color: text_style.color, + underline: text_style.underline, + }, + )); + } + + dbg!(&line, &editor_style.text.font_size, &styles); + let layout_line = text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles); + layout_line.x_for_index(display_point.column() as usize) + } + pub fn chars_at( &self, mut point: DisplayPoint, @@ -869,17 +1045,21 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat #[cfg(test)] pub mod tests { use super::*; - use crate::{movement, test::marked_display_snapshot}; + use crate::{ + movement, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + }; use gpui::{color::Color, elements::*, test::observe, AppContext}; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, Buffer, Language, LanguageConfig, SelectionGoal, }; + use project::Project; use rand::{prelude::*, Rng}; use settings::SettingsStore; use smol::stream::StreamExt; use std::{env, sync::Arc}; - use theme::SyntaxTheme; + use theme::{SyntaxTheme, Theme}; use util::test::{marked_text_ranges, sample_text}; use Bias::*; @@ -1148,95 +1328,119 @@ pub mod tests { } #[gpui::test(retries = 5)] - fn test_soft_wraps(cx: &mut AppContext) { + async fn test_soft_wraps(cx: &mut gpui::TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - init_test(cx, |_| {}); - - let font_cache = cx.font_cache(); - - let family_id = font_cache - .load_family(&["Helvetica"], &Default::default()) - .unwrap(); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 12.0; - let wrap_width = Some(64.); - - let text = "one two three four five\nsix seven eight"; - let buffer = MultiBuffer::build_simple(text, cx); - let map = cx.add_model(|cx| { - DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) + cx.update(|cx| { + init_test(cx, |_| {}); }); - let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - snapshot.text_chunks(0).collect::(), - "one two \nthree four \nfive\nsix seven \neight" - ); - assert_eq!( - snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), - DisplayPoint::new(0, 7) - ); - assert_eq!( - snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), - DisplayPoint::new(1, 0) - ); - assert_eq!( - movement::right(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(1, 0) - ); - assert_eq!( - movement::left(&snapshot, DisplayPoint::new(1, 0)), - DisplayPoint::new(0, 7) - ); - assert_eq!( - movement::up( - &snapshot, - DisplayPoint::new(1, 10), - SelectionGoal::None, - false - ), - (DisplayPoint::new(0, 7), SelectionGoal::Column(10)) - ); - assert_eq!( - movement::down( - &snapshot, - DisplayPoint::new(0, 7), - SelectionGoal::Column(10), - false - ), - (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) - ); - assert_eq!( - movement::down( - &snapshot, - DisplayPoint::new(1, 10), - SelectionGoal::Column(10), - false - ), - (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) - ); + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); - let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); - buffer.update(cx, |buffer, cx| { - buffer.edit([(ix..ix, "and ")], None, cx); + cx.update_window(window, |cx| { + let editor_style = editor.read(&cx).style(cx); + + let font_cache = cx.font_cache().clone(); + + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 12.0; + let wrap_width = Some(64.); + + let text = "one two three four five\nsix seven eight"; + let buffer = MultiBuffer::build_simple(text, cx); + let map = cx.add_model(|cx| { + DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx) + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(0).collect::(), + "one two \nthree four \nfive\nsix seven \neight" + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), + DisplayPoint::new(0, 7) + ); + assert_eq!( + snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::right(&snapshot, DisplayPoint::new(0, 7)), + DisplayPoint::new(1, 0) + ); + assert_eq!( + movement::left(&snapshot, DisplayPoint::new(1, 0)), + DisplayPoint::new(0, 7) + ); + + let x = snapshot.x_for_point( + DisplayPoint::new(1, 10), + cx.font_cache(), + cx.text_layout_cache(), + &editor_style, + ); + dbg!(x); + assert_eq!( + movement::up( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::None, + false, + cx.font_cache(), + cx.text_layout_cache(), + &editor_style, + ), + ( + DisplayPoint::new(0, 7), + SelectionGoal::HorizontalPosition(x) + ) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(0, 7), + SelectionGoal::Column(10), + false + ), + (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) + ); + assert_eq!( + movement::down( + &snapshot, + DisplayPoint::new(1, 10), + SelectionGoal::Column(10), + false + ), + (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) + ); + + let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(ix..ix, "and ")], None, cx); + }); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three four \nfive\nsix and \nseven eight" + ); + + // Re-wrap on font size changes + map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx)); + + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!( + snapshot.text_chunks(1).collect::(), + "three \nfour five\nsix and \nseven \neight" + ) }); - - let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - snapshot.text_chunks(1).collect::(), - "three four \nfive\nsix and \nseven eight" - ); - - // Re-wrap on font size changes - map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx)); - - let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - snapshot.text_chunks(1).collect::(), - "three \nfour five\nsix and \nseven \neight" - ) } #[gpui::test] @@ -1731,6 +1935,9 @@ pub mod tests { cx.foreground().forbid_parking(); cx.set_global(SettingsStore::test(cx)); language::init(cx); + crate::init(cx); + Project::init_settings(cx); + theme::init((), cx); cx.update_global::(|store, cx| { store.update_user_settings::(cx, f); }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 24ffa64a6a..bf1aa2e6b5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, - Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + serde_json, text_layout, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, + Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, + WeakViewHandle, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -5274,13 +5274,25 @@ impl Editor { return; } + let font_cache = cx.font_cache().clone(); + let text_layout_cache = cx.text_layout_cache().clone(); + let editor_style = self.style(cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = movement::up(map, selection.start, selection.goal, false); + let (cursor, goal) = movement::up( + map, + selection.start, + selection.goal, + false, + &font_cache, + &text_layout_cache, + &editor_style, + ); selection.collapse_to(cursor, goal); }); }) @@ -5308,22 +5320,47 @@ impl Editor { Autoscroll::fit() }; + let font_cache = cx.font_cache().clone(); + let text_layout = cx.text_layout_cache().clone(); + let editor_style = self.style(cx); + self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = - movement::up_by_rows(map, selection.end, row_count, selection.goal, false); + let (cursor, goal) = movement::up_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + &font_cache, + &text_layout, + &editor_style, + ); selection.collapse_to(cursor, goal); }); }); } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { + let font_cache = cx.font_cache().clone(); + let text_layout = cx.text_layout_cache().clone(); + let editor_style = self.style(cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false)) + s.move_heads_with(|map, head, goal| { + movement::up( + map, + head, + goal, + false, + &font_cache, + &text_layout, + &editor_style, + ) + }) }) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 924d66c21c..24cbadfd37 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -1584,56 +1584,7 @@ impl EditorElement { .collect() } else { let style = &self.style; - let chunks = snapshot - .chunks( - rows.clone(), - true, - Some(style.theme.hint), - Some(style.theme.suggestion), - ) - .map(|chunk| { - let mut highlight_style = chunk - .syntax_highlight_id - .and_then(|id| id.style(&style.syntax)); - - if let Some(chunk_highlight) = chunk.highlight_style { - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(chunk_highlight); - } else { - highlight_style = Some(chunk_highlight); - } - } - - let mut diagnostic_highlight = HighlightStyle::default(); - - if chunk.is_unnecessary { - diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } - - if let Some(severity) = chunk.diagnostic_severity { - // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. - if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { - let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - squiggly: true, - }); - } - } - - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(diagnostic_highlight); - } else { - highlight_style = Some(diagnostic_highlight); - } - - HighlightedChunk { - chunk: chunk.text, - style: highlight_style, - is_tab: chunk.is_tab, - } - }); + let chunks = snapshot.highlighted_chunks(rows.clone(), style); LineWithInvisibles::from_chunks( chunks, @@ -1870,12 +1821,6 @@ impl EditorElement { } } -struct HighlightedChunk<'a> { - chunk: &'a str, - style: Option, - is_tab: bool, -} - #[derive(Debug)] pub struct LineWithInvisibles { pub line: Line, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 974af4bc24..3403790681 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,5 +1,6 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, ToOffset, ToPoint}; +use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; +use gpui::{FontCache, TextLayoutCache, WindowContext}; use language::Point; use std::ops::Range; @@ -47,8 +48,20 @@ pub fn up( start: DisplayPoint, goal: SelectionGoal, preserve_column_at_start: bool, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, ) -> (DisplayPoint, SelectionGoal) { - up_by_rows(map, start, 1, goal, preserve_column_at_start) + up_by_rows( + map, + start, + 1, + goal, + preserve_column_at_start, + font_cache, + text_layout_cache, + editor_style, + ) } pub fn down( @@ -66,11 +79,14 @@ pub fn up_by_rows( row_count: u32, goal: SelectionGoal, preserve_column_at_start: bool, + font_cache: &FontCache, + text_layout_cache: &TextLayoutCache, + editor_style: &EditorStyle, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = match goal { - SelectionGoal::Column(column) => column, - SelectionGoal::ColumnRange { end, .. } => end, - _ => map.column_to_chars(start.row(), start.column()), + let mut goal_x = match goal { + SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::HorizontalRange { end, .. } => end, + _ => map.x_for_point(start, font_cache, text_layout_cache, editor_style), }; let prev_row = start.row().saturating_sub(row_count); @@ -79,19 +95,27 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map.column_from_chars(point.row(), goal_column); + *point.column_mut() = map + .column_for_x( + point.row(), + goal_x, + font_cache, + text_layout_cache, + editor_style, + ) + .unwrap_or(point.column()); } else if preserve_column_at_start { return (start, goal); } else { point = DisplayPoint::new(0, 0); - goal_column = 0; + goal_x = 0.0; } let mut clipped_point = map.clip_point(point, Bias::Left); if clipped_point.row() < point.row() { clipped_point = map.clip_point(point, Bias::Right); } - (clipped_point, SelectionGoal::Column(goal_column)) + (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) } pub fn down_by_rows( @@ -692,6 +716,7 @@ mod tests { #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { + /* init_test(cx); let family_id = cx @@ -727,6 +752,7 @@ mod tests { cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); // Can't move up into the first excerpt's header @@ -737,7 +763,10 @@ mod tests { SelectionGoal::Column(2), false ), - (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), ); assert_eq!( up( @@ -808,6 +837,7 @@ mod tests { ), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); + */ } fn init_test(cx: &mut gpui::AppContext) { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 60d5e2f1c4..38831f92c2 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -5,6 +5,8 @@ use std::ops::Range; #[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, + HorizontalPosition(f32), + HorizontalRange { start: f32, end: f32 }, Column(u32), ColumnRange { start: u32, end: u32 }, } From e7badb38e96ffedbf9c24780d671b2d6c8cc7cdd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Sep 2023 20:35:06 -0600 Subject: [PATCH 033/274] Refactor to pass a TextLayoutDetails around --- crates/editor/src/display_map.rs | 44 +++++++++++------------------- crates/editor/src/editor.rs | 31 +++++---------------- crates/editor/src/movement.rs | 46 ++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 3d13447fc2..45b4c0abed 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,8 +5,8 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, EditorStyle, InlayId, - MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, + EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; @@ -565,9 +565,11 @@ impl DisplaySnapshot { fn layout_line_for_row( &self, display_row: u32, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + TextLayoutDetails { + font_cache, + text_layout_cache, + editor_style, + }: &TextLayoutDetails, ) -> Line { let mut styles = Vec::new(); let mut line = String::new(); @@ -605,16 +607,9 @@ impl DisplaySnapshot { pub fn x_for_point( &self, display_point: DisplayPoint, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> f32 { - let layout_line = self.layout_line_for_row( - display_point.row(), - font_cache, - text_layout_cache, - editor_style, - ); + let layout_line = self.layout_line_for_row(display_point.row(), text_layout_details); layout_line.x_for_index(display_point.column() as usize) } @@ -622,12 +617,9 @@ impl DisplaySnapshot { &self, display_row: u32, x_coordinate: f32, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> Option { - let layout_line = - self.layout_line_for_row(display_row, font_cache, text_layout_cache, editor_style); + let layout_line = self.layout_line_for_row(display_row, text_layout_details); layout_line.index_for_x(x_coordinate).map(|c| c as u32) } @@ -1339,7 +1331,8 @@ pub mod tests { let window = cx.window.clone(); cx.update_window(window, |cx| { - let editor_style = editor.read(&cx).style(cx); + let text_layout_details = + editor.read_with(cx, |editor, cx| TextLayoutDetails::new(editor, cx)); let font_cache = cx.font_cache().clone(); @@ -1380,12 +1373,7 @@ pub mod tests { DisplayPoint::new(0, 7) ); - let x = snapshot.x_for_point( - DisplayPoint::new(1, 10), - cx.font_cache(), - cx.text_layout_cache(), - &editor_style, - ); + let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details); dbg!(x); assert_eq!( movement::up( @@ -1393,9 +1381,7 @@ pub mod tests { DisplayPoint::new(1, 10), SelectionGoal::None, false, - cx.font_cache(), - cx.text_layout_cache(), - &editor_style, + &text_layout_details, ), ( DisplayPoint::new(0, 7), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bf1aa2e6b5..081d33c8a0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -71,6 +71,7 @@ use link_go_to_definition::{ }; use log::error; use lsp::LanguageServerId; +use movement::TextLayoutDetails; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, @@ -5274,9 +5275,7 @@ impl Editor { return; } - let font_cache = cx.font_cache().clone(); - let text_layout_cache = cx.text_layout_cache().clone(); - let editor_style = self.style(cx); + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; @@ -5289,9 +5288,7 @@ impl Editor { selection.start, selection.goal, false, - &font_cache, - &text_layout_cache, - &editor_style, + &text_layout_details, ); selection.collapse_to(cursor, goal); }); @@ -5320,9 +5317,7 @@ impl Editor { Autoscroll::fit() }; - let font_cache = cx.font_cache().clone(); - let text_layout = cx.text_layout_cache().clone(); - let editor_style = self.style(cx); + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; @@ -5336,9 +5331,7 @@ impl Editor { row_count, selection.goal, false, - &font_cache, - &text_layout, - &editor_style, + &text_layout_details, ); selection.collapse_to(cursor, goal); }); @@ -5346,20 +5339,10 @@ impl Editor { } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let font_cache = cx.font_cache().clone(); - let text_layout = cx.text_layout_cache().clone(); - let editor_style = self.style(cx); + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, goal| { - movement::up( - map, - head, - goal, - false, - &font_cache, - &text_layout, - &editor_style, - ) + movement::up(map, head, goal, false, &text_layout_details) }) }) } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 3403790681..836d5dda2f 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,8 +1,8 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; -use gpui::{FontCache, TextLayoutCache, WindowContext}; +use crate::{char_kind, CharKind, Editor, EditorStyle, ToOffset, ToPoint}; +use gpui::{text_layout, FontCache, TextLayoutCache, WindowContext}; use language::Point; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; #[derive(Debug, PartialEq)] pub enum FindRange { @@ -10,6 +10,24 @@ pub enum FindRange { MultiLine, } +/// TextLayoutDetails encompasses everything we need to move vertically +/// taking into account variable width characters. +pub struct TextLayoutDetails { + pub font_cache: Arc, + pub text_layout_cache: Arc, + pub editor_style: EditorStyle, +} + +impl TextLayoutDetails { + pub fn new(editor: &Editor, cx: &WindowContext) -> TextLayoutDetails { + TextLayoutDetails { + font_cache: cx.font_cache().clone(), + text_layout_cache: cx.text_layout_cache().clone(), + editor_style: editor.style(cx), + } + } +} + pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -48,9 +66,7 @@ pub fn up( start: DisplayPoint, goal: SelectionGoal, preserve_column_at_start: bool, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { up_by_rows( map, @@ -58,9 +74,7 @@ pub fn up( 1, goal, preserve_column_at_start, - font_cache, - text_layout_cache, - editor_style, + text_layout_details, ) } @@ -79,14 +93,12 @@ pub fn up_by_rows( row_count: u32, goal: SelectionGoal, preserve_column_at_start: bool, - font_cache: &FontCache, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x, SelectionGoal::HorizontalRange { end, .. } => end, - _ => map.x_for_point(start, font_cache, text_layout_cache, editor_style), + _ => map.x_for_point(start, text_layout_details), }; let prev_row = start.row().saturating_sub(row_count); @@ -96,13 +108,7 @@ pub fn up_by_rows( ); if point.row() < start.row() { *point.column_mut() = map - .column_for_x( - point.row(), - goal_x, - font_cache, - text_layout_cache, - editor_style, - ) + .column_for_x(point.row(), goal_x, text_layout_details) .unwrap_or(point.column()); } else if preserve_column_at_start { return (start, goal); From ef7e2c5d86208a8ecf738ae16c355188fa5d357a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Sep 2023 21:39:24 -0600 Subject: [PATCH 034/274] Get the project running! --- crates/editor/src/display_map.rs | 21 +- crates/editor/src/editor.rs | 37 +++- crates/editor/src/editor_tests.rs | 1 + crates/editor/src/movement.rs | 311 +++++++++++++++++----------- crates/vim/src/motion.rs | 32 ++- crates/vim/src/normal.rs | 34 +-- crates/vim/src/normal/change.rs | 31 ++- crates/vim/src/normal/delete.rs | 7 +- crates/vim/src/normal/paste.rs | 17 +- crates/vim/src/normal/substitute.rs | 26 ++- crates/vim/src/normal/yank.rs | 4 +- crates/vim/src/visual.rs | 15 +- 12 files changed, 353 insertions(+), 183 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 45b4c0abed..cebafbd651 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1392,19 +1392,28 @@ pub mod tests { movement::down( &snapshot, DisplayPoint::new(0, 7), - SelectionGoal::Column(10), - false + SelectionGoal::HorizontalPosition(x), + false, + &text_layout_details ), - (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) + ( + DisplayPoint::new(1, 10), + SelectionGoal::HorizontalPosition(x) + ) ); + dbg!("starting down..."); assert_eq!( movement::down( &snapshot, DisplayPoint::new(1, 10), - SelectionGoal::Column(10), - false + SelectionGoal::HorizontalPosition(x), + false, + &text_layout_details ), - (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) + ( + DisplayPoint::new(2, 4), + SelectionGoal::HorizontalPosition(x) + ) ); let ix = snapshot.buffer_snapshot.text().find("seven").unwrap(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 081d33c8a0..e68b1f008f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4988,6 +4988,7 @@ impl Editor { } pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext) { + let text_layout_details = TextLayoutDetails::new(&self, cx); self.transact(cx, |this, cx| { let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| { let mut edits: Vec<(Range, String)> = Default::default(); @@ -5011,7 +5012,10 @@ impl Editor { *head.column_mut() += 1; head = display_map.clip_point(head, Bias::Right); - selection.collapse_to(head, SelectionGoal::Column(head.column())); + let goal = SelectionGoal::HorizontalPosition( + display_map.x_for_point(head, &text_layout_details), + ); + selection.collapse_to(head, goal); let transpose_start = display_map .buffer_snapshot @@ -5355,13 +5359,20 @@ impl Editor { return; } + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = movement::down(map, selection.end, selection.goal, false); + let (cursor, goal) = movement::down( + map, + selection.end, + selection.goal, + false, + &text_layout_details, + ); selection.collapse_to(cursor, goal); }); }); @@ -5398,22 +5409,32 @@ impl Editor { Autoscroll::fit() }; + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(autoscroll), cx, |s| { let line_mode = s.line_mode; s.move_with(|map, selection| { if !selection.is_empty() && !line_mode { selection.goal = SelectionGoal::None; } - let (cursor, goal) = - movement::down_by_rows(map, selection.end, row_count, selection.goal, false); + let (cursor, goal) = movement::down_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + &text_layout_details, + ); selection.collapse_to(cursor, goal); }); }); } pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { + let text_layout_details = TextLayoutDetails::new(&self, cx); self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false)) + s.move_heads_with(|map, head, goal| { + movement::down(map, head, goal, false, &text_layout_details) + }) }); } @@ -6286,6 +6307,7 @@ impl Editor { } pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { + let text_layout_details = TextLayoutDetails::new(&self, cx); self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); let mut edits = Vec::new(); @@ -6528,7 +6550,10 @@ impl Editor { point.row += 1; point = snapshot.clip_point(point, Bias::Left); let display_point = point.to_display_point(display_snapshot); - (display_point, SelectionGoal::Column(display_point.column())) + let goal = SelectionGoal::HorizontalPosition( + display_snapshot.x_for_point(display_point, &text_layout_details), + ); + (display_point, goal) }) }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dee27e0121..affe9f60a2 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -847,6 +847,7 @@ fn test_move_cursor(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut TestAppContext) { + todo!(); init_test(cx, |_| {}); let view = cx diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 836d5dda2f..e2306a1b2d 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -83,8 +83,16 @@ pub fn down( start: DisplayPoint, goal: SelectionGoal, preserve_column_at_end: bool, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - down_by_rows(map, start, 1, goal, preserve_column_at_end) + down_by_rows( + map, + start, + 1, + goal, + preserve_column_at_end, + text_layout_details, + ) } pub fn up_by_rows( @@ -130,29 +138,32 @@ pub fn down_by_rows( row_count: u32, goal: SelectionGoal, preserve_column_at_end: bool, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = match goal { - SelectionGoal::Column(column) => column, - SelectionGoal::ColumnRange { end, .. } => end, - _ => map.column_to_chars(start.row(), start.column()), + let mut goal_x = match goal { + SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::HorizontalRange { end, .. } => end, + _ => map.x_for_point(start, text_layout_details), }; let new_row = start.row() + row_count; let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { - *point.column_mut() = map.column_from_chars(point.row(), goal_column); + *point.column_mut() = map + .column_for_x(point.row(), goal_x, text_layout_details) + .unwrap_or(map.line_len(point.row())); } else if preserve_column_at_end { return (start, goal); } else { point = map.max_point(); - goal_column = map.column_to_chars(point.row(), point.column()) + goal_x = map.x_for_point(point, text_layout_details) } let mut clipped_point = map.clip_point(point, Bias::Right); if clipped_point.row() > point.row() { clipped_point = map.clip_point(point, Bias::Left); } - (clipped_point, SelectionGoal::Column(goal_column)) + (clipped_point, SelectionGoal::HorizontalPosition(goal_x)) } pub fn line_beginning( @@ -426,9 +437,12 @@ pub fn split_display_range_by_lines( mod tests { use super::*; use crate::{ - display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, - InlayId, MultiBuffer, + display_map::Inlay, + test::{editor_test_context::EditorTestContext, marked_display_snapshot}, + Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, }; + use language::language_settings::AllLanguageSettings; + use project::Project; use settings::SettingsStore; use util::post_inc; @@ -721,129 +735,173 @@ mod tests { } #[gpui::test] - fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { - /* - init_test(cx); - - let family_id = cx - .font_cache() - .load_family(&["Helvetica"], &Default::default()) - .unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); - - let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(2, 0)..Point::new(3, 2), - primary: None, - }, - ], - cx, - ); - multibuffer + async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + init_test(cx); }); - let display_map = - cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut cx = EditorTestContext::new(cx).await; + let editor = cx.editor.clone(); + let window = cx.window.clone(); + cx.update_window(window, |cx| { + let text_layout_details = + editor.read_with(cx, |editor, cx| TextLayoutDetails::new(editor, cx)); - assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); + let family_id = cx + .font_cache() + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); - // Can't move up into the first excerpt's header - assert_eq!( - up( - &snapshot, - DisplayPoint::new(2, 2), - SelectionGoal::Column(2), - false - ), - ( - DisplayPoint::new(2, 0), - SelectionGoal::HorizontalPosition(0.0) - ), - ); - assert_eq!( - up( - &snapshot, - DisplayPoint::new(2, 0), - SelectionGoal::None, - false - ), - (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), - ); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 4), + primary: None, + }, + ExcerptRange { + context: Point::new(2, 0)..Point::new(3, 2), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + let display_map = + cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - // Move up and down within first excerpt - assert_eq!( - up( - &snapshot, - DisplayPoint::new(3, 4), - SelectionGoal::Column(4), - false - ), - (DisplayPoint::new(2, 3), SelectionGoal::Column(4)), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(2, 3), - SelectionGoal::Column(4), - false - ), - (DisplayPoint::new(3, 4), SelectionGoal::Column(4)), - ); + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); - // Move up and down across second excerpt's header - assert_eq!( - up( - &snapshot, - DisplayPoint::new(6, 5), - SelectionGoal::Column(5), - false - ), - (DisplayPoint::new(3, 4), SelectionGoal::Column(5)), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(3, 4), - SelectionGoal::Column(5), - false - ), - (DisplayPoint::new(6, 5), SelectionGoal::Column(5)), - ); + let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details); - // Can't move down off the end - assert_eq!( - down( - &snapshot, - DisplayPoint::new(7, 0), - SelectionGoal::Column(0), - false - ), - (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), - ); - assert_eq!( - down( - &snapshot, - DisplayPoint::new(7, 2), - SelectionGoal::Column(2), - false - ), - (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), - ); - */ + // Can't move up into the first excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 2), + SelectionGoal::HorizontalPosition(col_2_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + assert_eq!( + up( + &snapshot, + DisplayPoint::new(2, 0), + SelectionGoal::None, + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 0), + SelectionGoal::HorizontalPosition(0.0) + ), + ); + + let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details); + + // Move up and down within first excerpt + assert_eq!( + up( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(2, 3), + SelectionGoal::HorizontalPosition(col_4_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_4_x) + ), + ); + + let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details); + + // Move up and down across second excerpt's header + assert_eq!( + up( + &snapshot, + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(3, 4), + SelectionGoal::HorizontalPosition(col_5_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(6, 5), + SelectionGoal::HorizontalPosition(col_5_x) + ), + ); + + let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details); + + // Can't move down off the end + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 0), + SelectionGoal::HorizontalPosition(0.0), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x) + ), + ); + assert_eq!( + down( + &snapshot, + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x), + false, + &text_layout_details + ), + ( + DisplayPoint::new(7, 2), + SelectionGoal::HorizontalPosition(max_point_x) + ), + ); + }); } fn init_test(cx: &mut gpui::AppContext) { @@ -851,5 +909,6 @@ mod tests { theme::init((), cx); language::init(cx); crate::init(cx); + Project::init_settings(cx); } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a197121626..2f1e376c3e 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3,7 +3,7 @@ use std::cmp; use editor::{ char_kind, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, - movement::{self, find_boundary, find_preceding_boundary, FindRange}, + movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails}, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -361,6 +361,7 @@ impl Motion { point: DisplayPoint, goal: SelectionGoal, maybe_times: Option, + text_layout_details: &TextLayoutDetails, ) -> Option<(DisplayPoint, SelectionGoal)> { let times = maybe_times.unwrap_or(1); use Motion::*; @@ -373,13 +374,13 @@ impl Motion { } => down(map, point, goal, times), Down { display_lines: true, - } => down_display(map, point, goal, times), + } => down_display(map, point, goal, times, &text_layout_details), Up { display_lines: false, } => up(map, point, goal, times), Up { display_lines: true, - } => up_display(map, point, goal, times), + } => up_display(map, point, goal, times, &text_layout_details), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( next_word_start(map, point, *ignore_punctuation, times), @@ -442,10 +443,15 @@ impl Motion { selection: &mut Selection, times: Option, expand_to_surrounding_newline: bool, + text_layout_details: &TextLayoutDetails, ) -> bool { - if let Some((new_head, goal)) = - self.move_point(map, selection.head(), selection.goal, times) - { + if let Some((new_head, goal)) = self.move_point( + map, + selection.head(), + selection.goal, + times, + &text_layout_details, + ) { selection.set_head(new_head, goal); if self.linewise() { @@ -566,9 +572,10 @@ fn down_display( mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { for _ in 0..times { - (point, goal) = movement::down(map, point, goal, true); + (point, goal) = movement::down(map, point, goal, true, text_layout_details); } (point, goal) @@ -606,9 +613,10 @@ fn up_display( mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { for _ in 0..times { - (point, goal) = movement::up(map, point, goal, true); + (point, goal) = movement::up(map, point, goal, true, &text_layout_details); } (point, goal) @@ -707,7 +715,7 @@ fn previous_word_start( point } -fn first_non_whitespace( +pub(crate) fn first_non_whitespace( map: &DisplaySnapshot, display_lines: bool, from: DisplayPoint, @@ -890,7 +898,11 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> first_non_whitespace(map, false, correct_line) } -fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { +pub(crate) fn next_line_end( + map: &DisplaySnapshot, + mut point: DisplayPoint, + times: usize, +) -> DisplayPoint { if times > 1 { point = down(map, point, SelectionGoal::None, times - 1).0; } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 36eab2c4c0..9c93f19fc7 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -12,13 +12,13 @@ mod yank; use std::sync::Arc; use crate::{ - motion::{self, Motion}, + motion::{self, first_non_whitespace, next_line_end, right, Motion}, object::Object, state::{Mode, Operator}, Vim, }; use collections::HashSet; -use editor::scroll::autoscroll::Autoscroll; +use editor::{movement::TextLayoutDetails, scroll::autoscroll::Autoscroll}; use editor::{Bias, DisplayPoint}; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::SelectionGoal; @@ -177,10 +177,11 @@ pub(crate) fn move_cursor( cx: &mut WindowContext, ) { vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { motion - .move_point(map, cursor, goal, times) + .move_point(map, cursor, goal, times, &text_layout_details) .unwrap_or((cursor, goal)) }) }) @@ -193,8 +194,8 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext, cx: &m | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now editor.set_clip_at_line_ends(false, cx); @@ -27,9 +28,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m s.move_with(|map, selection| { motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion { - expand_changed_word_selection(map, selection, times, ignore_punctuation) + expand_changed_word_selection( + map, + selection, + times, + ignore_punctuation, + &text_layout_details, + ) } else { - motion.expand_selection(map, selection, times, false) + motion.expand_selection(map, selection, times, false, &text_layout_details) }; }); }); @@ -81,6 +88,7 @@ fn expand_changed_word_selection( selection: &mut Selection, times: Option, ignore_punctuation: bool, + text_layout_details: &TextLayoutDetails, ) -> bool { if times.is_none() || times.unwrap() == 1 { let scope = map @@ -103,11 +111,22 @@ fn expand_changed_word_selection( }); true } else { - Motion::NextWordStart { ignore_punctuation } - .expand_selection(map, selection, None, false) + Motion::NextWordStart { ignore_punctuation }.expand_selection( + map, + selection, + None, + false, + &text_layout_details, + ) } } else { - Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false) + Motion::NextWordStart { ignore_punctuation }.expand_selection( + map, + selection, + times, + false, + &text_layout_details, + ) } } diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 517ece8033..1ad91ff308 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,12 +1,15 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; +use editor::{ + display_map::ToDisplayPoint, movement::TextLayoutDetails, scroll::autoscroll::Autoscroll, Bias, +}; use gpui::WindowContext; use language::Point; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.stop_recording(); vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); @@ -14,7 +17,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m s.move_with(|map, selection| { let original_head = selection.head(); original_columns.insert(selection.id, original_head.column()); - motion.expand_selection(map, selection, times, true); + motion.expand_selection(map, selection, times, true, &text_layout_details); // Motion::NextWordStart on an empty line should delete it. if let Motion::NextWordStart { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index dda8dea1e4..7cb5261c49 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,8 +1,10 @@ use std::{borrow::Cow, cmp}; use editor::{ - display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection, - DisplayPoint, + display_map::ToDisplayPoint, + movement::{self, TextLayoutDetails}, + scroll::autoscroll::Autoscroll, + ClipboardSelection, DisplayPoint, }; use gpui::{impl_actions, AppContext, ViewContext}; use language::{Bias, SelectionGoal}; @@ -30,6 +32,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); @@ -168,8 +171,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { let mut cursor = anchor.to_display_point(map); if *line_mode { if !before { - cursor = - movement::down(map, cursor, SelectionGoal::None, false).0; + cursor = movement::down( + map, + cursor, + SelectionGoal::None, + false, + &text_layout_details, + ) + .0; } cursor = movement::indented_line_beginning(map, cursor, true); } else if !is_multiline { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index bb6e1abf92..ddc937d03f 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,9 +1,13 @@ -use editor::movement; +use editor::movement::{self, TextLayoutDetails}; use gpui::{actions, AppContext, WindowContext}; use language::Point; use workspace::Workspace; -use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; +use crate::{ + motion::{right, Motion}, + utils::copy_selections_content, + Mode, Vim, +}; actions!(vim, [Substitute, SubstituteLine]); @@ -32,10 +36,17 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut vim.update_active_editor(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if selection.start == selection.end { - Motion::Right.expand_selection(map, selection, count, true); + Motion::Right.expand_selection( + map, + selection, + count, + true, + &text_layout_details, + ); } if line_mode { // in Visual mode when the selection contains the newline at the end @@ -43,7 +54,13 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut if !selection.is_empty() && selection.end.column() == 0 { selection.end = movement::left(map, selection.end); } - Motion::CurrentLine.expand_selection(map, selection, None, false); + Motion::CurrentLine.expand_selection( + map, + selection, + None, + false, + &text_layout_details, + ); if let Some((point, _)) = (Motion::FirstNonWhitespace { display_lines: false, }) @@ -52,6 +69,7 @@ pub fn substitute(vim: &mut Vim, count: Option, line_mode: bool, cx: &mut selection.start, selection.goal, None, + &text_layout_details, ) { selection.start = point; } diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 7212a865bd..b50fcdf7ec 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,9 +1,11 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::HashMap; +use editor::movement::TextLayoutDetails; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); @@ -11,7 +13,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut s.move_with(|map, selection| { let original_position = (selection.head(), selection.goal); original_positions.insert(selection.id, original_position); - motion.expand_selection(map, selection, times, true); + motion.expand_selection(map, selection, times, true, &text_layout_details); }); }); copy_selections_content(editor, motion.linewise(), cx); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index eac823de61..bec91007e3 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -4,7 +4,7 @@ use std::{cmp, sync::Arc}; use collections::HashMap; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, + movement::{self, TextLayoutDetails}, scroll::autoscroll::Autoscroll, Bias, DisplayPoint, Editor, }; @@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { + let text_layout_details = TextLayoutDetails::new(editor, cx); if vim.state().mode == Mode::VisualBlock && !matches!( motion, @@ -67,7 +68,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex { let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { - motion.move_point(map, point, goal, times) + motion.move_point(map, point, goal, times, &text_layout_details) }) } else { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { @@ -89,9 +90,13 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex current_head = movement::left(map, selection.end) } - let Some((new_head, goal)) = - motion.move_point(map, current_head, selection.goal, times) - else { + let Some((new_head, goal)) = motion.move_point( + map, + current_head, + selection.goal, + times, + &text_layout_details, + ) else { return; }; From 002e2cc42c41f4fcb847061ce0b25721fa1895d5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 28 Sep 2023 22:40:27 -0600 Subject: [PATCH 035/274] Round better for up/down --- crates/editor/src/display_map.rs | 4 ++-- crates/editor/src/movement.rs | 8 ++------ crates/gpui/src/text_layout.rs | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index cebafbd651..424ff1518a 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -618,9 +618,9 @@ impl DisplaySnapshot { display_row: u32, x_coordinate: f32, text_layout_details: &TextLayoutDetails, - ) -> Option { + ) -> u32 { let layout_line = self.layout_line_for_row(display_row, text_layout_details); - layout_line.index_for_x(x_coordinate).map(|c| c as u32) + layout_line.closest_index_for_x(x_coordinate) as u32 } // column_for_x(row, x) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index e2306a1b2d..38cf5cd6c1 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -115,9 +115,7 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map - .column_for_x(point.row(), goal_x, text_layout_details) - .unwrap_or(point.column()); + *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_start { return (start, goal); } else { @@ -149,9 +147,7 @@ pub fn down_by_rows( let new_row = start.row() + row_count; let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { - *point.column_mut() = map - .column_for_x(point.row(), goal_x, text_layout_details) - .unwrap_or(map.line_len(point.row())); + *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_end { return (start, goal); } else { diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 97f4b7a12d..7fb87b10df 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -266,6 +266,8 @@ impl Line { self.layout.len == 0 } + /// index_for_x returns the character containing the given x coordinate. + /// (e.g. to handle a mouse-click) pub fn index_for_x(&self, x: f32) -> Option { if x >= self.layout.width { None @@ -281,6 +283,28 @@ impl Line { } } + /// closest_index_for_x returns the character boundary closest to the given x coordinate + /// (e.g. to handle aligning up/down arrow keys) + pub fn closest_index_for_x(&self, x: f32) -> usize { + let mut prev_index = 0; + let mut prev_x = 0.0; + + for run in self.layout.runs.iter() { + for glyph in run.glyphs.iter() { + if glyph.position.x() >= x { + if glyph.position.x() - x < x - prev_x { + return glyph.index; + } else { + return prev_index; + } + } + prev_index = glyph.index; + prev_x = glyph.position.x(); + } + } + prev_index + } + pub fn paint( &self, origin: Vector2F, From ab050d18901e47d41fdcd11da3e950688ae2e665 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Oct 2023 19:10:01 -0600 Subject: [PATCH 036/274] Use Horizontal ranges everywhere --- crates/editor/src/display_map.rs | 49 +-------- crates/editor/src/editor.rs | 28 +++-- crates/editor/src/element.rs | 5 +- crates/editor/src/movement.rs | 6 +- crates/editor/src/selections_collection.rs | 23 ++-- crates/text/src/selection.rs | 4 +- crates/vim/src/motion.rs | 120 ++++++++++++--------- crates/vim/src/normal.rs | 32 ++++-- crates/vim/src/normal/substitute.rs | 6 +- crates/vim/src/test.rs | 56 ++++++++++ crates/vim/src/vim.rs | 2 +- crates/vim/src/visual.rs | 44 ++++++-- crates/vim/test_data/test_j.json | 3 + 13 files changed, 229 insertions(+), 149 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 424ff1518a..0f2b5665c6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -15,7 +15,7 @@ use gpui::{ color::Color, fonts::{FontId, HighlightStyle, Underline}, text_layout::{Line, RunStyle}, - AppContext, Entity, FontCache, ModelContext, ModelHandle, TextLayoutCache, + Entity, ModelContext, ModelHandle, }; use inlay_map::InlayMap; use language::{ @@ -576,7 +576,6 @@ impl DisplaySnapshot { let range = display_row..display_row + 1; for chunk in self.highlighted_chunks(range, editor_style) { - dbg!(chunk.chunk); line.push_str(chunk.chunk); let text_style = if let Some(style) = chunk.style { @@ -600,7 +599,6 @@ impl DisplaySnapshot { )); } - dbg!(&line, &editor_style.text.font_size, &styles); text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles) } @@ -623,49 +621,6 @@ impl DisplaySnapshot { layout_line.closest_index_for_x(x_coordinate) as u32 } - // column_for_x(row, x) - - fn point( - &self, - display_point: DisplayPoint, - text_layout_cache: &TextLayoutCache, - editor_style: &EditorStyle, - cx: &AppContext, - ) -> f32 { - let mut styles = Vec::new(); - let mut line = String::new(); - - let range = display_point.row()..display_point.row() + 1; - for chunk in self.highlighted_chunks(range, editor_style) { - dbg!(chunk.chunk); - line.push_str(chunk.chunk); - - let text_style = if let Some(style) = chunk.style { - editor_style - .text - .clone() - .highlight(style, cx.font_cache()) - .map(Cow::Owned) - .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text)) - } else { - Cow::Borrowed(&editor_style.text) - }; - - styles.push(( - chunk.chunk.len(), - RunStyle { - font_id: text_style.font_id, - color: text_style.color, - underline: text_style.underline, - }, - )); - } - - dbg!(&line, &editor_style.text.font_size, &styles); - let layout_line = text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles); - layout_line.x_for_index(display_point.column() as usize) - } - pub fn chars_at( &self, mut point: DisplayPoint, @@ -1374,7 +1329,6 @@ pub mod tests { ); let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details); - dbg!(x); assert_eq!( movement::up( &snapshot, @@ -1401,7 +1355,6 @@ pub mod tests { SelectionGoal::HorizontalPosition(x) ) ); - dbg!("starting down..."); assert_eq!( movement::down( &snapshot, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e68b1f008f..88db2f1dfe 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json, text_layout, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, - Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, - WeakViewHandle, WindowContext, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, + Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -5953,11 +5953,14 @@ impl Editor { fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); + let text_layout_details = TextLayoutDetails::new(self, cx); let mut state = self.add_selections_state.take().unwrap_or_else(|| { let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); let range = oldest_selection.display_range(&display_map).sorted(); - let columns = cmp::min(range.start.column(), range.end.column()) - ..cmp::max(range.start.column(), range.end.column()); + + let start_x = display_map.x_for_point(range.start, &text_layout_details); + let end_x = display_map.x_for_point(range.end, &text_layout_details); + let positions = start_x.min(end_x)..start_x.max(end_x); selections.clear(); let mut stack = Vec::new(); @@ -5965,8 +5968,9 @@ impl Editor { if let Some(selection) = self.selections.build_columnar_selection( &display_map, row, - &columns, + &positions, oldest_selection.reversed, + &text_layout_details, ) { stack.push(selection.id); selections.push(selection); @@ -5994,12 +5998,15 @@ impl Editor { let range = selection.display_range(&display_map).sorted(); debug_assert_eq!(range.start.row(), range.end.row()); let mut row = range.start.row(); - let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal + let positions = if let SelectionGoal::HorizontalRange { start, end } = + selection.goal { start..end } else { - cmp::min(range.start.column(), range.end.column()) - ..cmp::max(range.start.column(), range.end.column()) + let start_x = display_map.x_for_point(range.start, &text_layout_details); + let end_x = display_map.x_for_point(range.end, &text_layout_details); + + start_x.min(end_x)..start_x.max(end_x) }; while row != end_row { @@ -6012,8 +6019,9 @@ impl Editor { if let Some(new_selection) = self.selections.build_columnar_selection( &display_map, row, - &columns, + &positions, selection.reversed, + &text_layout_details, ) { state.stack.push(new_selection.id); if above { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 24cbadfd37..30015eb760 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus; use gpui::{ color::Color, elements::*, - fonts::{HighlightStyle, TextStyle, Underline}, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -37,8 +37,7 @@ use gpui::{ use itertools::Itertools; use json::json; use language::{ - language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, - Selection, + language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection, }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 38cf5cd6c1..7e75ae5e5d 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,6 +1,6 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, Editor, EditorStyle, ToOffset, ToPoint}; -use gpui::{text_layout, FontCache, TextLayoutCache, WindowContext}; +use gpui::{FontCache, TextLayoutCache, WindowContext}; use language::Point; use std::{ops::Range, sync::Arc}; @@ -105,7 +105,9 @@ pub fn up_by_rows( ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::WrappedHorizontalPosition((_, x)) => x, SelectionGoal::HorizontalRange { end, .. } => end, + SelectionGoal::WrappedHorizontalRange { end: (_, end), .. } => end, _ => map.x_for_point(start, text_layout_details), }; @@ -140,7 +142,9 @@ pub fn down_by_rows( ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x, + SelectionGoal::WrappedHorizontalPosition((_, x)) => x, SelectionGoal::HorizontalRange { end, .. } => end, + SelectionGoal::WrappedHorizontalRange { end: (_, end), .. } => end, _ => map.x_for_point(start, text_layout_details), }; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 6a21c898ef..2fa8ffe408 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -1,6 +1,6 @@ use std::{ cell::Ref, - cmp, iter, mem, + iter, mem, ops::{Deref, DerefMut, Range, Sub}, sync::Arc, }; @@ -13,6 +13,7 @@ use util::post_inc; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + movement::TextLayoutDetails, Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset, }; @@ -305,23 +306,27 @@ impl SelectionsCollection { &mut self, display_map: &DisplaySnapshot, row: u32, - columns: &Range, + positions: &Range, reversed: bool, + text_layout_details: &TextLayoutDetails, ) -> Option> { - let is_empty = columns.start == columns.end; + let is_empty = positions.start == positions.end; let line_len = display_map.line_len(row); - if columns.start < line_len || (is_empty && columns.start == line_len) { - let start = DisplayPoint::new(row, columns.start); - let end = DisplayPoint::new(row, cmp::min(columns.end, line_len)); + + let start_col = display_map.column_for_x(row, positions.start, text_layout_details); + if start_col < line_len || (is_empty && start_col == line_len) { + let start = DisplayPoint::new(row, start_col); + let end_col = display_map.column_for_x(row, positions.end, text_layout_details); + let end = DisplayPoint::new(row, end_col); Some(Selection { id: post_inc(&mut self.next_selection_id), start: start.to_point(display_map), end: end.to_point(display_map), reversed, - goal: SelectionGoal::ColumnRange { - start: columns.start, - end: columns.end, + goal: SelectionGoal::HorizontalRange { + start: positions.start, + end: positions.end, }, }) } else { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 38831f92c2..e127083112 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -7,8 +7,8 @@ pub enum SelectionGoal { None, HorizontalPosition(f32), HorizontalRange { start: f32, end: f32 }, - Column(u32), - ColumnRange { start: u32, end: u32 }, + WrappedHorizontalPosition((u32, f32)), + WrappedHorizontalRange { start: (u32, f32), end: (u32, f32) }, } #[derive(Clone, Debug, PartialEq)] diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2f1e376c3e..36514f8cc4 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,5 +1,3 @@ -use std::cmp; - use editor::{ char_kind, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, @@ -371,13 +369,13 @@ impl Motion { Backspace => (backspace(map, point, times), SelectionGoal::None), Down { display_lines: false, - } => down(map, point, goal, times), + } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details), Down { display_lines: true, } => down_display(map, point, goal, times, &text_layout_details), Up { display_lines: false, - } => up(map, point, goal, times), + } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details), Up { display_lines: true, } => up_display(map, point, goal, times, &text_layout_details), @@ -536,35 +534,86 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di point } -fn down( +pub(crate) fn start_of_relative_buffer_row( + map: &DisplaySnapshot, + point: DisplayPoint, + times: isize, +) -> DisplayPoint { + let start = map.display_point_to_fold_point(point, Bias::Left); + let target = start.row() as isize + times; + let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row()); + + map.clip_point( + map.fold_point_to_display_point( + map.fold_snapshot + .clip_point(FoldPoint::new(new_row, 0), Bias::Right), + ), + Bias::Right, + ) +} + +fn up_down_buffer_rows( map: &DisplaySnapshot, point: DisplayPoint, mut goal: SelectionGoal, - times: usize, + times: isize, + text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let start = map.display_point_to_fold_point(point, Bias::Left); + let begin_folded_line = map.fold_point_to_display_point( + map.fold_snapshot + .clip_point(FoldPoint::new(start.row(), 0), Bias::Left), + ); + let select_nth_wrapped_row = point.row() - begin_folded_line.row(); - let goal_column = match goal { - SelectionGoal::Column(column) => column, - SelectionGoal::ColumnRange { end, .. } => end, + let (goal_wrap, goal_x) = match goal { + SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x), + SelectionGoal::WrappedHorizontalRange { end: (row, x), .. } => (row, x), + SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end), + SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x), _ => { - goal = SelectionGoal::Column(start.column()); - start.column() + let x = map.x_for_point(point, text_layout_details); + goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x)); + (select_nth_wrapped_row, x) } }; - let new_row = cmp::min( - start.row() + times as u32, - map.fold_snapshot.max_point().row(), - ); - let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); - let point = map.fold_point_to_display_point( + let target = start.row() as isize + times; + let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row()); + + let mut begin_folded_line = map.fold_point_to_display_point( map.fold_snapshot - .clip_point(FoldPoint::new(new_row, new_col), Bias::Left), + .clip_point(FoldPoint::new(new_row, 0), Bias::Left), ); - // clip twice to "clip at end of line" - (map.clip_point(point, Bias::Left), goal) + let mut i = 0; + while i < goal_wrap && begin_folded_line.row() < map.max_point().row() { + let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0); + if map + .display_point_to_fold_point(next_folded_line, Bias::Right) + .row() + == new_row + { + i += 1; + begin_folded_line = next_folded_line; + } else { + break; + } + } + + let new_col = if i == goal_wrap { + map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details) + } else { + map.line_len(begin_folded_line.row()) + }; + + ( + map.clip_point( + DisplayPoint::new(begin_folded_line.row(), new_col), + Bias::Left, + ), + goal, + ) } fn down_display( @@ -581,33 +630,6 @@ fn down_display( (point, goal) } -pub(crate) fn up( - map: &DisplaySnapshot, - point: DisplayPoint, - mut goal: SelectionGoal, - times: usize, -) -> (DisplayPoint, SelectionGoal) { - let start = map.display_point_to_fold_point(point, Bias::Left); - - let goal_column = match goal { - SelectionGoal::Column(column) => column, - SelectionGoal::ColumnRange { end, .. } => end, - _ => { - goal = SelectionGoal::Column(start.column()); - start.column() - } - }; - - let new_row = start.row().saturating_sub(times as u32); - let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); - let point = map.fold_point_to_display_point( - map.fold_snapshot - .clip_point(FoldPoint::new(new_row, new_col), Bias::Left), - ); - - (map.clip_point(point, Bias::Left), goal) -} - fn up_display( map: &DisplaySnapshot, mut point: DisplayPoint, @@ -894,7 +916,7 @@ fn find_backward( } fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = down(map, point, SelectionGoal::None, times).0; + let correct_line = start_of_relative_buffer_row(map, point, times as isize); first_non_whitespace(map, false, correct_line) } @@ -904,7 +926,7 @@ pub(crate) fn next_line_end( times: usize, ) -> DisplayPoint { if times > 1 { - point = down(map, point, SelectionGoal::None, times - 1).0; + point = start_of_relative_buffer_row(map, point, times as isize - 1); } end_of_line(map, false, point) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 9c93f19fc7..0e883cd758 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -194,9 +194,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() { - if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) { + if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { vim.switch_mode(Mode::VisualBlock, false, cx); } else { vim.switch_mode(Mode::Visual, false, cx) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index bec91007e3..ac4c5478a1 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -140,17 +140,21 @@ pub fn visual_block_motion( SelectionGoal, ) -> Option<(DisplayPoint, SelectionGoal)>, ) { + let text_layout_details = TextLayoutDetails::new(editor, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { let map = &s.display_map(); let mut head = s.newest_anchor().head().to_display_point(map); let mut tail = s.oldest_anchor().tail().to_display_point(map); let (start, end) = match s.newest_anchor().goal { - SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), - SelectionGoal::Column(start) if preserve_goal => (start, start + 1), - _ => (tail.column(), head.column()), + SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), + SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start + 10.0), + _ => ( + map.x_for_point(tail, &text_layout_details), + map.x_for_point(head, &text_layout_details), + ), }; - let goal = SelectionGoal::ColumnRange { start, end }; + let goal = SelectionGoal::HorizontalRange { start, end }; let was_reversed = tail.column() > head.column(); if !was_reversed && !preserve_goal { @@ -172,21 +176,39 @@ pub fn visual_block_motion( head = movement::saturating_right(map, head) } - let columns = if is_reversed { - head.column()..tail.column() + let positions = if is_reversed { + map.x_for_point(head, &text_layout_details)..map.x_for_point(tail, &text_layout_details) } else if head.column() == tail.column() { - head.column()..(head.column() + 1) + map.x_for_point(head, &text_layout_details) + ..map.x_for_point(head, &text_layout_details) + 10.0 } else { - tail.column()..head.column() + map.x_for_point(tail, &text_layout_details)..map.x_for_point(head, &text_layout_details) }; let mut selections = Vec::new(); let mut row = tail.row(); loop { - let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left); - let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left); - if columns.start <= map.line_len(row) { + let start = map.clip_point( + DisplayPoint::new( + row, + map.column_for_x(row, positions.start, &text_layout_details), + ), + Bias::Left, + ); + let end = map.clip_point( + DisplayPoint::new( + row, + map.column_for_x(row, positions.end, &text_layout_details), + ), + Bias::Left, + ); + if positions.start + <= map.x_for_point( + DisplayPoint::new(row, map.line_len(row)), + &text_layout_details, + ) + { let selection = Selection { id: s.new_selection_id(), start: start.to_point(map), diff --git a/crates/vim/test_data/test_j.json b/crates/vim/test_data/test_j.json index 64aaf65ef8..703f69d22c 100644 --- a/crates/vim/test_data/test_j.json +++ b/crates/vim/test_data/test_j.json @@ -1,3 +1,6 @@ +{"Put":{"state":"aaˇaa\n😃😃"}} +{"Key":"j"} +{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}} {"Put":{"state":"ˇThe quick brown\nfox jumps"}} {"Key":"j"} {"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} From 354882f2c00d877defaf050ae8a34451ef5fa851 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 10 Oct 2023 00:16:15 -0400 Subject: [PATCH 037/274] Enable completion menu to resolve documentation when guest --- crates/collab/src/rpc.rs | 1 + crates/editor/src/editor.rs | 74 ++++++++++++-- crates/language/src/buffer.rs | 13 +-- crates/project/src/lsp_command.rs | 12 ++- crates/project/src/project.rs | 35 +++++++ crates/rpc/proto/zed.proto | 157 ++++++++++++++++-------------- crates/rpc/src/proto.rs | 7 ++ crates/rpc/src/rpc.rs | 2 +- 8 files changed, 207 insertions(+), 94 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5eb434e167..ed09cde061 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -224,6 +224,7 @@ impl Server { .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) + .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b8c9690b90..3143b19629 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -60,10 +60,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, - CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, IndentSize, - Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, - TransactionId, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, + Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, + IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, + SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -80,7 +80,7 @@ use ordered_float::OrderedFloat; use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; -use rpc::proto::PeerId; +use rpc::proto::{self, PeerId}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -999,7 +999,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completions_guard = completions.read(); let completion = &completions_guard[index]; - if completion.lsp_completion.documentation.is_some() { + if completion.documentation.is_some() { return; } @@ -1007,6 +1007,57 @@ impl CompletionsMenu { let completion = completion.lsp_completion.clone(); drop(completions_guard); + if project.read(cx).is_remote() { + let Some(project_id) = project.read(cx).remote_id() else { + log::error!("Remote project without remote_id"); + return; + }; + + let client = project.read(cx).client(); + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + cx.spawn(|this, mut cx| async move { + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; + + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[index]; + completion.documentation = Some(documentation); + drop(completions); + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); + + return; + } + let Some(server) = project.read(cx).language_server_for_id(server_id) else { return; }; @@ -1037,11 +1088,14 @@ impl CompletionsMenu { let mut completions = completions.write(); let completion = &mut completions[index]; - completion.documentation = documentation; - completion.lsp_completion.documentation = Some(lsp_documentation); + completion.documentation = Some(documentation); drop(completions); _ = this.update(&mut cx, |_, cx| cx.notify()); + } else { + let mut completions = completions.write(); + let completion = &mut completions[index]; + completion.documentation = Some(Documentation::Undocumented); } }) .detach(); @@ -1061,10 +1115,10 @@ impl CompletionsMenu { .max_by_key(|(_, mat)| { let completions = self.completions.read(); let completion = &completions[mat.candidate_id]; - let documentation = &completion.lsp_completion.documentation; + let documentation = &completion.documentation; let mut len = completion.label.text.chars().count(); - if let Some(lsp::Documentation::String(text)) = documentation { + if let Some(Documentation::SingleLine(text)) = documentation { len += text.chars().count(); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d318a87b40..d8ebc1d445 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -149,28 +149,28 @@ pub async fn prepare_completion_documentation( documentation: &lsp::Documentation, language_registry: &Arc, language: Option>, -) -> Option { +) -> Documentation { match documentation { lsp::Documentation::String(text) => { if text.lines().count() <= 1 { - Some(Documentation::SingleLine(text.clone())) + Documentation::SingleLine(text.clone()) } else { - Some(Documentation::MultiLinePlainText(text.clone())) + Documentation::MultiLinePlainText(text.clone()) } } lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { lsp::MarkupKind::PlainText => { if value.lines().count() <= 1 { - Some(Documentation::SingleLine(value.clone())) + Documentation::SingleLine(value.clone()) } else { - Some(Documentation::MultiLinePlainText(value.clone())) + Documentation::MultiLinePlainText(value.clone()) } } lsp::MarkupKind::Markdown => { let parsed = parse_markdown(value, language_registry, language).await; - Some(Documentation::MultiLineMarkdown(parsed)) + Documentation::MultiLineMarkdown(parsed) } }, } @@ -178,6 +178,7 @@ pub async fn prepare_completion_documentation( #[derive(Clone, Debug)] pub enum Documentation { + Undocumented, SingleLine(String), MultiLinePlainText(String), MultiLineMarkdown(ParsedMarkdown), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c71b378da6..72d79ca979 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1466,12 +1466,14 @@ impl LspCommand for GetCompletions { } let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { - prepare_completion_documentation( - lsp_docs, - &language_registry, - language.clone(), + Some( + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await, ) - .await } else { None }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a50e02a631..f25309b9c6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -580,6 +580,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_completion_documentation); client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); @@ -7155,6 +7156,40 @@ impl Project { }) } + async fn handle_resolve_completion_documentation( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?; + + let completion = this + .read_with(&mut cx, |this, _| { + let id = LanguageServerId(envelope.payload.language_server_id as usize); + let Some(server) = this.language_server_for_id(id) else { + return Err(anyhow!("No language server {id}")); + }; + + Ok(server.request::(lsp_completion)) + })? + .await?; + + let mut is_markdown = false; + let text = match completion.documentation { + Some(lsp::Documentation::String(text)) => text, + + Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => { + is_markdown = kind == lsp::MarkupKind::Markdown; + value + } + + _ => String::new(), + }; + + Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown }) + } + async fn handle_apply_code_action( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3501e70e6a..e97ede3fee 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -89,88 +89,90 @@ message Envelope { FormatBuffersResponse format_buffers_response = 70; GetCompletions get_completions = 71; GetCompletionsResponse get_completions_response = 72; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; - GetCodeActions get_code_actions = 75; - GetCodeActionsResponse get_code_actions_response = 76; - GetHover get_hover = 77; - GetHoverResponse get_hover_response = 78; - ApplyCodeAction apply_code_action = 79; - ApplyCodeActionResponse apply_code_action_response = 80; - PrepareRename prepare_rename = 81; - PrepareRenameResponse prepare_rename_response = 82; - PerformRename perform_rename = 83; - PerformRenameResponse perform_rename_response = 84; - SearchProject search_project = 85; - SearchProjectResponse search_project_response = 86; + ResolveCompletionDocumentation resolve_completion_documentation = 73; + ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76; + GetCodeActions get_code_actions = 77; + GetCodeActionsResponse get_code_actions_response = 78; + GetHover get_hover = 79; + GetHoverResponse get_hover_response = 80; + ApplyCodeAction apply_code_action = 81; + ApplyCodeActionResponse apply_code_action_response = 82; + PrepareRename prepare_rename = 83; + PrepareRenameResponse prepare_rename_response = 84; + PerformRename perform_rename = 85; + PerformRenameResponse perform_rename_response = 86; + SearchProject search_project = 87; + SearchProjectResponse search_project_response = 88; - UpdateContacts update_contacts = 87; - UpdateInviteInfo update_invite_info = 88; - ShowContacts show_contacts = 89; + UpdateContacts update_contacts = 89; + UpdateInviteInfo update_invite_info = 90; + ShowContacts show_contacts = 91; - GetUsers get_users = 90; - FuzzySearchUsers fuzzy_search_users = 91; - UsersResponse users_response = 92; - RequestContact request_contact = 93; - RespondToContactRequest respond_to_contact_request = 94; - RemoveContact remove_contact = 95; + GetUsers get_users = 92; + FuzzySearchUsers fuzzy_search_users = 93; + UsersResponse users_response = 94; + RequestContact request_contact = 95; + RespondToContactRequest respond_to_contact_request = 96; + RemoveContact remove_contact = 97; - Follow follow = 96; - FollowResponse follow_response = 97; - UpdateFollowers update_followers = 98; - Unfollow unfollow = 99; - GetPrivateUserInfo get_private_user_info = 100; - GetPrivateUserInfoResponse get_private_user_info_response = 101; - UpdateDiffBase update_diff_base = 102; + Follow follow = 98; + FollowResponse follow_response = 99; + UpdateFollowers update_followers = 100; + Unfollow unfollow = 101; + GetPrivateUserInfo get_private_user_info = 102; + GetPrivateUserInfoResponse get_private_user_info_response = 103; + UpdateDiffBase update_diff_base = 104; - OnTypeFormatting on_type_formatting = 103; - OnTypeFormattingResponse on_type_formatting_response = 104; + OnTypeFormatting on_type_formatting = 105; + OnTypeFormattingResponse on_type_formatting_response = 106; - UpdateWorktreeSettings update_worktree_settings = 105; + UpdateWorktreeSettings update_worktree_settings = 107; - InlayHints inlay_hints = 106; - InlayHintsResponse inlay_hints_response = 107; - ResolveInlayHint resolve_inlay_hint = 108; - ResolveInlayHintResponse resolve_inlay_hint_response = 109; - RefreshInlayHints refresh_inlay_hints = 110; + InlayHints inlay_hints = 108; + InlayHintsResponse inlay_hints_response = 109; + ResolveInlayHint resolve_inlay_hint = 110; + ResolveInlayHintResponse resolve_inlay_hint_response = 111; + RefreshInlayHints refresh_inlay_hints = 112; - CreateChannel create_channel = 111; - CreateChannelResponse create_channel_response = 112; - InviteChannelMember invite_channel_member = 113; - RemoveChannelMember remove_channel_member = 114; - RespondToChannelInvite respond_to_channel_invite = 115; - UpdateChannels update_channels = 116; - JoinChannel join_channel = 117; - DeleteChannel delete_channel = 118; - GetChannelMembers get_channel_members = 119; - GetChannelMembersResponse get_channel_members_response = 120; - SetChannelMemberAdmin set_channel_member_admin = 121; - RenameChannel rename_channel = 122; - RenameChannelResponse rename_channel_response = 123; + CreateChannel create_channel = 113; + CreateChannelResponse create_channel_response = 114; + InviteChannelMember invite_channel_member = 115; + RemoveChannelMember remove_channel_member = 116; + RespondToChannelInvite respond_to_channel_invite = 117; + UpdateChannels update_channels = 118; + JoinChannel join_channel = 119; + DeleteChannel delete_channel = 120; + GetChannelMembers get_channel_members = 121; + GetChannelMembersResponse get_channel_members_response = 122; + SetChannelMemberAdmin set_channel_member_admin = 123; + RenameChannel rename_channel = 124; + RenameChannelResponse rename_channel_response = 125; - JoinChannelBuffer join_channel_buffer = 124; - JoinChannelBufferResponse join_channel_buffer_response = 125; - UpdateChannelBuffer update_channel_buffer = 126; - LeaveChannelBuffer leave_channel_buffer = 127; - UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; - RejoinChannelBuffers rejoin_channel_buffers = 129; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - AckBufferOperation ack_buffer_operation = 143; + JoinChannelBuffer join_channel_buffer = 126; + JoinChannelBufferResponse join_channel_buffer_response = 127; + UpdateChannelBuffer update_channel_buffer = 128; + LeaveChannelBuffer leave_channel_buffer = 129; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130; + RejoinChannelBuffers rejoin_channel_buffers = 131; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132; + AckBufferOperation ack_buffer_operation = 145; - JoinChannelChat join_channel_chat = 131; - JoinChannelChatResponse join_channel_chat_response = 132; - LeaveChannelChat leave_channel_chat = 133; - SendChannelMessage send_channel_message = 134; - SendChannelMessageResponse send_channel_message_response = 135; - ChannelMessageSent channel_message_sent = 136; - GetChannelMessages get_channel_messages = 137; - GetChannelMessagesResponse get_channel_messages_response = 138; - RemoveChannelMessage remove_channel_message = 139; - AckChannelMessage ack_channel_message = 144; + JoinChannelChat join_channel_chat = 133; + JoinChannelChatResponse join_channel_chat_response = 134; + LeaveChannelChat leave_channel_chat = 135; + SendChannelMessage send_channel_message = 136; + SendChannelMessageResponse send_channel_message_response = 137; + ChannelMessageSent channel_message_sent = 138; + GetChannelMessages get_channel_messages = 139; + GetChannelMessagesResponse get_channel_messages_response = 140; + RemoveChannelMessage remove_channel_message = 141; + AckChannelMessage ack_channel_message = 146; - LinkChannel link_channel = 140; - UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 144 + LinkChannel link_channel = 142; + UnlinkChannel unlink_channel = 143; + MoveChannel move_channel = 144; // current max: 146 } } @@ -832,6 +834,17 @@ message ResolveState { } } +message ResolveCompletionDocumentation { + uint64 project_id = 1; + uint64 language_server_id = 2; + bytes lsp_completion = 3; +} + +message ResolveCompletionDocumentationResponse { + string text = 1; + bool is_markdown = 2; +} + message ResolveInlayHint { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0d7937f6f..abadada328 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -205,6 +205,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveCompletionDocumentation, Background), + (ResolveCompletionDocumentationResponse, Background), (ResolveInlayHint, Background), (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), @@ -318,6 +320,10 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + ( + ResolveCompletionDocumentation, + ResolveCompletionDocumentationResponse + ), (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), @@ -381,6 +387,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveCompletionDocumentation, ResolveInlayHint, RefreshInlayHints, PrepareRename, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 942672b94b..5ba531a50e 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 64; +pub const PROTOCOL_VERSION: u32 = 65; From f5af5f7334a7b523090c0741029dde913cd00c2c Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 10 Oct 2023 09:27:18 -0400 Subject: [PATCH 038/274] Avoid leaving selected item index past end of matches list Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3143b19629..9b276f002c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1332,6 +1332,7 @@ impl CompletionsMenu { } self.matches = matches.into(); + self.selected_item = 0; } } From 801af95a13e0332af3f9a4d1a1d62c47bea07529 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 10 Oct 2023 10:08:29 -0400 Subject: [PATCH 039/274] Make completion documentation scroll & fix accompanying panic from tag Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 26 +++++++---- crates/editor/src/hover_popover.rs | 4 +- crates/gpui/src/elements/flex.rs | 75 ++++++++++++++++++------------ 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9b276f002c..06482dbbc6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -119,7 +119,7 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -pub fn render_parsed_markdown( +pub fn render_parsed_markdown( parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, cx: &mut ViewContext, @@ -153,7 +153,7 @@ pub fn render_parsed_markdown( style: CursorStyle::PointingHand, }); cx.scene().push_mouse_region( - MouseRegion::new::(view_id, region_id, bounds) + MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) .on_click::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), @@ -1247,6 +1247,8 @@ impl CompletionsMenu { }) .with_width_from_item(widest_completion_ix); + enum MultiLineDocumentation {} + Flex::row() .with_child(list) .with_children({ @@ -1256,13 +1258,21 @@ impl CompletionsMenu { let documentation = &completion.documentation; match documentation { - Some(Documentation::MultiLinePlainText(text)) => { - Some(Text::new(text.clone(), style.text.clone())) - } + Some(Documentation::MultiLinePlainText(text)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child( + Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), + ), + ), - Some(Documentation::MultiLineMarkdown(parsed)) => { - Some(render_parsed_markdown(parsed, &style, cx)) - } + Some(Documentation::MultiLineMarkdown(parsed)) => Some( + Flex::column() + .scrollable::(0, None, cx) + .with_child(render_parsed_markdown::( + parsed, &style, cx, + )), + ), _ => None, } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index d5ccb481b2..e8901ad6c1 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -474,8 +474,8 @@ impl InfoPopover { ) -> AnyElement { MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() - .scrollable::(1, None, cx) - .with_child(crate::render_parsed_markdown( + .scrollable::(0, None, cx) + .with_child(crate::render_parsed_markdown::( &self.parsed_content, style, cx, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index cdce0423fd..ba387c5e48 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc}; use crate::{ json::{self, ToJson, Value}, - AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext, + AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt, + ViewContext, }; use pathfinder_geometry::{ rect::RectF, @@ -10,10 +11,10 @@ use pathfinder_geometry::{ }; use serde_json::json; -#[derive(Default)] struct ScrollState { scroll_to: Cell>, scroll_position: Cell, + type_tag: TypeTag, } pub struct Flex { @@ -66,8 +67,14 @@ impl Flex { where Tag: 'static, { - let scroll_state = cx.default_element_state::>(element_id); - scroll_state.read(cx).scroll_to.set(scroll_to); + let scroll_state = cx.element_state::>( + element_id, + Rc::new(ScrollState { + scroll_to: Cell::new(scroll_to), + scroll_position: Default::default(), + type_tag: TypeTag::new::(), + }), + ); self.scroll_state = Some((scroll_state, cx.handle().id())); self } @@ -276,38 +283,44 @@ impl Element for Flex { if let Some((scroll_state, id)) = &self.scroll_state { let scroll_state = scroll_state.read(cx).clone(); cx.scene().push_mouse_region( - crate::MouseRegion::new::(*id, 0, bounds) - .on_scroll({ - let axis = self.axis; - move |e, _: &mut V, cx| { - if remaining_space < 0. { - let scroll_delta = e.delta.raw(); + crate::MouseRegion::from_handlers( + scroll_state.type_tag, + *id, + 0, + bounds, + Default::default(), + ) + .on_scroll({ + let axis = self.axis; + move |e, _: &mut V, cx| { + if remaining_space < 0. { + let scroll_delta = e.delta.raw(); - let mut delta = match axis { - Axis::Horizontal => { - if scroll_delta.x().abs() >= scroll_delta.y().abs() { - scroll_delta.x() - } else { - scroll_delta.y() - } + let mut delta = match axis { + Axis::Horizontal => { + if scroll_delta.x().abs() >= scroll_delta.y().abs() { + scroll_delta.x() + } else { + scroll_delta.y() } - Axis::Vertical => scroll_delta.y(), - }; - if !e.delta.precise() { - delta *= 20.; } - - scroll_state - .scroll_position - .set(scroll_state.scroll_position.get() - delta); - - cx.notify(); - } else { - cx.propagate_event(); + Axis::Vertical => scroll_delta.y(), + }; + if !e.delta.precise() { + delta *= 20.; } + + scroll_state + .scroll_position + .set(scroll_state.scroll_position.get() - delta); + + cx.notify(); + } else { + cx.propagate_event(); } - }) - .on_move(|_, _: &mut V, _| { /* Capture move events */ }), + } + }) + .on_move(|_, _: &mut V, _| { /* Capture move events */ }), ) } From 7c867b6e5456eac337d7008fc7ed89ba3b9d669a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 09:23:06 -0600 Subject: [PATCH 040/274] 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 zcmdUV36v9M);8TVTeB*-iv&awnkJP!Q9x@;Qb{UFRVpirR;ntMr7C+;N!*}SRKRg# zbW}tXonhP+k#S>0{S#y`DfN-NmCVyrBp7vV1{lUG(#g~xg4L$iVJ208GE;N#+eH{=TFCcC=>PL zn!J=Ml{Nk#;vrDYjBc$K&gUgTtHl^h@>s`Zb}7K`lQNl40M3dKC$7&Uk_ZGSU$NoY}M4vMkmfkpD5uuu=_4c$7ZTc=}m zrqlJ-(~bJrd~Nep@0$D)TLwFjf3$%9ZTbR{pP!m2MZnbwB%5VLf$twA!Ad1T+joLk z>qvwm3T^;w8XSR8>Tlb(l`-?dy4ZyzLDUGtDG06a1TPkiooDy5-mAkT@Z&AB||$x`dGMDi*p&99aSsV+n(vS#iH1U?;!JX0GL0)f23N>5(I7F> z6mHf<)w)`5oiUt7kz503Nknv@q8xDLWX77RWx!rZG^3-vOeh@A)O=wHuhAHSBBD2h zz$EE|Xo;6YrMLl+xD?`|QKY#KHtbFE4yhVX=ftoa%7F8z#GIi9?{wv35wfI~qjjU` z#)piZCuvf_sopbz^WGp6)PKnm0q@;#4%|D1c@wgo&5A@e>gNSc%t2AwcX&5|_n5|N#U^`h5xLhO|62oY`m`kT*f88Dmm0k6W&DT}tyiAqK z+3h0*$({#s^6pcA%>6Q9(PR;0S_IK)zuG5yc`R;&z1M6gV2quudA{uw2!n zx1~7)T#2^yr4rTK%Dr_8S=VJ^kyt*4!FsSZ#Hq?34T%W&4R(scbW=XSdNYM&Cgrf{ zy%~hU6)|B{M2;M^4gw_!haMvwjUdnoHf#s;1VvTvf&S*XI+7l&+d$V-LI6p|vl)?d z*z3GABSs_M1|QY|Sw@mkJ-`nqos7#~iE!9t!b#^qAAldB3gNIToTH{U%X+V<6k}u=3F6nictAU0qV^G{yYcfYVIKSDL#Zy|W)F5J~f z%N7Rrd)PV^farV`tz_(ufVFOrv=(pKRw_an*-ZFL4a!jQa?S`qcXx%a$yAri%7js~ z&74WgkZ9DUbFq?JFr||X9iE}m?rOmpx5k}hkX7<*ChbO1KAuTvOIbHk*5id7q%Tnr z0l6qMWq`~2q)i}%0H?L_G^dprr>_<@rHdlj08FK6rU8BEBO$#^0>kNUTqSY>-woJS zA|(wWBTdWhrQyNWn?rL1;q@{oiZM6}EL;tskrxpqF_{sO7zMBeAPB5#z5-BYz+yOI z7SvU7TogTJGD@Ik1_r{yhk;f%R7(x|AwV)n6yQ%V=*`8#-Xa(uNF7X~5?}z~P%sYF z^1K{G;iOyumRxOH0_;GJ8(d`)4ubABCXB%h2Af=*E}4$&X^ByhB+N@O7+5U~jsfdO zak5R%M!bMkm>N{LR#MpqxEF~7Xs88Rg26S6lS3^loT0)0U<6|T17P49$>2INEkdM} zQ=FpW3pql`cu>ly+N?5 z7XwT}MNcMJ_hD2$oQ6dVP6lIPdzq2#VrLR3A@vy8w}7elc^K+nw-MDx8*l}KC6y7mXxx;Hhl3;$1nU4i6EG!k z=Y)#Z4X{sjz4>?wQ7w@aci0plD;02w77PjDeRV+2hOk4xq!dD=L>ko^WSTLvtW7FJ zTwSCYO+T#`^s)zzkVmKJvAwlsz1UH}v1OxUHN0E9O?gHi= z$Q?9*+D)t3&tl8$;Ik7Ic8Ht=OkfYuiU3%jN6JevR0q}{RosdqH?6MRya&l64`z<0 zBxmqeqk0L*Dd-obtMRZsAJ38`9LOmorKJBwUr3cRPk4y@9?n`7H>`wNeaIBmCzEX0 zSdNChz#4lSo-hyOCZuo;*aviG2;=mm3L^NksEmbb0Nv;0e(%7;MXV{W#~v^R6;!o; zz&mK9A>av+O00p$fxz~827x}79+YiW9tB(_0GA1zYy8JN0ESY$7^#qqSo1NkzUcup zGDGXqNS35ailfE8F${6_c_e|j-Ub101|d*Lq#I<-m4-7d51>RkRe5&Qq<`z}3^02) z(6PR;U2&PLkW@)QTk|?_#T5!R5Iw+52T})?nl#z~t~!uF1pyQ?1?veIai#N~h7RTm zOgU2rE*nh($Us9@E}UkvPR@?RV^}3DBZ0gj<}4?ebegHuYIr%K)j2KEdQ>vF%uzbd zG!Q=?%E8)DGK#q)I;>vvA`r*$d?6y4_$nD{l-(wsg}|!ON{ER?@^Z!};ck-kqQFWh zy-d-EAW_`zsVIhmJxUr=Y@R5@3`JlT&04lz4-`qb#cXjEn-$7Vsum$({RxJGJ4iSO z7-ehUz-$0i6=Bj5L`e$8%$yUqOGY-rP*J^LsM;Kg55{AjinpsyCjl)7Fd*dW$-G?k zDWX4^^@o{SHeh7ST?L{B#&GE5R1$ORU_+3x>b+FM45ehBMCvW+IG>A@^;Dv6C*`ha zGR0#Vk6l;w1X+;EP$C84q9=ej$p$WyC}3m2lSUPV@6#7bAF2bK0vO)wO{NfpkI*?_ zEAxOY*YE~Rx5mx^eeaXQre|408?dYOF3P}C2zbp#VE=G{*+d;+IC#i9uz#Qq{KtqI zQ2rM4v*#2pJ+L0&+4rv}XpjI_hk))%06#<7K0Dad4UGCv=tWFrrGxdNPgY zL;{wQ*PIMvK0+=8+%j+OstT2=zEsK-8i->MqX)#CGLx7%#DgGVhhvZjQH>hBil^Uh z0FNeMYu15iM!DtD0gne@6!3na^K zUa}c|iKuc(r=wx6;gP-dI3Rb;v3k7?AtRW5L@wmQ#S~rAccp8&jL@Jkx19NDT?QC7 z#DhrUfLp5G!{Iv40Uf7M5($!psKrq<#qz9KNEkwmAgq&0MadJ4lB|cXpoSXjsiBdE zC|A72lHj$H9F-Lcf-jZO7kPzHq6{fo16gfXl&gb)ArZrLdO_#NKq67)izboCJ8L>7 zuLEQyRZA7Z+EUbI!1KV*U=%G`r~_MvA!(rJKnD6EVYnkv!Q?`%93~F4b;SQ-U4aF= z0@Z;$KqNx|-6o7)Mq<@EZv)l=9<098H9bAtSaYZ0TnjHqk_Z8mu@j=oNqJdna(gyx z$~HNsK9*NK?x0@EHhE^_|1KgIW?%*pIS@%m`}`Dv0A1+)O(KZmMPZmGt3i*`Ym^dj zB&T>1SvVT#5{mJlJpfz6=6s|Ofx2o1eM%0Yt_;QbN%j!5b5o;J5Jz#s1X|j zctAvC2seO@b}^Lo2dp}(f;PIsUEm1Wl9se;t0Zh}?ip)w1HdW>CvcwpgQHCtqWYvi z!VMIL0B+zEO5k}u!3DwrGvrRoVMo;?`1zWfbJu;^x-FGJjbW?99jX|ysuxZnIk78& zmN`DkQw70WLkuM;7;q306*PuJoWmL_1!{t>>gGIY2MY75y>|whzOB>vzgOQCf*6cb zYQ#QD#0WSX#1P=yAQUQ!LPl})%aa<(&tQOi4)yUFnCm}#1wv3NSi@>+6rBVZ849D00Bo0Fg#sKY2_u^?p$25FBUF!x+72xly88lmw*fQrd6H>q?L3gHx& zboq-oRAC6aU#Bzs1#!WG{)o?*fxk}hvzZ<9`zbUjaGCyePG9P$H!6gKG*?*?)O6TD z%BMHI2F6gwX$=H2b-g*e-VB;hoX3_*IW7gVMSM4(li5_Ze~A9<)SN-Nsh=57%>?$Q z-MUl#dAGg=_m4D?X`2UDFs~u-^I~9Pki7c2DM0Rx1DKxeW;sqMmAb`Zu96?L#Qw|{ z*kW%caL^<}R^A^>Q_tgHA4*wZ<^QvN3|_YyuN$1z#B2Ff@n^G!MxB~9gv>veHTw!~k%Bnin&#j!u@)dQBLTRps$a3=rkD}&SnUpLrIZvuO5Y$X%%YrBr7n)5L zXuh_u)GPPFAc6I-ws-RuL9e&5;B0vdWbQVa;^uB!TyN>tnc|j&)xcWJX72ELL#;%4 z|4@C?=myQ$ceC@g&209VuTra^@%`!fwy_{zA8)4bhX*@GPHqndbd#YYMuI};6t&&($>Uq?=v=O>0ao&)W5(+tonB|w*$h^bId;r= zgIa`Ie=(?{!yu&DbKDuNp5a_k;uWB`s&J{9b)l%C1(3A{6P7eTH^2HJg1XLe?G97_ zPR0%1NpFkhUZI}08G}!wZN#wQJ#7=gCk$G z=-XU-e%J1YUwLt7;)pY1pN_#xSNh-devlk-(XPsBcia8D_Pi>W$=!kNJC3*ZoWA|m zJ?G3m|DmOy&7L*((0LauJMz$%ao@;KJg-gLDX;mww5v9K{H^(&_V6|5ikEIFDV^)% z@kj5^FMFbL@|E}1|NilvZ+>^lsrt?{W?r{QbNlMov`db?<-Pf1KiT-*z9r#_$E-QC z>+)Go9B3cjHmq&k(l4N;`=BGi!A_mr*7ifkXlNw(*WNY^nx@vB+;J>)^pYuE+f$BJ z)iK|How;h>S-(EuATJ)h7{W%5?i$%PV#KJnwhk*~hKzk>sBPJ4yZ6M?XH8msXc1l> z|7GLCE3VzPn_Gcg`TDW%!y6)NMvlJR`QBL%oV?@ny>rj|Xz!gDpZMwOOWs;_t|x9= z?!ABVRTrYadw21@n~yzf|Hl*Oo-_ISug(|NKa+Is{^5}uZW(t-Z(g%z{4dY7cCNVL z^&_QCE5`ofj|=zZ+V0*);^)0{X(18+=r{p-W%&x_`0i6SOQ z57~N){}CkncaZVnwZ9!R?b>%va`^X-_i=LiJ1JfMX){NvD`{3{=?^WYav^JM%tz*jz3j*p#m34FO>`O=4= z$i7&Ier0mQ)l+v1r60t_!jr_?)!PhHgxi%>FLt*eqo=I_K(zWJRhy`)UK2z{K*NCs z4pP;h#E*XqFg$ZqGYRHJk8tw)rMccWu+>D{p!CZ^|wEtFJ4NZ_BjH zF1hT4n-(}M`+s}OS-+V3=0n#U`S#|oOSk;>b$;T@b)WI=I~E?S{gBx@YTt#nOI8Fl zZ&;su==V4KIx|0XeH;DFh%px&b5`hxlge*wT5<9jj&(b7Ge6k-+NS*b6Y)RKdg&{< z_W36pk2z-jyBEzcy>!ZsofF^6ym`mWlaDxX`@L6}mY(Z9c9#oSi+l4JaLFW<52!*92b8D)8}Qkf3* zj9396tki<9Nk@TOIrjNQmV4^g&AU0X`P{)rrgEbHAwHtgBt2w>^m?7n3RwY7(wiZx z0Wc*{w8S79e8D&u$a}}|ai@?hr||_ek)I^qnm|faiTM8m4z&N&b0VVwpct;0VA*}m z$Pas_JuvI_;fv_><5xaA=j#)D4)z3AOdho-{?0S6y=xdYectRB&^um7-kW;i=#hIv zr>)w&4!QS)?1F6*Z}?=EY4%-{FaPN218t|Bv@p5$Q^JLA`18yYrv7Qnz7u( z8h=jgcmzfl1~mtAg7V}ksr&ZlT~izNY<+>w?aMbMrd3=fVD$I`Oi;MjeH-R0RF>6aj3hm47D$t-bZ49LYD^7 z7?AELiYDNU79m}K^R{KD{7bsj>IAG~Pg|4ysDL;H6efXZ+z>$g{&0{R_x~rznfuNL z!1|n@hV=oc6xco&1%lKYoYpZL0(5dMpp(m{_1(h|GTD0zL+Rx5eeh++IT-Ebb@8j+ zW4@j;WBcObP#ZUD2{f_)xI4!F9GhJBg}-t9D;pPod&vL!#ubar^KLYp`I7DV)eh6l z)QYv^e{DUot8u*lqZ#hc*FXKVrmO42mm_C>H(~vQ=Y4q7q2ztDzIbiV{0CQ_vV6`B z3;*(kX6y%(zg}8aangy=-T|* zzp;ns-LT=g`?oE*)imSy!tc*FPTO^H{OTDqZwrmJ9liMJUyVL)>8EczcER%edv|@O zTP6M4xpzN#^B3#ahAXZ|Y^zUVu6psW-T(EMl`G|^EzF+U%lBM<$+E!C_uc%Zr(ycv zUi;X#^Y&#=8a?973oadZljbP}pYGG@Yo!Ar$HyIlA>7{_s&i{Q7>kx3>>l0xa1;x$P`yRO`e1QA6ub8*K2v6aX!T zu}9sz<%PCIU)@N5-?jLYyB(Je8>T(@t^Ue6u}{hqo?kGFY1PH_@7?5f{C2VDUQV0r`_->TQz9{`Q3OpJx93v=7WFP+piXN-G;uyw~>ohLreZ`wTWSDSCV@`Ka;&}QzT 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 0cec0c1c1d82c5b2c5bc0fe5b7afaf9f9073337f Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Oct 2023 13:41:58 -0400 Subject: [PATCH 041/274] Fixup layout --- crates/editor/src/editor.rs | 19 ++++++++++++++++--- crates/theme/src/theme.rs | 4 +++- styles/src/style_tree/editor.ts | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 06482dbbc6..3fc47f48e9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1240,6 +1240,9 @@ impl CompletionsMenu { ) .map(|task| task.detach()); }) + .constrained() + .with_min_width(style.autocomplete.completion_min_width) + .with_max_width(style.autocomplete.completion_max_width) .into_any(), ); } @@ -1250,7 +1253,7 @@ impl CompletionsMenu { enum MultiLineDocumentation {} Flex::row() - .with_child(list) + .with_child(list.flex(1., false)) .with_children({ let mat = &self.matches[selected_item]; let completions = self.completions.read(); @@ -1263,7 +1266,12 @@ impl CompletionsMenu { .scrollable::(0, None, cx) .with_child( Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), - ), + ) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), ), Some(Documentation::MultiLineMarkdown(parsed)) => Some( @@ -1271,7 +1279,12 @@ impl CompletionsMenu { .scrollable::(0, None, cx) .with_child(render_parsed_markdown::( parsed, &style, cx, - )), + )) + .contained() + .with_style(style.autocomplete.alongside_docs_container) + .constrained() + .with_max_width(style.autocomplete.alongside_docs_max_width) + .flex(1., false), ), _ => None, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9f7530ec18..f335444b58 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -867,10 +867,12 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, + pub completion_min_width: f32, + pub completion_max_width: f32, pub inline_docs_container: ContainerStyle, pub inline_docs_color: Color, pub inline_docs_size_percent: f32, - pub alongside_docs_width: f32, + pub alongside_docs_max_width: f32, pub alongside_docs_container: ContainerStyle, } diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index e7717583a8..27a6eaf195 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -206,10 +206,12 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, + completion_min_width: 300, + completion_max_width: 700, inline_docs_container: { padding: { left: 40 } }, inline_docs_color: text(theme.middle, "sans", "disabled", {}).color, inline_docs_size_percent: 0.75, - alongside_docs_width: 700, + alongside_docs_max_width: 700, alongside_docs_container: { padding: autocomplete_item.padding } }, diagnostic_header: { From a09ee3a41b2a95f2dc016b588970e9b97537e7b8 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Oct 2023 14:39:34 -0400 Subject: [PATCH 042/274] Fire markdown link on mouse down Previously any amount of mouse movement would disqualify the mouse down and up from being a click, being a drag instead, which is a long standing UX issue. We can get away with just firing on mouse down here for now --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3fc47f48e9..9c1e0b3c18 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -154,7 +154,7 @@ pub fn render_parsed_markdown( }); cx.scene().push_mouse_region( MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) - .on_click::(MouseButton::Left, move |_, _, cx| { + .on_down::(MouseButton::Left, move |_, _, cx| { cx.platform().open_url(&url) }), ); From f6d0934b5d87af4c2e36a14884f498430e14a980 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 15:17:46 -0600 Subject: [PATCH 043/274] 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 044/274] 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 4688a94a54501d4604b8aad6de77aecc8d556c7d Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 12:11:27 -0400 Subject: [PATCH 045/274] Allow file links in markdown & filter links a bit aggressively --- crates/editor/src/editor.rs | 38 ++++++++++++++++----- crates/editor/src/element.rs | 10 ++++-- crates/editor/src/hover_popover.rs | 8 +++-- crates/language/src/markdown.rs | 41 ++++++++++++++++++----- crates/terminal_view/src/terminal_view.rs | 7 ++++ 5 files changed, 81 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c1e0b3c18..0748a0fcf4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -122,6 +122,7 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> Text { enum RenderedMarkdown {} @@ -147,15 +148,22 @@ pub fn render_parsed_markdown( region_id += 1; let region = parsed.regions[ix].clone(); - if let Some(url) = region.link_url { + if let Some(link) = region.link { cx.scene().push_cursor_region(CursorRegion { bounds, style: CursorStyle::PointingHand, }); cx.scene().push_mouse_region( MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) - .on_down::(MouseButton::Left, move |_, _, cx| { - cx.platform().open_url(&url) + .on_down::(MouseButton::Left, move |_, _, cx| match &link { + markdown::Link::Web { url } => cx.platform().open_url(url), + markdown::Link::Path { path } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } }), ); } @@ -916,10 +924,11 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), } } @@ -1105,7 +1114,12 @@ impl CompletionsMenu { !self.matches.is_empty() } - fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { + fn render( + &self, + style: EditorStyle, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { enum CompletionTag {} let widest_completion_ix = self @@ -1278,7 +1292,7 @@ impl CompletionsMenu { Flex::column() .scrollable::(0, None, cx) .with_child(render_parsed_markdown::( - parsed, &style, cx, + parsed, &style, workspace, cx, )) .contained() .with_style(style.autocomplete.alongside_docs_container) @@ -3140,6 +3154,7 @@ impl Editor { false }); } + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset); @@ -4215,9 +4230,14 @@ impl Editor { style: EditorStyle, cx: &mut ViewContext, ) -> Option<(DisplayPoint, AnyElement)> { - self.context_menu - .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) + self.context_menu.as_ref().map(|menu| { + menu.render( + cursor_position, + style, + self.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }) } fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 924d66c21c..316e143413 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2439,9 +2439,13 @@ impl Element for EditorElement { } let visible_rows = start_row..start_row + line_layouts.len() as u32; - let mut hover = editor - .hover_state - .render(&snapshot, &style, visible_rows, cx); + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); let mode = editor.mode; let mut fold_indicators = editor.render_fold_indicators( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e8901ad6c1..00a307df68 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -9,7 +9,7 @@ use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle, }; use language::{ markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown, @@ -17,6 +17,7 @@ use language::{ use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; +use workspace::Workspace; pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; @@ -422,6 +423,7 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec>)> { // If there is a diagnostic, position the popovers based on that. @@ -451,7 +453,7 @@ impl HoverState { elements.push(diagnostic_popover.render(style, cx)); } if let Some(info_popover) = self.info_popover.as_mut() { - elements.push(info_popover.render(style, cx)); + elements.push(info_popover.render(style, workspace, cx)); } Some((point, elements)) @@ -470,6 +472,7 @@ impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, + workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { MouseEventHandler::new::(0, cx, |_, cx| { @@ -478,6 +481,7 @@ impl InfoPopover { .with_child(crate::render_parsed_markdown::( &self.parsed_content, style, + workspace, cx, )) .contained() diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index 8be15e81f6..7f57eba309 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -1,5 +1,5 @@ -use std::ops::Range; use std::sync::Arc; +use std::{ops::Range, path::PathBuf}; use crate::{HighlightId, Language, LanguageRegistry}; use gpui::fonts::{self, HighlightStyle, Weight}; @@ -58,7 +58,28 @@ pub struct MarkdownHighlightStyle { #[derive(Debug, Clone)] pub struct ParsedRegion { pub code: bool, - pub link_url: Option, + pub link: Option, +} + +#[derive(Debug, Clone)] +pub enum Link { + Web { url: String }, + Path { path: PathBuf }, +} + +impl Link { + fn identify(text: String) -> Option { + if text.starts_with("http") { + return Some(Link::Web { url: text }); + } + + let path = PathBuf::from(text); + if path.is_absolute() { + return Some(Link::Path { path }); + } + + None + } } pub async fn parse_markdown( @@ -115,17 +136,20 @@ pub async fn parse_markdown_block( text.push_str(t.as_ref()); let mut style = MarkdownHighlightStyle::default(); + if bold_depth > 0 { style.weight = Weight::BOLD; } + if italic_depth > 0 { style.italic = true; } - if let Some(link_url) = link_url.clone() { + + if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { - link_url: Some(link_url), code: false, + link: Some(link), }); style.underline = true; } @@ -151,7 +175,9 @@ pub async fn parse_markdown_block( Event::Code(t) => { text.push_str(t.as_ref()); region_ranges.push(prev_len..text.len()); - if link_url.is_some() { + + let link = link_url.clone().and_then(|u| Link::identify(u)); + if link.is_some() { highlights.push(( prev_len..text.len(), MarkdownHighlight::Style(MarkdownHighlightStyle { @@ -160,10 +186,7 @@ pub async fn parse_markdown_block( }), )); } - regions.push(ParsedRegion { - code: true, - link_url: link_url.clone(), - }); + regions.push(ParsedRegion { code: true, link }); } Event::Start(tag) => match tag { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cd939b5604..5a13efd07a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -150,11 +150,14 @@ impl TerminalView { cx.notify(); cx.emit(Event::Wakeup); } + Event::Bell => { this.has_bell = true; cx.emit(Event::Wakeup); } + Event::BlinkChanged => this.blinking_on = !this.blinking_on, + Event::TitleChanged => { if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { let cwd = foreground_info.cwd.clone(); @@ -171,6 +174,7 @@ impl TerminalView { .detach(); } } + Event::NewNavigationTarget(maybe_navigation_target) => { this.can_navigate_to_selected_word = match maybe_navigation_target { Some(MaybeNavigationTarget::Url(_)) => true, @@ -180,8 +184,10 @@ impl TerminalView { None => false, } } + Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.platform().open_url(url), + MaybeNavigationTarget::PathLike(maybe_path) => { if !this.can_navigate_to_selected_word { return; @@ -246,6 +252,7 @@ impl TerminalView { } } }, + _ => cx.emit(event.clone()), }) .detach(); From 85332eacbd861847582526cec0642f2d76e88944 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 13:23:26 -0400 Subject: [PATCH 046/274] Race completion filter w/completion request & make not block UI --- crates/editor/src/editor.rs | 107 +++++++++++++++++++----------- crates/editor/src/editor_tests.rs | 14 ++-- crates/editor/src/element.rs | 2 +- 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0748a0fcf4..d7ef82da36 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -656,7 +656,7 @@ pub struct Editor { background_highlights: BTreeMap, inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, - context_menu: Option, + context_menu: RwLock>, mouse_context_menu: ViewHandle, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, @@ -934,12 +934,13 @@ impl ContextMenu { } } +#[derive(Clone)] struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, completions: Arc>>, - match_candidates: Vec, + match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, list: UniformListState, @@ -1333,13 +1334,13 @@ impl CompletionsMenu { .collect() }; - //Remove all candidates where the query's start does not match the start of any word in the candidate + // Remove all candidates where the query's start does not match the start of any word in the candidate if let Some(query) = query { if let Some(query_start) = query.chars().next() { matches.retain(|string_match| { split_words(&string_match.string).any(|word| { - //Check that the first codepoint of the word as lowercase matches the first - //codepoint of the query as lowercase + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase word.chars() .flat_map(|codepoint| codepoint.to_lowercase()) .zip(query_start.to_lowercase()) @@ -1805,7 +1806,7 @@ impl Editor { background_highlights: Default::default(), inlay_background_highlights: Default::default(), nav_history: None, - context_menu: None, + context_menu: RwLock::new(None), mouse_context_menu: cx .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)), completion_tasks: Default::default(), @@ -2100,10 +2101,12 @@ impl Editor { if local { let new_cursor_position = self.selections.newest_anchor().head(); - let completion_menu = match self.context_menu.as_mut() { + let mut context_menu = self.context_menu.write(); + let completion_menu = match context_menu.as_ref() { Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { - self.context_menu.take(); + *context_menu = None; None } }; @@ -2115,13 +2118,39 @@ impl Editor { if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) { + let mut completion_menu = completion_menu.clone(); + drop(context_menu); + let query = Self::completion_query(buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + cx.spawn(move |this, mut cx| async move { + completion_menu + .filter(query.as_deref(), cx.background().clone()) + .await; + + this.update(&mut cx, |this, cx| { + let mut context_menu = this.context_menu.write(); + let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else { + return; + }; + + if menu.id > completion_menu.id { + return; + } + + *context_menu = Some(ContextMenu::Completions(completion_menu)); + drop(context_menu); + cx.notify(); + }) + }) + .detach(); + self.show_completions(&ShowCompletions, cx); } else { + drop(context_menu); self.hide_context_menu(cx); } + } else { + drop(context_menu); } hide_hover(self, cx); @@ -3432,23 +3461,31 @@ impl Editor { this.update(&mut cx, |this, cx| { this.completion_tasks.retain(|(task_id, _)| *task_id > id); - match this.context_menu.as_ref() { + let mut context_menu = this.context_menu.write(); + match context_menu.as_ref() { None => {} + Some(ContextMenu::Completions(prev_menu)) => { if prev_menu.id > id { return; } } + _ => return, } if this.focused && menu.is_some() { let menu = menu.unwrap(); - this.show_context_menu(ContextMenu::Completions(menu), cx); + *context_menu = Some(ContextMenu::Completions(menu)); + drop(context_menu); + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + cx.notify(); } else if this.completion_tasks.is_empty() { // If there are no more completion tasks and the last menu was // empty, we should hide it. If it was already hidden, we should // also show the copilot suggestion when available. + drop(context_menu); if this.hide_context_menu(cx).is_none() { this.update_visible_copilot_suggestion(cx); } @@ -3593,14 +3630,13 @@ impl Editor { } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { - if matches!( - self.context_menu.as_ref(), - Some(ContextMenu::CodeActions(_)) - ) { - self.context_menu.take(); + let mut context_menu = self.context_menu.write(); + if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { + *context_menu = None; cx.notify(); return; } + drop(context_menu); let deployed_from_indicator = action.deployed_from_indicator; let mut task = self.code_actions_task.take(); @@ -3613,16 +3649,16 @@ impl Editor { this.update(&mut cx, |this, cx| { if this.focused { if let Some((buffer, actions)) = this.available_code_actions.clone() { - this.show_context_menu( - ContextMenu::CodeActions(CodeActionsMenu { + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + *this.context_menu.write() = + Some(ContextMenu::CodeActions(CodeActionsMenu { buffer, actions, selected_item: Default::default(), list: Default::default(), deployed_from_indicator, - }), - cx, - ); + })); } } })?; @@ -4086,7 +4122,7 @@ impl Editor { let selection = self.selections.newest_anchor(); let cursor = selection.head(); - if self.context_menu.is_some() + if self.context_menu.read().is_some() || !self.completion_tasks.is_empty() || selection.start != selection.end { @@ -4220,6 +4256,7 @@ impl Editor { pub fn context_menu_visible(&self) -> bool { self.context_menu + .read() .as_ref() .map_or(false, |menu| menu.visible()) } @@ -4230,7 +4267,7 @@ impl Editor { style: EditorStyle, cx: &mut ViewContext, ) -> Option<(DisplayPoint, AnyElement)> { - self.context_menu.as_ref().map(|menu| { + self.context_menu.read().as_ref().map(|menu| { menu.render( cursor_position, style, @@ -4240,19 +4277,10 @@ impl Editor { }) } - fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { - if !matches!(menu, ContextMenu::Completions(_)) { - self.completion_tasks.clear(); - } - self.context_menu = Some(menu); - self.discard_copilot_suggestion(cx); - cx.notify(); - } - fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); self.completion_tasks.clear(); - let context_menu = self.context_menu.take(); + let context_menu = self.context_menu.write().take(); if context_menu.is_some() { self.update_visible_copilot_suggestion(cx); } @@ -5604,6 +5632,7 @@ impl Editor { if self .context_menu + .write() .as_mut() .map(|menu| menu.select_last(self.project.as_ref(), cx)) .unwrap_or(false) @@ -5648,25 +5677,25 @@ impl Editor { } pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_first(self.project.as_ref(), cx); } } pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_prev(self.project.as_ref(), cx); } } pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_next(self.project.as_ref(), cx); } } pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - if let Some(context_menu) = self.context_menu.as_mut() { + if let Some(context_menu) = self.context_menu.write().as_mut() { context_menu.select_last(self.project.as_ref(), cx); } } @@ -9164,7 +9193,7 @@ impl View for Editor { keymap.add_identifier("renaming"); } if self.context_menu_visible() { - match self.context_menu.as_ref() { + match self.context_menu.read().as_ref() { Some(ContextMenu::Completions(_)) => { keymap.add_identifier("menu"); keymap.add_identifier("showing_completions") diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dee27e0121..4be29ea084 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5430,9 +5430,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { additional edit "}); cx.simulate_keystroke(" "); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.simulate_keystroke("s"); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.assert_editor_state(indoc! {" one.second_completion @@ -5494,12 +5494,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { }); cx.set_state("editorˇ"); cx.simulate_keystroke("."); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.simulate_keystroke("c"); cx.simulate_keystroke("l"); cx.simulate_keystroke("o"); cx.assert_editor_state("editor.cloˇ"); - assert!(cx.editor(|e, _| e.context_menu.is_none())); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); cx.update_editor(|editor, cx| { editor.show_completions(&ShowCompletions, cx); }); @@ -7788,7 +7788,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("-"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-red", "bg-blue", "bg-yellow"] @@ -7801,7 +7801,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("l"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-blue", "bg-yellow"] @@ -7817,7 +7817,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui: cx.simulate_keystroke("l"); cx.foreground().run_until_parked(); cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { assert_eq!( menu.matches.iter().map(|m| &m.string).collect::>(), &["bg-yellow"] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 316e143413..00c8508b6c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2428,7 +2428,7 @@ impl Element for EditorElement { } let active = matches!( - editor.context_menu, + editor.context_menu.read().as_ref(), Some(crate::ContextMenu::CodeActions(_)) ); From 540436a1f9892b9f3c6aafbf21d525335880d8bc Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 11 Oct 2023 21:57:46 -0600 Subject: [PATCH 047/274] 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 048/274] 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 d23bb3b05da84c29ce9626f4d7a68461f2e19c93 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 16:18:54 -0400 Subject: [PATCH 049/274] Unbork markdown parse test by making links match --- crates/editor/src/hover_popover.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 00a307df68..5b3985edf9 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -911,7 +911,7 @@ mod tests { // Links Row { blocks: vec![HoverBlock { - text: "one [two](the-url) three".to_string(), + text: "one [two](https://the-url) three".to_string(), kind: HoverBlockKind::Markdown, }], expected_marked_text: "one «two» three".to_string(), @@ -932,7 +932,7 @@ mod tests { - a - b * two - - [c](the-url) + - [c](https://the-url) - d" .unindent(), kind: HoverBlockKind::Markdown, From c4fc9f7ed81e6f0199c24ed99a60a64dd1eb98cb Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 19:28:17 -0400 Subject: [PATCH 050/274] Eagerly attempt to resolve missing completion documentation --- crates/editor/src/editor.rs | 264 ++++++++++++++++++++++++++---------- 1 file changed, 191 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d7ef82da36..bdacf0be38 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; -use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings}; +use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -62,8 +62,8 @@ use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind, - IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, - SelectionGoal, TransactionId, + IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, + Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, @@ -954,7 +954,7 @@ impl CompletionsMenu { ) { self.selected_item = 0; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -967,7 +967,7 @@ impl CompletionsMenu { self.selected_item -= 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -980,7 +980,7 @@ impl CompletionsMenu { self.selected_item += 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); } - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -991,16 +991,99 @@ impl CompletionsMenu { ) { self.selected_item = self.matches.len() - 1; self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - self.attempt_resolve_selected_completion(project, cx); + self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } - fn attempt_resolve_selected_completion( + fn pre_resolve_completion_documentation( + &self, + project: Option>, + cx: &mut ViewContext, + ) { + let Some(project) = project else { + return; + }; + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); + + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); + + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + + cx.spawn(move |this, mut cx| async move { + if is_remote { + let Some(project_id) = project_id else { + log::error!("Remote project without remote_id"); + return; + }; + + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = project.read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }); + let Some(server) = server else { + return; + }; + + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } + }) + .detach(); + } + + fn attempt_resolve_selected_completion_documentation( &mut self, project: Option<&ModelHandle>, cx: &mut ViewContext, ) { - let index = self.matches[self.selected_item].candidate_id; + let completion_index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { return; }; @@ -1008,7 +1091,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completions_guard = completions.read(); - let completion = &completions_guard[index]; + let completion = &completions_guard[completion_index]; if completion.documentation.is_some() { return; } @@ -1024,54 +1107,95 @@ impl CompletionsMenu { }; let client = project.read(cx).client(); - let request = proto::ResolveCompletionDocumentation { - project_id, - language_server_id: server_id.0 as u64, - lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), - }; - cx.spawn(|this, mut cx| async move { - let Some(response) = client - .request(request) - .await - .context("completion documentation resolve proto request") - .log_err() - else { - return; - }; - - if response.text.is_empty() { - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(Documentation::Undocumented); - } - - let documentation = if response.is_markdown { - Documentation::MultiLineMarkdown( - markdown::parse_markdown(&response.text, &language_registry, None).await, - ) - } else if response.text.lines().count() <= 1 { - Documentation::SingleLine(response.text) - } else { - Documentation::MultiLinePlainText(response.text) - }; - - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(documentation); - drop(completions); + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client, + language_registry.clone(), + ) + .await; _ = this.update(&mut cx, |_, cx| cx.notify()); }) .detach(); + } else { + let Some(server) = project.read(cx).language_server_for_id(server_id) else { + return; + }; - return; + cx.spawn(move |this, mut cx| async move { + Self::resolve_completion_documentation_local( + server, + completions, + completion_index, + completion, + language_registry, + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + }) + .detach(); } + } - let Some(server) = project.read(cx).language_server_for_id(server_id) else { + async fn resolve_completion_documentation_remote( + project_id: u64, + server_id: LanguageServerId, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + client: Arc, + language_registry: Arc, + ) { + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { return; }; + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } + + async fn resolve_completion_documentation_local( + server: Arc, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { let can_resolve = server .capabilities() .completion_provider @@ -1082,33 +1206,27 @@ impl CompletionsMenu { return; } - cx.spawn(|this, mut cx| async move { - let request = server.request::(completion); - let Some(completion_item) = request.await.log_err() else { - return; - }; + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; - if let Some(lsp_documentation) = completion_item.documentation { - let documentation = language::prepare_completion_documentation( - &lsp_documentation, - &language_registry, - None, // TODO: Try to reasonably work out which language the completion is for - ) - .await; + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(documentation); - drop(completions); - - _ = this.update(&mut cx, |_, cx| cx.notify()); - } else { - let mut completions = completions.write(); - let completion = &mut completions[index]; - completion.documentation = Some(Documentation::Undocumented); - } - }) - .detach(); + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } } fn visible(&self) -> bool { @@ -3450,7 +3568,7 @@ impl Editor { None } else { _ = this.update(&mut cx, |editor, cx| { - menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx); + menu.pre_resolve_completion_documentation(editor.project.clone(), cx); }); Some(menu) } From cf6ce0dbadf971d9366e418415992907e4b871e5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Oct 2023 14:16:32 -0700 Subject: [PATCH 051/274] Start work on storing notifications in the database --- Cargo.lock | 23 +++ Cargo.toml | 1 + .../20221109000000_test_schema.sql | 19 +++ .../20231004130100_create_notifications.sql | 18 +++ crates/collab/src/db.rs | 2 +- crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries.rs | 1 + crates/collab/src/db/queries/access_tokens.rs | 1 + crates/collab/src/db/queries/notifications.rs | 140 ++++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/notification.rs | 29 ++++ .../collab/src/db/tables/notification_kind.rs | 14 ++ crates/rpc/Cargo.toml | 1 + crates/rpc/proto/zed.proto | 41 ++++- crates/rpc/src/notification.rs | 105 +++++++++++++ crates/rpc/src/rpc.rs | 3 + 16 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 crates/collab/migrations/20231004130100_create_notifications.sql create mode 100644 crates/collab/src/db/queries/notifications.rs create mode 100644 crates/collab/src/db/tables/notification.rs create mode 100644 crates/collab/src/db/tables/notification_kind.rs create mode 100644 crates/rpc/src/notification.rs diff --git a/Cargo.lock b/Cargo.lock index 01153ca0f8..a426a6a1ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6403,6 +6403,7 @@ dependencies = [ "serde_derive", "smol", "smol-timeout", + "strum", "tempdir", "tracing", "util", @@ -6623,6 +6624,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "rustybuzz" version = "0.3.0" @@ -7698,6 +7705,22 @@ name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] [[package]] name = "subtle" diff --git a/Cargo.toml b/Cargo.toml index 532610efd6..adb7fedb26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } smallvec = { version = "1.6", features = ["union"] } smol = { version = "1.2" } +strum = { version = "0.25.0", features = ["derive"] } sysinfo = "0.29.10" tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 5a84bfd796..0e811d8455 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -312,3 +312,22 @@ CREATE TABLE IF NOT EXISTS "observed_channel_messages" ( ); CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id"); + +CREATE TABLE "notification_kinds" ( + "id" INTEGER PRIMARY KEY NOT NULL, + "name" VARCHAR NOT NULL, +); + +CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name"); + +CREATE TABLE "notifications" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "created_at" TIMESTAMP NOT NULL default now, + "recipent_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), + "is_read" BOOLEAN NOT NULL DEFAULT FALSE, + "entity_id_1" INTEGER, + "entity_id_2" INTEGER +); + +CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/migrations/20231004130100_create_notifications.sql b/crates/collab/migrations/20231004130100_create_notifications.sql new file mode 100644 index 0000000000..e0c7b290b4 --- /dev/null +++ b/crates/collab/migrations/20231004130100_create_notifications.sql @@ -0,0 +1,18 @@ +CREATE TABLE "notification_kinds" ( + "id" INTEGER PRIMARY KEY NOT NULL, + "name" VARCHAR NOT NULL, +); + +CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name"); + +CREATE TABLE notifications ( + "id" SERIAL PRIMARY KEY, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "recipent_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), + "is_read" BOOLEAN NOT NULL DEFAULT FALSE + "entity_id_1" INTEGER, + "entity_id_2" INTEGER +); + +CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e60b7cc33d..56e7c0d942 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -20,7 +20,7 @@ use rpc::{ }; use sea_orm::{ entity::prelude::*, - sea_query::{Alias, Expr, OnConflict, Query}, + sea_query::{Alias, Expr, OnConflict}, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement, TransactionTrait, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 23bb9e53bf..b5873a152f 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -80,3 +80,4 @@ id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); id_type!(FlagId); +id_type!(NotificationId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 80bd8704b2..629e26f1a9 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -5,6 +5,7 @@ pub mod buffers; pub mod channels; pub mod contacts; pub mod messages; +pub mod notifications; pub mod projects; pub mod rooms; pub mod servers; diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs index def9428a2b..589b6483df 100644 --- a/crates/collab/src/db/queries/access_tokens.rs +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -1,4 +1,5 @@ use super::*; +use sea_orm::sea_query::Query; impl Database { pub async fn create_access_token( diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs new file mode 100644 index 0000000000..2907ad85b7 --- /dev/null +++ b/crates/collab/src/db/queries/notifications.rs @@ -0,0 +1,140 @@ +use super::*; +use rpc::{Notification, NotificationEntityKind, NotificationKind}; + +impl Database { + pub async fn ensure_notification_kinds(&self) -> Result<()> { + self.transaction(|tx| async move { + notification_kind::Entity::insert_many(NotificationKind::all().map(|kind| { + notification_kind::ActiveModel { + id: ActiveValue::Set(kind as i32), + name: ActiveValue::Set(kind.to_string()), + } + })) + .on_conflict(OnConflict::new().do_nothing().to_owned()) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_notifications( + &self, + recipient_id: UserId, + limit: usize, + ) -> Result { + self.transaction(|tx| async move { + let mut result = proto::AddNotifications::default(); + + let mut rows = notification::Entity::find() + .filter(notification::Column::RecipientId.eq(recipient_id)) + .order_by_desc(notification::Column::Id) + .limit(limit as u64) + .stream(&*tx) + .await?; + + let mut user_ids = Vec::new(); + let mut channel_ids = Vec::new(); + let mut message_ids = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + + let Some(kind) = NotificationKind::from_i32(row.kind) else { + continue; + }; + let Some(notification) = Notification::from_fields( + kind, + [ + row.entity_id_1.map(|id| id as u64), + row.entity_id_2.map(|id| id as u64), + row.entity_id_3.map(|id| id as u64), + ], + ) else { + continue; + }; + + // Gather the ids of all associated entities. + let (_, associated_entities) = notification.to_fields(); + for entity in associated_entities { + let Some((id, kind)) = entity else { + break; + }; + match kind { + NotificationEntityKind::User => &mut user_ids, + NotificationEntityKind::Channel => &mut channel_ids, + NotificationEntityKind::ChannelMessage => &mut message_ids, + } + .push(id); + } + + result.notifications.push(proto::Notification { + kind: row.kind as u32, + timestamp: row.created_at.assume_utc().unix_timestamp() as u64, + is_read: row.is_read, + entity_id_1: row.entity_id_1.map(|id| id as u64), + entity_id_2: row.entity_id_2.map(|id| id as u64), + entity_id_3: row.entity_id_3.map(|id| id as u64), + }); + } + + let users = user::Entity::find() + .filter(user::Column::Id.is_in(user_ids)) + .all(&*tx) + .await?; + let channels = channel::Entity::find() + .filter(user::Column::Id.is_in(channel_ids)) + .all(&*tx) + .await?; + let messages = channel_message::Entity::find() + .filter(user::Column::Id.is_in(message_ids)) + .all(&*tx) + .await?; + + for user in users { + result.users.push(proto::User { + id: user.id.to_proto(), + github_login: user.github_login, + avatar_url: String::new(), + }); + } + for channel in channels { + result.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + }); + } + for message in messages { + result.messages.push(proto::ChannelMessage { + id: message.id.to_proto(), + body: message.body, + timestamp: message.sent_at.assume_utc().unix_timestamp() as u64, + sender_id: message.sender_id.to_proto(), + nonce: None, + }); + } + + Ok(result) + }) + .await + } + + pub async fn create_notification( + &self, + recipient_id: UserId, + notification: Notification, + tx: &DatabaseTransaction, + ) -> Result<()> { + let (kind, associated_entities) = notification.to_fields(); + notification::ActiveModel { + recipient_id: ActiveValue::Set(recipient_id), + kind: ActiveValue::Set(kind as i32), + entity_id_1: ActiveValue::Set(associated_entities[0].map(|(id, _)| id as i32)), + entity_id_2: ActiveValue::Set(associated_entities[1].map(|(id, _)| id as i32)), + entity_id_3: ActiveValue::Set(associated_entities[2].map(|(id, _)| id as i32)), + ..Default::default() + } + .save(&*tx) + .await?; + Ok(()) + } +} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index e19391da7d..4336217b23 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -12,6 +12,8 @@ pub mod contact; pub mod feature_flag; pub mod follower; pub mod language_server; +pub mod notification; +pub mod notification_kind; pub mod observed_buffer_edits; pub mod observed_channel_messages; pub mod project; diff --git a/crates/collab/src/db/tables/notification.rs b/crates/collab/src/db/tables/notification.rs new file mode 100644 index 0000000000..6a0abe9dc6 --- /dev/null +++ b/crates/collab/src/db/tables/notification.rs @@ -0,0 +1,29 @@ +use crate::db::{NotificationId, UserId}; +use sea_orm::entity::prelude::*; +use time::PrimitiveDateTime; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "notifications")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: NotificationId, + pub recipient_id: UserId, + pub kind: i32, + pub is_read: bool, + pub created_at: PrimitiveDateTime, + pub entity_id_1: Option, + pub entity_id_2: Option, + pub entity_id_3: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::RecipientId", + to = "super::user::Column::Id" + )] + Recipient, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/notification_kind.rs b/crates/collab/src/db/tables/notification_kind.rs new file mode 100644 index 0000000000..32dfb2065a --- /dev/null +++ b/crates/collab/src/db/tables/notification_kind.rs @@ -0,0 +1,14 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "notification_kinds")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 3c307be4fb..bc750374dd 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -29,6 +29,7 @@ rsa = "0.4" serde.workspace = true serde_derive.workspace = true smol-timeout = "0.6" +strum.workspace = true tracing = { version = "0.1.34", features = ["log"] } zstd = "0.11" diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3501e70e6a..f51d11d3db 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -170,7 +170,9 @@ message Envelope { LinkChannel link_channel = 140; UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; // current max: 144 + MoveChannel move_channel = 142; + + AddNotifications add_notification = 145; // Current max } } @@ -1557,3 +1559,40 @@ message UpdateDiffBase { uint64 buffer_id = 2; optional string diff_base = 3; } + +message AddNotifications { + repeated Notification notifications = 1; + repeated User users = 2; + repeated Channel channels = 3; + repeated ChannelMessage messages = 4; +} + +message Notification { + uint32 kind = 1; + uint64 timestamp = 2; + bool is_read = 3; + optional uint64 entity_id_1 = 4; + optional uint64 entity_id_2 = 5; + optional uint64 entity_id_3 = 6; + + // oneof variant { + // ContactRequest contact_request = 3; + // ChannelInvitation channel_invitation = 4; + // ChatMessageMention chat_message_mention = 5; + // }; + + // message ContactRequest { + // uint64 requester_id = 1; + // } + + // message ChannelInvitation { + // uint64 inviter_id = 1; + // uint64 channel_id = 2; + // } + + // message ChatMessageMention { + // uint64 sender_id = 1; + // uint64 channel_id = 2; + // uint64 message_id = 3; + // } +} diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs new file mode 100644 index 0000000000..40794a11c3 --- /dev/null +++ b/crates/rpc/src/notification.rs @@ -0,0 +1,105 @@ +use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; + +// An integer indicating a type of notification. The variants' numerical +// values are stored in the database, so they should never be removed +// or changed. +#[repr(i32)] +#[derive(Copy, Clone, Debug, EnumIter, EnumString, Display)] +pub enum NotificationKind { + ContactRequest = 0, + ChannelInvitation = 1, + ChannelMessageMention = 2, +} + +pub enum Notification { + ContactRequest { + requester_id: u64, + }, + ChannelInvitation { + inviter_id: u64, + channel_id: u64, + }, + ChannelMessageMention { + sender_id: u64, + channel_id: u64, + message_id: u64, + }, +} + +#[derive(Copy, Clone)] +pub enum NotificationEntityKind { + User, + Channel, + ChannelMessage, +} + +impl Notification { + pub fn from_fields(kind: NotificationKind, entity_ids: [Option; 3]) -> Option { + use NotificationKind::*; + + Some(match kind { + ContactRequest => Self::ContactRequest { + requester_id: entity_ids[0]?, + }, + ChannelInvitation => Self::ChannelInvitation { + inviter_id: entity_ids[0]?, + channel_id: entity_ids[1]?, + }, + ChannelMessageMention => Self::ChannelMessageMention { + sender_id: entity_ids[0]?, + channel_id: entity_ids[1]?, + message_id: entity_ids[2]?, + }, + }) + } + + pub fn to_fields(&self) -> (NotificationKind, [Option<(u64, NotificationEntityKind)>; 3]) { + use NotificationKind::*; + + match self { + Self::ContactRequest { requester_id } => ( + ContactRequest, + [ + Some((*requester_id, NotificationEntityKind::User)), + None, + None, + ], + ), + + Self::ChannelInvitation { + inviter_id, + channel_id, + } => ( + ChannelInvitation, + [ + Some((*inviter_id, NotificationEntityKind::User)), + Some((*channel_id, NotificationEntityKind::User)), + None, + ], + ), + + Self::ChannelMessageMention { + sender_id, + channel_id, + message_id, + } => ( + ChannelMessageMention, + [ + Some((*sender_id, NotificationEntityKind::User)), + Some((*channel_id, NotificationEntityKind::ChannelMessage)), + Some((*message_id, NotificationEntityKind::Channel)), + ], + ), + } + } +} + +impl NotificationKind { + pub fn all() -> impl Iterator { + Self::iter() + } + + pub fn from_i32(i: i32) -> Option { + Self::iter().find(|kind| *kind as i32 == i) + } +} diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 942672b94b..539ef014bb 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -1,9 +1,12 @@ pub mod auth; mod conn; +mod notification; mod peer; pub mod proto; + pub use conn::Connection; pub use peer::*; +pub use notification::*; mod macros; pub const PROTOCOL_VERSION: u32 = 64; From 50cf25ae970decfd11b24d4bd0bba579de097708 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Oct 2023 16:43:18 -0700 Subject: [PATCH 052/274] Add notification doc comments --- crates/collab/src/db/queries/notifications.rs | 6 +++--- crates/rpc/src/notification.rs | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 2907ad85b7..67fd00e3ec 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -42,7 +42,7 @@ impl Database { let Some(kind) = NotificationKind::from_i32(row.kind) else { continue; }; - let Some(notification) = Notification::from_fields( + let Some(notification) = Notification::from_parts( kind, [ row.entity_id_1.map(|id| id as u64), @@ -54,7 +54,7 @@ impl Database { }; // Gather the ids of all associated entities. - let (_, associated_entities) = notification.to_fields(); + let (_, associated_entities) = notification.to_parts(); for entity in associated_entities { let Some((id, kind)) = entity else { break; @@ -124,7 +124,7 @@ impl Database { notification: Notification, tx: &DatabaseTransaction, ) -> Result<()> { - let (kind, associated_entities) = notification.to_fields(); + let (kind, associated_entities) = notification.to_parts(); notification::ActiveModel { recipient_id: ActiveValue::Set(recipient_id), kind: ActiveValue::Set(kind as i32), diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 40794a11c3..512a4731b4 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -34,7 +34,13 @@ pub enum NotificationEntityKind { } impl Notification { - pub fn from_fields(kind: NotificationKind, entity_ids: [Option; 3]) -> Option { + /// Load this notification from its generic representation, which is + /// used to represent it in the database, and in the wire protocol. + /// + /// The order in which a given notification type's fields are listed must + /// match the order they're listed in the `to_parts` method, and it must + /// not change, because they're stored in that order in the database. + pub fn from_parts(kind: NotificationKind, entity_ids: [Option; 3]) -> Option { use NotificationKind::*; Some(match kind { @@ -53,7 +59,17 @@ impl Notification { }) } - pub fn to_fields(&self) -> (NotificationKind, [Option<(u64, NotificationEntityKind)>; 3]) { + /// Convert this notification into its generic representation, which is + /// used to represent it in the database, and in the wire protocol. + /// + /// The order in which a given notification type's fields are listed must + /// match the order they're listed in the `from_parts` method, and it must + /// not change, because they're stored in that order in the database. + /// + /// Along with each field, provide the kind of entity that the field refers + /// to. This is used to load the associated entities for a batch of + /// notifications from the database. + pub fn to_parts(&self) -> (NotificationKind, [Option<(u64, NotificationEntityKind)>; 3]) { use NotificationKind::*; match self { From d1756b621f62c7541cffc86f632fb305e2ab2228 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 6 Oct 2023 12:56:18 -0700 Subject: [PATCH 053/274] Start work on notification panel --- Cargo.lock | 22 + Cargo.toml | 1 + assets/icons/bell.svg | 3 + assets/settings/default.json | 8 + crates/channel/src/channel_chat.rs | 41 +- crates/channel/src/channel_store.rs | 29 +- .../20221109000000_test_schema.sql | 3 +- .../20231004130100_create_notifications.sql | 9 +- crates/collab/src/db/queries/contacts.rs | 15 +- crates/collab/src/db/queries/notifications.rs | 96 +--- crates/collab/src/rpc.rs | 17 +- crates/collab_ui/Cargo.toml | 2 + crates/collab_ui/src/chat_panel.rs | 69 +-- crates/collab_ui/src/collab_ui.rs | 66 ++- crates/collab_ui/src/notification_panel.rs | 427 ++++++++++++++++++ crates/collab_ui/src/panel_settings.rs | 23 +- crates/notifications/Cargo.toml | 42 ++ .../notifications/src/notification_store.rs | 256 +++++++++++ crates/rpc/proto/zed.proto | 44 +- crates/rpc/src/notification.rs | 57 +-- crates/rpc/src/proto.rs | 3 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 27 +- 24 files changed, 1021 insertions(+), 241 deletions(-) create mode 100644 assets/icons/bell.svg create mode 100644 crates/collab_ui/src/notification_panel.rs create mode 100644 crates/notifications/Cargo.toml create mode 100644 crates/notifications/src/notification_store.rs diff --git a/Cargo.lock b/Cargo.lock index a426a6a1ca..e43cc8b5eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1559,6 +1559,7 @@ dependencies = [ "language", "log", "menu", + "notifications", "picker", "postage", "project", @@ -4727,6 +4728,26 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notifications" +version = "0.1.0" +dependencies = [ + "anyhow", + "channel", + "client", + "clock", + "collections", + "db", + "feature_flags", + "gpui", + "rpc", + "settings", + "sum_tree", + "text", + "time", + "util", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -10123,6 +10144,7 @@ dependencies = [ "log", "lsp", "node_runtime", + "notifications", "num_cpus", "outline", "parking_lot 0.11.2", diff --git a/Cargo.toml b/Cargo.toml index adb7fedb26..ca4a308bae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "crates/media", "crates/menu", "crates/node_runtime", + "crates/notifications", "crates/outline", "crates/picker", "crates/plugin", diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg new file mode 100644 index 0000000000..46b01b6b38 --- /dev/null +++ b/assets/icons/bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index 1611d80e2f..bab114b2f0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -139,6 +139,14 @@ // Default width of the channels panel. "default_width": 240 }, + "notification_panel": { + // Whether to show the collaboration panel button in the status bar. + "button": true, + // Where to dock channels panel. Can be 'left' or 'right'. + "dock": "right", + // Default width of the channels panel. + "default_width": 240 + }, "assistant": { // Whether to show the assistant panel button in the status bar. "button": true, diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 734182886b..5c4e0f88f6 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -451,22 +451,7 @@ async fn messages_from_proto( user_store: &ModelHandle, cx: &mut AsyncAppContext, ) -> Result> { - let unique_user_ids = proto_messages - .iter() - .map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - user_store - .update(cx, |user_store, cx| { - user_store.get_users(unique_user_ids, cx) - }) - .await?; - - let mut messages = Vec::with_capacity(proto_messages.len()); - for message in proto_messages { - messages.push(ChannelMessage::from_proto(message, user_store, cx).await?); - } + let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?; let mut result = SumTree::new(); result.extend(messages, &()); Ok(result) @@ -498,6 +483,30 @@ impl ChannelMessage { pub fn is_pending(&self) -> bool { matches!(self.id, ChannelMessageId::Pending(_)) } + + pub async fn from_proto_vec( + proto_messages: Vec, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> Result> { + let unique_user_ids = proto_messages + .iter() + .map(|m| m.sender_id) + .collect::>() + .into_iter() + .collect(); + user_store + .update(cx, |user_store, cx| { + user_store.get_users(unique_user_ids, cx) + }) + .await?; + + let mut messages = Vec::with_capacity(proto_messages.len()); + for message in proto_messages { + messages.push(ChannelMessage::from_proto(message, user_store, cx).await?); + } + Ok(messages) + } } impl sum_tree::Item for ChannelMessage { diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index bceb2c094d..4a1292cdb2 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,6 +1,6 @@ mod channel_index; -use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat}; +use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; use anyhow::{anyhow, Result}; use channel_index::ChannelIndex; use client::{Client, Subscription, User, UserId, UserStore}; @@ -248,6 +248,33 @@ impl ChannelStore { ) } + pub fn fetch_channel_messages( + &self, + message_ids: Vec, + cx: &mut ModelContext, + ) -> Task>> { + let request = if message_ids.is_empty() { + None + } else { + Some( + self.client + .request(proto::GetChannelMessagesById { message_ids }), + ) + }; + cx.spawn_weak(|this, mut cx| async move { + if let Some(request) = request { + let response = request.await?; + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("channel store dropped"))?; + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await + } else { + Ok(Vec::new()) + } + }) + } + pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option { self.channel_index .by_id() diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 0e811d8455..70c913dc95 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -327,7 +327,8 @@ CREATE TABLE "notifications" ( "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "entity_id_1" INTEGER, - "entity_id_2" INTEGER + "entity_id_2" INTEGER, + "entity_id_3" INTEGER ); CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/migrations/20231004130100_create_notifications.sql b/crates/collab/migrations/20231004130100_create_notifications.sql index e0c7b290b4..cac3f2d8df 100644 --- a/crates/collab/migrations/20231004130100_create_notifications.sql +++ b/crates/collab/migrations/20231004130100_create_notifications.sql @@ -1,6 +1,6 @@ CREATE TABLE "notification_kinds" ( "id" INTEGER PRIMARY KEY NOT NULL, - "name" VARCHAR NOT NULL, + "name" VARCHAR NOT NULL ); CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name"); @@ -8,11 +8,12 @@ CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ( CREATE TABLE notifications ( "id" SERIAL PRIMARY KEY, "created_at" TIMESTAMP NOT NULL DEFAULT now(), - "recipent_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "is_read" BOOLEAN NOT NULL DEFAULT FALSE + "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "entity_id_1" INTEGER, - "entity_id_2" INTEGER + "entity_id_2" INTEGER, + "entity_id_3" INTEGER ); CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index 2171f1a6bf..d922bc5ca2 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -124,7 +124,11 @@ impl Database { .await } - pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> { + pub async fn send_contact_request( + &self, + sender_id: UserId, + receiver_id: UserId, + ) -> Result { self.transaction(|tx| async move { let (id_a, id_b, a_to_b) = if sender_id < receiver_id { (sender_id, receiver_id, true) @@ -162,7 +166,14 @@ impl Database { .await?; if rows_affected == 1 { - Ok(()) + self.create_notification( + receiver_id, + rpc::Notification::ContactRequest { + requester_id: sender_id.to_proto(), + }, + &*tx, + ) + .await } else { Err(anyhow!("contact already requested"))? } diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 67fd00e3ec..293b896a50 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -1,5 +1,5 @@ use super::*; -use rpc::{Notification, NotificationEntityKind, NotificationKind}; +use rpc::{Notification, NotificationKind}; impl Database { pub async fn ensure_notification_kinds(&self) -> Result<()> { @@ -25,49 +25,16 @@ impl Database { ) -> Result { self.transaction(|tx| async move { let mut result = proto::AddNotifications::default(); - let mut rows = notification::Entity::find() .filter(notification::Column::RecipientId.eq(recipient_id)) .order_by_desc(notification::Column::Id) .limit(limit as u64) .stream(&*tx) .await?; - - let mut user_ids = Vec::new(); - let mut channel_ids = Vec::new(); - let mut message_ids = Vec::new(); while let Some(row) = rows.next().await { let row = row?; - - let Some(kind) = NotificationKind::from_i32(row.kind) else { - continue; - }; - let Some(notification) = Notification::from_parts( - kind, - [ - row.entity_id_1.map(|id| id as u64), - row.entity_id_2.map(|id| id as u64), - row.entity_id_3.map(|id| id as u64), - ], - ) else { - continue; - }; - - // Gather the ids of all associated entities. - let (_, associated_entities) = notification.to_parts(); - for entity in associated_entities { - let Some((id, kind)) = entity else { - break; - }; - match kind { - NotificationEntityKind::User => &mut user_ids, - NotificationEntityKind::Channel => &mut channel_ids, - NotificationEntityKind::ChannelMessage => &mut message_ids, - } - .push(id); - } - result.notifications.push(proto::Notification { + id: row.id.to_proto(), kind: row.kind as u32, timestamp: row.created_at.assume_utc().unix_timestamp() as u64, is_read: row.is_read, @@ -76,43 +43,7 @@ impl Database { entity_id_3: row.entity_id_3.map(|id| id as u64), }); } - - let users = user::Entity::find() - .filter(user::Column::Id.is_in(user_ids)) - .all(&*tx) - .await?; - let channels = channel::Entity::find() - .filter(user::Column::Id.is_in(channel_ids)) - .all(&*tx) - .await?; - let messages = channel_message::Entity::find() - .filter(user::Column::Id.is_in(message_ids)) - .all(&*tx) - .await?; - - for user in users { - result.users.push(proto::User { - id: user.id.to_proto(), - github_login: user.github_login, - avatar_url: String::new(), - }); - } - for channel in channels { - result.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - }); - } - for message in messages { - result.messages.push(proto::ChannelMessage { - id: message.id.to_proto(), - body: message.body, - timestamp: message.sent_at.assume_utc().unix_timestamp() as u64, - sender_id: message.sender_id.to_proto(), - nonce: None, - }); - } - + result.notifications.reverse(); Ok(result) }) .await @@ -123,18 +54,27 @@ impl Database { recipient_id: UserId, notification: Notification, tx: &DatabaseTransaction, - ) -> Result<()> { + ) -> Result { let (kind, associated_entities) = notification.to_parts(); - notification::ActiveModel { + let model = notification::ActiveModel { recipient_id: ActiveValue::Set(recipient_id), kind: ActiveValue::Set(kind as i32), - entity_id_1: ActiveValue::Set(associated_entities[0].map(|(id, _)| id as i32)), - entity_id_2: ActiveValue::Set(associated_entities[1].map(|(id, _)| id as i32)), - entity_id_3: ActiveValue::Set(associated_entities[2].map(|(id, _)| id as i32)), + entity_id_1: ActiveValue::Set(associated_entities[0].map(|id| id as i32)), + entity_id_2: ActiveValue::Set(associated_entities[1].map(|id| id as i32)), + entity_id_3: ActiveValue::Set(associated_entities[2].map(|id| id as i32)), ..Default::default() } .save(&*tx) .await?; - Ok(()) + + Ok(proto::Notification { + id: model.id.as_ref().to_proto(), + kind: *model.kind.as_ref() as u32, + timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, + is_read: false, + entity_id_1: model.entity_id_1.as_ref().map(|id| id as u64), + entity_id_2: model.entity_id_2.as_ref().map(|id| id as u64), + entity_id_3: model.entity_id_3.as_ref().map(|id| id as u64), + }) } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e5c6d94ce0..eb123cf960 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -70,6 +70,7 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; +const INITIAL_NOTIFICATION_COUNT: usize = 30; lazy_static! { static ref METRIC_CONNECTIONS: IntGauge = @@ -290,6 +291,8 @@ impl Server { let pool = self.connection_pool.clone(); let live_kit_client = self.app_state.live_kit_client.clone(); + self.app_state.db.ensure_notification_kinds().await?; + let span = info_span!("start server"); self.executor.spawn_detached( async move { @@ -578,15 +581,17 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, channels_for_user, channel_invites) = future::try_join3( + let (contacts, channels_for_user, channel_invites, notifications) = future::try_join4( this.app_state.db.get_contacts(user_id), this.app_state.db.get_channels_for_user(user_id), - this.app_state.db.get_channel_invites_for_user(user_id) + this.app_state.db.get_channel_invites_for_user(user_id), + this.app_state.db.get_notifications(user_id, INITIAL_NOTIFICATION_COUNT) ).await?; { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); + this.peer.send(connection_id, notifications)?; this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_channels_update( channels_for_user, @@ -2064,7 +2069,7 @@ async fn request_contact( return Err(anyhow!("cannot add yourself as a contact"))?; } - session + let notification = session .db() .await .send_contact_request(requester_id, responder_id) @@ -2095,6 +2100,12 @@ async fn request_contact( .user_connection_ids(responder_id) { session.peer.send(connection_id, update.clone())?; + session.peer.send( + connection_id, + proto::AddNotifications { + notifications: vec![notification.clone()], + }, + )?; } response.send(proto::Ack {})?; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 98790778c9..25f2d9f91a 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } +notifications = { path = "../notifications" } rich_text = { path = "../rich_text" } picker = { path = "../picker" } project = { path = "../project" } @@ -65,6 +66,7 @@ client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +notifications = { path = "../notifications", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 1a17b48f19..d58a406d78 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -1,4 +1,7 @@ -use crate::{channel_view::ChannelView, ChatPanelSettings}; +use crate::{ + channel_view::ChannelView, format_timestamp, is_channels_feature_enabled, render_avatar, + ChatPanelSettings, +}; use anyhow::Result; use call::ActiveCall; use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; @@ -6,15 +9,14 @@ use client::Client; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use editor::Editor; -use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; use gpui::{ actions, elements::*, platform::{CursorStyle, MouseButton}, serde_json, views::{ItemType, Select, SelectStyle}, - AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use language::{language_settings::SoftWrap, LanguageRegistry}; use menu::Confirm; @@ -675,32 +677,6 @@ impl ChatPanel { } } -fn render_avatar(avatar: Option>, theme: &Arc) -> AnyElement { - let avatar_style = theme.chat_panel.avatar; - - avatar - .map(|avatar| { - Image::from_data(avatar) - .with_style(avatar_style.image) - .aligned() - .contained() - .with_corner_radius(avatar_style.outer_corner_radius) - .constrained() - .with_width(avatar_style.outer_width) - .with_height(avatar_style.outer_width) - .into_any() - }) - .unwrap_or_else(|| { - Empty::new() - .constrained() - .with_width(avatar_style.outer_width) - .into_any() - }) - .contained() - .with_style(theme.chat_panel.avatar_container) - .into_any() -} - fn render_remove( message_id_to_remove: Option, cx: &mut ViewContext<'_, '_, ChatPanel>, @@ -810,14 +786,14 @@ impl Panel for ChatPanel { self.active = active; if active { self.acknowledge_last_message(cx); - if !is_chat_feature_enabled(cx) { + if !is_channels_feature_enabled(cx) { cx.emit(Event::Dismissed); } } } fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { - (settings::get::(cx).button && is_chat_feature_enabled(cx)) + (settings::get::(cx).button && is_channels_feature_enabled(cx)) .then(|| "icons/conversations.svg") } @@ -842,35 +818,6 @@ impl Panel for ChatPanel { } } -fn is_chat_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool { - cx.is_staff() || cx.has_flag::() -} - -fn format_timestamp( - mut timestamp: OffsetDateTime, - mut now: OffsetDateTime, - local_timezone: UtcOffset, -) -> String { - timestamp = timestamp.to_offset(local_timezone); - now = now.to_offset(local_timezone); - - let today = now.date(); - let date = timestamp.date(); - let mut hour = timestamp.hour(); - let mut part = "am"; - if hour > 12 { - hour -= 12; - part = "pm"; - } - if date == today { - format!("{:02}:{:02}{}", hour, timestamp.minute(), part) - } else if date.next_day() == Some(today) { - format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) - } else { - format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) - } -} - fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { Svg::new(svg_path) .with_color(style.color) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 57d6f7b4f6..0a22c063be 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -5,27 +5,34 @@ mod collab_titlebar_item; mod contact_notification; mod face_pile; mod incoming_call_notification; +pub mod notification_panel; mod notifications; mod panel_settings; pub mod project_shared_notification; mod sharing_status_indicator; use call::{report_call_event_for_room, ActiveCall, Room}; +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; use gpui::{ actions, + elements::{Empty, Image}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, platform::{Screen, WindowBounds, WindowKind, WindowOptions}, - AppContext, Task, + AnyElement, AppContext, Element, ImageData, Task, }; use std::{rc::Rc, sync::Arc}; +use theme::Theme; +use time::{OffsetDateTime, UtcOffset}; use util::ResultExt; use workspace::AppState; pub use collab_titlebar_item::CollabTitlebarItem; -pub use panel_settings::{ChatPanelSettings, CollaborationPanelSettings}; +pub use panel_settings::{ + ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, +}; actions!( collab, @@ -35,6 +42,7 @@ actions!( pub fn init(app_state: &Arc, cx: &mut AppContext) { settings::register::(cx); settings::register::(cx); + settings::register::(cx); vcs_menu::init(cx); collab_titlebar_item::init(cx); @@ -130,3 +138,57 @@ fn notification_window_options( screen: Some(screen), } } + +fn render_avatar(avatar: Option>, theme: &Arc) -> AnyElement { + let avatar_style = theme.chat_panel.avatar; + avatar + .map(|avatar| { + Image::from_data(avatar) + .with_style(avatar_style.image) + .aligned() + .contained() + .with_corner_radius(avatar_style.outer_corner_radius) + .constrained() + .with_width(avatar_style.outer_width) + .with_height(avatar_style.outer_width) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width(avatar_style.outer_width) + .into_any() + }) + .contained() + .with_style(theme.chat_panel.avatar_container) + .into_any() +} + +fn format_timestamp( + mut timestamp: OffsetDateTime, + mut now: OffsetDateTime, + local_timezone: UtcOffset, +) -> String { + timestamp = timestamp.to_offset(local_timezone); + now = now.to_offset(local_timezone); + + let today = now.date(); + let date = timestamp.date(); + let mut hour = timestamp.hour(); + let mut part = "am"; + if hour > 12 { + hour -= 12; + part = "pm"; + } + if date == today { + format!("{:02}:{:02}{}", hour, timestamp.minute(), part) + } else if date.next_day() == Some(today) { + format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) + } else { + format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) + } +} + +fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool { + cx.is_staff() || cx.has_flag::() +} diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs new file mode 100644 index 0000000000..a78caf5ff6 --- /dev/null +++ b/crates/collab_ui/src/notification_panel.rs @@ -0,0 +1,427 @@ +use crate::{ + format_timestamp, is_channels_feature_enabled, render_avatar, NotificationPanelSettings, +}; +use anyhow::Result; +use channel::ChannelStore; +use client::{Client, Notification, UserStore}; +use db::kvp::KEY_VALUE_STORE; +use futures::StreamExt; +use gpui::{ + actions, + elements::*, + platform::{CursorStyle, MouseButton}, + serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, + ViewContext, ViewHandle, WeakViewHandle, WindowContext, +}; +use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; +use project::Fs; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; +use std::sync::Arc; +use theme::{IconButton, Theme}; +use time::{OffsetDateTime, UtcOffset}; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; + +const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel"; + +pub struct NotificationPanel { + client: Arc, + user_store: ModelHandle, + channel_store: ModelHandle, + notification_store: ModelHandle, + fs: Arc, + width: Option, + active: bool, + notification_list: ListState, + pending_serialization: Task>, + subscriptions: Vec, + local_timezone: UtcOffset, + has_focus: bool, +} + +#[derive(Serialize, Deserialize)] +struct SerializedNotificationPanel { + width: Option, +} + +#[derive(Debug)] +pub enum Event { + DockPositionChanged, + Focus, + Dismissed, +} + +actions!(chat_panel, [ToggleFocus]); + +pub fn init(_cx: &mut AppContext) {} + +impl NotificationPanel { + pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { + let fs = workspace.app_state().fs.clone(); + let client = workspace.app_state().client.clone(); + let user_store = workspace.app_state().user_store.clone(); + + let notification_list = + ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { + this.render_notification(ix, cx) + }); + + cx.add_view(|cx| { + let mut status = client.status(); + + cx.spawn(|this, mut cx| async move { + while let Some(_) = status.next().await { + if this + .update(&mut cx, |_, cx| { + cx.notify(); + }) + .is_err() + { + break; + } + } + }) + .detach(); + + let mut this = Self { + fs, + client, + user_store, + local_timezone: cx.platform().local_timezone(), + channel_store: ChannelStore::global(cx), + notification_store: NotificationStore::global(cx), + notification_list, + pending_serialization: Task::ready(None), + has_focus: false, + subscriptions: Vec::new(), + active: false, + width: None, + }; + + let mut old_dock_position = this.position(cx); + this.subscriptions.extend([ + cx.subscribe(&this.notification_store, Self::on_notification_event), + cx.observe_global::(move |this: &mut Self, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(Event::DockPositionChanged); + } + cx.notify(); + }), + ]); + this + }) + } + + pub fn load( + workspace: WeakViewHandle, + cx: AsyncAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let serialized_panel = if let Some(panel) = cx + .background() + .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) }) + .await + .log_err() + .flatten() + { + Some(serde_json::from_str::(&panel)?) + } else { + None + }; + + workspace.update(&mut cx, |workspace, cx| { + let panel = Self::new(workspace, cx); + if let Some(serialized_panel) = serialized_panel { + panel.update(cx, |panel, cx| { + panel.width = serialized_panel.width; + cx.notify(); + }); + } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + self.pending_serialization = cx.background().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + NOTIFICATION_PANEL_KEY.into(), + serde_json::to_string(&SerializedNotificationPanel { width })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + + fn render_notification(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { + self.try_render_notification(ix, cx) + .unwrap_or_else(|| Empty::new().into_any()) + } + + fn try_render_notification( + &mut self, + ix: usize, + cx: &mut ViewContext, + ) -> Option> { + let notification_store = self.notification_store.read(cx); + let user_store = self.user_store.read(cx); + let channel_store = self.channel_store.read(cx); + let entry = notification_store.notification_at(ix).unwrap(); + let now = OffsetDateTime::now_utc(); + let timestamp = entry.timestamp; + + let icon; + let text; + let actor; + match entry.notification { + Notification::ContactRequest { requester_id } => { + actor = user_store.get_cached_user(requester_id)?; + icon = "icons/plus.svg"; + text = format!("{} wants to add you as a contact", actor.github_login); + } + Notification::ContactRequestAccepted { contact_id } => { + actor = user_store.get_cached_user(contact_id)?; + icon = "icons/plus.svg"; + text = format!("{} accepted your contact invite", actor.github_login); + } + Notification::ChannelInvitation { + inviter_id, + channel_id, + } => { + actor = user_store.get_cached_user(inviter_id)?; + let channel = channel_store.channel_for_id(channel_id)?; + + icon = "icons/hash.svg"; + text = format!( + "{} invited you to join the #{} channel", + actor.github_login, channel.name + ); + } + Notification::ChannelMessageMention { + sender_id, + channel_id, + message_id, + } => { + actor = user_store.get_cached_user(sender_id)?; + let channel = channel_store.channel_for_id(channel_id)?; + let message = notification_store.channel_message_for_id(message_id)?; + + icon = "icons/conversations.svg"; + text = format!( + "{} mentioned you in the #{} channel:\n{}", + actor.github_login, channel.name, message.body, + ); + } + } + + let theme = theme::current(cx); + let style = &theme.chat_panel.message; + + Some( + MouseEventHandler::new::(ix, cx, |state, _| { + let container = style.container.style_for(state); + + Flex::column() + .with_child( + Flex::row() + .with_child(render_avatar(actor.avatar.clone(), &theme)) + .with_child(render_icon_button(&theme.chat_panel.icon_button, icon)) + .with_child( + Label::new( + format_timestamp(timestamp, now, self.local_timezone), + style.timestamp.text.clone(), + ) + .contained() + .with_style(style.timestamp.container), + ) + .align_children_center(), + ) + .with_child(Text::new(text, style.body.clone())) + .contained() + .with_style(*container) + .into_any() + }) + .into_any(), + ) + } + + fn render_sign_in_prompt( + &self, + theme: &Arc, + cx: &mut ViewContext, + ) -> AnyElement { + enum SignInPromptLabel {} + + MouseEventHandler::new::(0, cx, |mouse_state, _| { + Label::new( + "Sign in to view your notifications".to_string(), + theme + .chat_panel + .sign_in_prompt + .style_for(mouse_state) + .clone(), + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + let client = this.client.clone(); + cx.spawn(|_, cx| async move { + client.authenticate_and_connect(true, &cx).log_err().await; + }) + .detach(); + }) + .aligned() + .into_any() + } + + fn on_notification_event( + &mut self, + _: ModelHandle, + event: &NotificationEvent, + _: &mut ViewContext, + ) { + match event { + NotificationEvent::NotificationsUpdated { + old_range, + new_count, + } => { + self.notification_list.splice(old_range.clone(), *new_count); + } + } + } +} + +impl Entity for NotificationPanel { + type Event = Event; +} + +impl View for NotificationPanel { + fn ui_name() -> &'static str { + "NotificationPanel" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = theme::current(cx); + let element = if self.client.user_id().is_some() { + List::new(self.notification_list.clone()) + .contained() + .with_style(theme.chat_panel.list) + .into_any() + } else { + self.render_sign_in_prompt(&theme, cx) + }; + element + .contained() + .with_style(theme.chat_panel.container) + .constrained() + .with_min_width(150.) + .into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = true; + } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl Panel for NotificationPanel { + fn position(&self, cx: &gpui::WindowContext) -> DockPosition { + settings::get::(cx).dock + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| settings.dock = Some(position), + ); + } + + fn size(&self, cx: &gpui::WindowContext) -> f32 { + self.width + .unwrap_or_else(|| settings::get::(cx).default_width) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + self.serialize(cx); + cx.notify(); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + self.active = active; + if active { + if !is_channels_feature_enabled(cx) { + cx.emit(Event::Dismissed); + } + } + } + + fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { + (settings::get::(cx).button && is_channels_feature_enabled(cx)) + .then(|| "icons/bell.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ( + "Notification Panel".to_string(), + Some(Box::new(ToggleFocus)), + ) + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + let count = self.notification_store.read(cx).unread_notification_count(); + if count == 0 { + None + } else { + Some(count.to_string()) + } + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, Event::DockPositionChanged) + } + + fn should_close_on_event(event: &Self::Event) -> bool { + matches!(event, Event::Dismissed) + } + + fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, Event::Focus) + } +} + +fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) +} diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index c1aa6e5e01..f8678d774e 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -18,6 +18,13 @@ pub struct ChatPanelSettings { pub default_width: f32, } +#[derive(Deserialize, Debug)] +pub struct NotificationPanelSettings { + pub button: bool, + pub dock: DockPosition, + pub default_width: f32, +} + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct PanelSettingsContent { pub button: Option, @@ -27,9 +34,7 @@ pub struct PanelSettingsContent { impl Setting for CollaborationPanelSettings { const KEY: Option<&'static str> = Some("collaboration_panel"); - type FileContent = PanelSettingsContent; - fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], @@ -41,9 +46,19 @@ impl Setting for CollaborationPanelSettings { impl Setting for ChatPanelSettings { const KEY: Option<&'static str> = Some("chat_panel"); - type FileContent = PanelSettingsContent; - + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +impl Setting for NotificationPanelSettings { + const KEY: Option<&'static str> = Some("notification_panel"); + type FileContent = PanelSettingsContent; fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml new file mode 100644 index 0000000000..1425e079d6 --- /dev/null +++ b/crates/notifications/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "notifications" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/notification_store.rs" +doctest = false + +[features] +test-support = [ + "channel/test-support", + "collections/test-support", + "gpui/test-support", + "rpc/test-support", +] + +[dependencies] +channel = { path = "../channel" } +client = { path = "../client" } +clock = { path = "../clock" } +collections = { path = "../collections" } +db = { path = "../db" } +feature_flags = { path = "../feature_flags" } +gpui = { path = "../gpui" } +rpc = { path = "../rpc" } +settings = { path = "../settings" } +sum_tree = { path = "../sum_tree" } +text = { path = "../text" } +util = { path = "../util" } + +anyhow.workspace = true +time.workspace = true + +[dev-dependencies] +client = { path = "../client", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs new file mode 100644 index 0000000000..9bfa67c76e --- /dev/null +++ b/crates/notifications/src/notification_store.rs @@ -0,0 +1,256 @@ +use anyhow::Result; +use channel::{ChannelMessage, ChannelMessageId, ChannelStore}; +use client::{Client, UserStore}; +use collections::HashMap; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; +use rpc::{proto, Notification, NotificationKind, TypedEnvelope}; +use std::{ops::Range, sync::Arc}; +use sum_tree::{Bias, SumTree}; +use time::OffsetDateTime; + +pub fn init(client: Arc, user_store: ModelHandle, cx: &mut AppContext) { + let notification_store = cx.add_model(|cx| NotificationStore::new(client, user_store, cx)); + cx.set_global(notification_store); +} + +pub struct NotificationStore { + _client: Arc, + user_store: ModelHandle, + channel_messages: HashMap, + channel_store: ModelHandle, + notifications: SumTree, + _subscriptions: Vec, +} + +pub enum NotificationEvent { + NotificationsUpdated { + old_range: Range, + new_count: usize, + }, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NotificationEntry { + pub id: u64, + pub notification: Notification, + pub timestamp: OffsetDateTime, + pub is_read: bool, +} + +#[derive(Clone, Debug, Default)] +pub struct NotificationSummary { + max_id: u64, + count: usize, + unread_count: usize, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct UnreadCount(usize); + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct NotificationId(u64); + +impl NotificationStore { + pub fn global(cx: &AppContext) -> ModelHandle { + cx.global::>().clone() + } + + pub fn new( + client: Arc, + user_store: ModelHandle, + cx: &mut ModelContext, + ) -> Self { + Self { + channel_store: ChannelStore::global(cx), + notifications: Default::default(), + channel_messages: Default::default(), + _subscriptions: vec![ + client.add_message_handler(cx.handle(), Self::handle_add_notifications) + ], + user_store, + _client: client, + } + } + + pub fn notification_count(&self) -> usize { + self.notifications.summary().count + } + + pub fn unread_notification_count(&self) -> usize { + self.notifications.summary().unread_count + } + + pub fn channel_message_for_id(&self, id: u64) -> Option<&ChannelMessage> { + self.channel_messages.get(&id) + } + + pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { + let mut cursor = self.notifications.cursor::(); + cursor.seek(&Count(ix), Bias::Right, &()); + cursor.item() + } + + async fn handle_add_notifications( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let mut user_ids = Vec::new(); + let mut message_ids = Vec::new(); + + let notifications = envelope + .payload + .notifications + .into_iter() + .filter_map(|message| { + Some(NotificationEntry { + id: message.id, + is_read: message.is_read, + timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) + .ok()?, + notification: Notification::from_parts( + NotificationKind::from_i32(message.kind as i32)?, + [ + message.entity_id_1, + message.entity_id_2, + message.entity_id_3, + ], + )?, + }) + }) + .collect::>(); + if notifications.is_empty() { + return Ok(()); + } + + for entry in ¬ifications { + match entry.notification { + Notification::ChannelInvitation { inviter_id, .. } => { + user_ids.push(inviter_id); + } + Notification::ContactRequest { requester_id } => { + user_ids.push(requester_id); + } + Notification::ContactRequestAccepted { contact_id } => { + user_ids.push(contact_id); + } + Notification::ChannelMessageMention { + sender_id, + message_id, + .. + } => { + user_ids.push(sender_id); + message_ids.push(message_id); + } + } + } + + let (user_store, channel_store) = this.read_with(&cx, |this, _| { + (this.user_store.clone(), this.channel_store.clone()) + }); + + user_store + .update(&mut cx, |store, cx| store.get_users(user_ids, cx)) + .await?; + let messages = channel_store + .update(&mut cx, |store, cx| { + store.fetch_channel_messages(message_ids, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.channel_messages + .extend(messages.into_iter().filter_map(|message| { + if let ChannelMessageId::Saved(id) = message.id { + Some((id, message)) + } else { + None + } + })); + + let mut cursor = this.notifications.cursor::<(NotificationId, Count)>(); + let mut new_notifications = SumTree::new(); + let mut old_range = 0..0; + for (i, notification) in notifications.into_iter().enumerate() { + new_notifications.append( + cursor.slice(&NotificationId(notification.id), Bias::Left, &()), + &(), + ); + + if i == 0 { + old_range.start = cursor.start().1 .0; + } + + if cursor + .item() + .map_or(true, |existing| existing.id != notification.id) + { + cursor.next(&()); + } + + new_notifications.push(notification, &()); + } + + old_range.end = cursor.start().1 .0; + let new_count = new_notifications.summary().count; + new_notifications.append(cursor.suffix(&()), &()); + drop(cursor); + + this.notifications = new_notifications; + cx.emit(NotificationEvent::NotificationsUpdated { + old_range, + new_count, + }); + }); + + Ok(()) + } +} + +impl Entity for NotificationStore { + type Event = NotificationEvent; +} + +impl sum_tree::Item for NotificationEntry { + type Summary = NotificationSummary; + + fn summary(&self) -> Self::Summary { + NotificationSummary { + max_id: self.id, + count: 1, + unread_count: if self.is_read { 0 } else { 1 }, + } + } +} + +impl sum_tree::Summary for NotificationSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.max_id = self.max_id.max(summary.max_id); + self.count += summary.count; + self.unread_count += summary.unread_count; + } +} + +impl<'a> sum_tree::Dimension<'a, NotificationSummary> for NotificationId { + fn add_summary(&mut self, summary: &NotificationSummary, _: &()) { + debug_assert!(summary.max_id > self.0); + self.0 = summary.max_id; + } +} + +impl<'a> sum_tree::Dimension<'a, NotificationSummary> for Count { + fn add_summary(&mut self, summary: &NotificationSummary, _: &()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, NotificationSummary> for UnreadCount { + fn add_summary(&mut self, summary: &NotificationSummary, _: &()) { + self.0 += summary.unread_count; + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f51d11d3db..4b5c17ae8b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -172,7 +172,8 @@ message Envelope { UnlinkChannel unlink_channel = 141; MoveChannel move_channel = 142; - AddNotifications add_notification = 145; // Current max + AddNotifications add_notifications = 145; + GetChannelMessagesById get_channel_messages_by_id = 146; // Current max } } @@ -1101,6 +1102,10 @@ message GetChannelMessagesResponse { bool done = 2; } +message GetChannelMessagesById { + repeated uint64 message_ids = 1; +} + message LinkChannel { uint64 channel_id = 1; uint64 to = 2; @@ -1562,37 +1567,14 @@ message UpdateDiffBase { message AddNotifications { repeated Notification notifications = 1; - repeated User users = 2; - repeated Channel channels = 3; - repeated ChannelMessage messages = 4; } message Notification { - uint32 kind = 1; - uint64 timestamp = 2; - bool is_read = 3; - optional uint64 entity_id_1 = 4; - optional uint64 entity_id_2 = 5; - optional uint64 entity_id_3 = 6; - - // oneof variant { - // ContactRequest contact_request = 3; - // ChannelInvitation channel_invitation = 4; - // ChatMessageMention chat_message_mention = 5; - // }; - - // message ContactRequest { - // uint64 requester_id = 1; - // } - - // message ChannelInvitation { - // uint64 inviter_id = 1; - // uint64 channel_id = 2; - // } - - // message ChatMessageMention { - // uint64 sender_id = 1; - // uint64 channel_id = 2; - // uint64 message_id = 3; - // } + uint64 id = 1; + uint32 kind = 2; + uint64 timestamp = 3; + bool is_read = 4; + optional uint64 entity_id_1 = 5; + optional uint64 entity_id_2 = 6; + optional uint64 entity_id_3 = 7; } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 512a4731b4..fc6dc54d15 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -7,14 +7,19 @@ use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; #[derive(Copy, Clone, Debug, EnumIter, EnumString, Display)] pub enum NotificationKind { ContactRequest = 0, - ChannelInvitation = 1, - ChannelMessageMention = 2, + ContactRequestAccepted = 1, + ChannelInvitation = 2, + ChannelMessageMention = 3, } +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Notification { ContactRequest { requester_id: u64, }, + ContactRequestAccepted { + contact_id: u64, + }, ChannelInvitation { inviter_id: u64, channel_id: u64, @@ -26,13 +31,6 @@ pub enum Notification { }, } -#[derive(Copy, Clone)] -pub enum NotificationEntityKind { - User, - Channel, - ChannelMessage, -} - impl Notification { /// Load this notification from its generic representation, which is /// used to represent it in the database, and in the wire protocol. @@ -42,15 +40,20 @@ impl Notification { /// not change, because they're stored in that order in the database. pub fn from_parts(kind: NotificationKind, entity_ids: [Option; 3]) -> Option { use NotificationKind::*; - Some(match kind { ContactRequest => Self::ContactRequest { requester_id: entity_ids[0]?, }, + + ContactRequestAccepted => Self::ContactRequest { + requester_id: entity_ids[0]?, + }, + ChannelInvitation => Self::ChannelInvitation { inviter_id: entity_ids[0]?, channel_id: entity_ids[1]?, }, + ChannelMessageMention => Self::ChannelMessageMention { sender_id: entity_ids[0]?, channel_id: entity_ids[1]?, @@ -65,33 +68,23 @@ impl Notification { /// The order in which a given notification type's fields are listed must /// match the order they're listed in the `from_parts` method, and it must /// not change, because they're stored in that order in the database. - /// - /// Along with each field, provide the kind of entity that the field refers - /// to. This is used to load the associated entities for a batch of - /// notifications from the database. - pub fn to_parts(&self) -> (NotificationKind, [Option<(u64, NotificationEntityKind)>; 3]) { + pub fn to_parts(&self) -> (NotificationKind, [Option; 3]) { use NotificationKind::*; - match self { - Self::ContactRequest { requester_id } => ( - ContactRequest, - [ - Some((*requester_id, NotificationEntityKind::User)), - None, - None, - ], - ), + Self::ContactRequest { requester_id } => { + (ContactRequest, [Some(*requester_id), None, None]) + } + + Self::ContactRequestAccepted { contact_id } => { + (ContactRequest, [Some(*contact_id), None, None]) + } Self::ChannelInvitation { inviter_id, channel_id, } => ( ChannelInvitation, - [ - Some((*inviter_id, NotificationEntityKind::User)), - Some((*channel_id, NotificationEntityKind::User)), - None, - ], + [Some(*inviter_id), Some(*channel_id), None], ), Self::ChannelMessageMention { @@ -100,11 +93,7 @@ impl Notification { message_id, } => ( ChannelMessageMention, - [ - Some((*sender_id, NotificationEntityKind::User)), - Some((*channel_id, NotificationEntityKind::ChannelMessage)), - Some((*message_id, NotificationEntityKind::Channel)), - ], + [Some(*sender_id), Some(*channel_id), Some(*message_id)], ), } } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0d7937f6f..4d8f60c896 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -133,6 +133,7 @@ impl fmt::Display for PeerId { messages!( (Ack, Foreground), + (AddNotifications, Foreground), (AddProjectCollaborator, Foreground), (ApplyCodeAction, Background), (ApplyCodeActionResponse, Background), @@ -166,6 +167,7 @@ messages!( (GetHoverResponse, Background), (GetChannelMessages, Background), (GetChannelMessagesResponse, Background), + (GetChannelMessagesById, Background), (SendChannelMessage, Background), (SendChannelMessageResponse, Background), (GetCompletions, Background), @@ -329,6 +331,7 @@ request_messages!( (SetChannelMemberAdmin, Ack), (SendChannelMessage, SendChannelMessageResponse), (GetChannelMessages, GetChannelMessagesResponse), + (GetChannelMessagesById, GetChannelMessagesResponse), (GetChannelMembers, GetChannelMembersResponse), (JoinChannel, JoinRoomResponse), (RemoveChannelMessage, Ack), diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4174f7d6d5..c9dab0d223 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -50,6 +50,7 @@ language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } +notifications = { path = "../notifications" } assistant = { path = "../assistant" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime",optional = true } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 16189f6c4e..52ba8247b7 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -202,6 +202,7 @@ fn main() { activity_indicator::init(cx); language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); + notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); feedback::init(cx); welcome::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4e9a34c269..8caff21c5f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -221,6 +221,13 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action( + |workspace: &mut Workspace, + _: &collab_ui::notification_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ); cx.add_action( |workspace: &mut Workspace, _: &terminal_panel::ToggleFocus, @@ -275,9 +282,8 @@ pub fn initialize_workspace( QuickActionBar::new(buffer_search_bar, workspace) }); toolbar.add_item(quick_action_bar, cx); - let diagnostic_editor_controls = cx.add_view(|_| { - diagnostics::ToolbarControls::new() - }); + let diagnostic_editor_controls = + cx.add_view(|_| diagnostics::ToolbarControls::new()); toolbar.add_item(diagnostic_editor_controls, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, cx); @@ -351,12 +357,24 @@ pub fn initialize_workspace( collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); let chat_panel = collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); - let (project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel) = futures::try_join!( + let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + workspace_handle.clone(), + cx.clone(), + ); + let ( project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel, + notification_panel, + ) = futures::try_join!( + project_panel, + terminal_panel, + assistant_panel, + channels_panel, + chat_panel, + notification_panel, )?; workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); @@ -377,6 +395,7 @@ pub fn initialize_workspace( workspace.add_panel(assistant_panel, cx); workspace.add_panel(channels_panel, cx); workspace.add_panel(chat_panel, cx); + workspace.add_panel(notification_panel, cx); if !was_deserialized && workspace From 69c65597d96925cdcf011a75bb97eb0c005e9efc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Oct 2023 17:39:15 -0700 Subject: [PATCH 054/274] Fix use statement order --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 539ef014bb..4bf90669b2 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,8 +5,8 @@ mod peer; pub mod proto; pub use conn::Connection; -pub use peer::*; pub use notification::*; +pub use peer::*; mod macros; pub const PROTOCOL_VERSION: u32 = 64; From 1e1256dbdd82dd59458f78d53dc7593a3b9760b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Oct 2023 17:39:41 -0700 Subject: [PATCH 055/274] Set RUST_LOG to info by default in zed-local script --- script/zed-local | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/zed-local b/script/zed-local index 683e31ef14..b7574a903b 100755 --- a/script/zed-local +++ b/script/zed-local @@ -55,6 +55,8 @@ let users = [ 'iamnbutler' ] +const RUST_LOG = process.env.RUST_LOG || 'info' + // If a user is specified, make sure it's first in the list const user = process.env.ZED_IMPERSONATE if (user) { @@ -81,7 +83,8 @@ setTimeout(() => { ZED_ALWAYS_ACTIVE: '1', ZED_SERVER_URL: 'http://localhost:8080', ZED_ADMIN_API_TOKEN: 'secret', - ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}` + ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`, + RUST_LOG, } }) } From fed3ffb681645b32ad8718aa721858740519ca7f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Oct 2023 14:43:36 -0700 Subject: [PATCH 056/274] Set up notification store for integration tests --- Cargo.lock | 1 + crates/collab/Cargo.toml | 1 + .../migrations.sqlite/20221109000000_test_schema.sql | 8 ++++---- crates/collab/src/tests/test_server.rs | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e43cc8b5eb..02deccb39a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1502,6 +1502,7 @@ dependencies = [ "lsp", "nanoid", "node_runtime", + "notifications", "parking_lot 0.11.2", "pretty_assertions", "project", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index b91f0e1a5f..c139da831e 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -73,6 +73,7 @@ git = { path = "../git", features = ["test-support"] } live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } node_runtime = { path = "../node_runtime" } +notifications = { path = "../notifications", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 70c913dc95..c5c556500f 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -192,7 +192,7 @@ 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 CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS "channel_chat_participants" ( @@ -315,15 +315,15 @@ CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "ob CREATE TABLE "notification_kinds" ( "id" INTEGER PRIMARY KEY NOT NULL, - "name" VARCHAR NOT NULL, + "name" VARCHAR NOT NULL ); CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ("name"); CREATE TABLE "notifications" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "created_at" TIMESTAMP NOT NULL default now, - "recipent_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "created_at" TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, + "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "entity_id_1" INTEGER, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 7397489b34..9d03d1e17e 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -231,7 +231,8 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); - channel::init(&client, user_store, cx); + channel::init(&client, user_store.clone(), cx); + notifications::init(client.clone(), user_store, cx); }); client From 324112884073afd168227a5cd1a3df3388127ac1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Oct 2023 17:17:45 -0700 Subject: [PATCH 057/274] Make notification db representation more flexible --- Cargo.lock | 1 + .../20221109000000_test_schema.sql | 9 +- .../20231004130100_create_notifications.sql | 9 +- crates/collab/src/db.rs | 9 + crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/contacts.rs | 37 +++-- crates/collab/src/db/queries/notifications.rs | 69 ++++---- crates/collab/src/db/tables/notification.rs | 11 +- .../collab/src/db/tables/notification_kind.rs | 3 +- crates/collab/src/db/tests.rs | 6 +- crates/collab/src/lib.rs | 4 +- crates/collab/src/rpc.rs | 2 - crates/collab_ui/src/notification_panel.rs | 12 +- .../notifications/src/notification_store.rs | 30 ++-- crates/rpc/Cargo.toml | 2 + crates/rpc/proto/zed.proto | 11 +- crates/rpc/src/notification.rs | 156 ++++++++---------- 17 files changed, 197 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02deccb39a..c6d7a5ef85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6423,6 +6423,7 @@ dependencies = [ "rsa 0.4.0", "serde", "serde_derive", + "serde_json", "smol", "smol-timeout", "strum", diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index c5c556500f..a10155fd1d 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -314,7 +314,7 @@ CREATE TABLE IF NOT EXISTS "observed_channel_messages" ( CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id"); CREATE TABLE "notification_kinds" ( - "id" INTEGER PRIMARY KEY NOT NULL, + "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL ); @@ -322,13 +322,12 @@ CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ( CREATE TABLE "notifications" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "created_at" TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "actor_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "is_read" BOOLEAN NOT NULL DEFAULT FALSE, - "entity_id_1" INTEGER, - "entity_id_2" INTEGER, - "entity_id_3" INTEGER + "content" TEXT ); CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/migrations/20231004130100_create_notifications.sql b/crates/collab/migrations/20231004130100_create_notifications.sql index cac3f2d8df..83cfd43978 100644 --- a/crates/collab/migrations/20231004130100_create_notifications.sql +++ b/crates/collab/migrations/20231004130100_create_notifications.sql @@ -1,5 +1,5 @@ CREATE TABLE "notification_kinds" ( - "id" INTEGER PRIMARY KEY NOT NULL, + "id" SERIAL PRIMARY KEY, "name" VARCHAR NOT NULL ); @@ -7,13 +7,12 @@ CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ( CREATE TABLE notifications ( "id" SERIAL PRIMARY KEY, + "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "actor_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "is_read" BOOLEAN NOT NULL DEFAULT FALSE, - "entity_id_1" INTEGER, - "entity_id_2" INTEGER, - "entity_id_3" INTEGER + "content" TEXT ); CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 56e7c0d942..9aea23ca84 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -55,6 +55,8 @@ pub struct Database { rooms: DashMap>>, rng: Mutex, executor: Executor, + notification_kinds_by_id: HashMap, + notification_kinds_by_name: HashMap, #[cfg(test)] runtime: Option, } @@ -69,6 +71,8 @@ impl Database { pool: sea_orm::Database::connect(options).await?, rooms: DashMap::with_capacity(16384), rng: Mutex::new(StdRng::seed_from_u64(0)), + notification_kinds_by_id: HashMap::default(), + notification_kinds_by_name: HashMap::default(), executor, #[cfg(test)] runtime: None, @@ -121,6 +125,11 @@ impl Database { Ok(new_migrations) } + pub async fn initialize_static_data(&mut self) -> Result<()> { + self.initialize_notification_enum().await?; + Ok(()) + } + pub async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index b5873a152f..bd07af8a35 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -81,3 +81,4 @@ id_type!(UserId); id_type!(ChannelBufferCollaboratorId); id_type!(FlagId); id_type!(NotificationId); +id_type!(NotificationKindId); diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index d922bc5ca2..083315e290 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -165,18 +165,18 @@ impl Database { .exec_without_returning(&*tx) .await?; - if rows_affected == 1 { - self.create_notification( - receiver_id, - rpc::Notification::ContactRequest { - requester_id: sender_id.to_proto(), - }, - &*tx, - ) - .await - } else { - Err(anyhow!("contact already requested"))? + if rows_affected == 0 { + Err(anyhow!("contact already requested"))?; } + + self.create_notification( + receiver_id, + rpc::Notification::ContactRequest { + actor_id: sender_id.to_proto(), + }, + &*tx, + ) + .await }) .await } @@ -260,7 +260,7 @@ impl Database { responder_id: UserId, requester_id: UserId, accept: bool, - ) -> Result<()> { + ) -> Result { self.transaction(|tx| async move { let (id_a, id_b, a_to_b) = if responder_id < requester_id { (responder_id, requester_id, false) @@ -298,11 +298,18 @@ impl Database { result.rows_affected }; - if rows_affected == 1 { - Ok(()) - } else { + if rows_affected == 0 { Err(anyhow!("no such contact request"))? } + + self.create_notification( + requester_id, + rpc::Notification::ContactRequestAccepted { + actor_id: responder_id.to_proto(), + }, + &*tx, + ) + .await }) .await } diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 293b896a50..8c4c511299 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -1,21 +1,25 @@ use super::*; -use rpc::{Notification, NotificationKind}; +use rpc::Notification; impl Database { - pub async fn ensure_notification_kinds(&self) -> Result<()> { - self.transaction(|tx| async move { - notification_kind::Entity::insert_many(NotificationKind::all().map(|kind| { - notification_kind::ActiveModel { - id: ActiveValue::Set(kind as i32), - name: ActiveValue::Set(kind.to_string()), - } - })) - .on_conflict(OnConflict::new().do_nothing().to_owned()) - .exec(&*tx) - .await?; - Ok(()) - }) - .await + pub async fn initialize_notification_enum(&mut self) -> Result<()> { + notification_kind::Entity::insert_many(Notification::all_kinds().iter().map(|kind| { + notification_kind::ActiveModel { + name: ActiveValue::Set(kind.to_string()), + ..Default::default() + } + })) + .on_conflict(OnConflict::new().do_nothing().to_owned()) + .exec_without_returning(&self.pool) + .await?; + + let mut rows = notification_kind::Entity::find().stream(&self.pool).await?; + while let Some(row) = rows.next().await { + let row = row?; + self.notification_kinds_by_name.insert(row.name, row.id); + } + + Ok(()) } pub async fn get_notifications( @@ -33,14 +37,16 @@ impl Database { .await?; while let Some(row) = rows.next().await { let row = row?; + let Some(kind) = self.notification_kinds_by_id.get(&row.kind) else { + continue; + }; result.notifications.push(proto::Notification { id: row.id.to_proto(), - kind: row.kind as u32, + kind: kind.to_string(), timestamp: row.created_at.assume_utc().unix_timestamp() as u64, is_read: row.is_read, - entity_id_1: row.entity_id_1.map(|id| id as u64), - entity_id_2: row.entity_id_2.map(|id| id as u64), - entity_id_3: row.entity_id_3.map(|id| id as u64), + content: row.content, + actor_id: row.actor_id.map(|id| id.to_proto()), }); } result.notifications.reverse(); @@ -55,26 +61,31 @@ impl Database { notification: Notification, tx: &DatabaseTransaction, ) -> Result { - let (kind, associated_entities) = notification.to_parts(); + let notification = notification.to_any(); + let kind = *self + .notification_kinds_by_name + .get(notification.kind.as_ref()) + .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification.kind))?; + let model = notification::ActiveModel { recipient_id: ActiveValue::Set(recipient_id), - kind: ActiveValue::Set(kind as i32), - entity_id_1: ActiveValue::Set(associated_entities[0].map(|id| id as i32)), - entity_id_2: ActiveValue::Set(associated_entities[1].map(|id| id as i32)), - entity_id_3: ActiveValue::Set(associated_entities[2].map(|id| id as i32)), - ..Default::default() + kind: ActiveValue::Set(kind), + content: ActiveValue::Set(notification.content.clone()), + actor_id: ActiveValue::Set(notification.actor_id.map(|id| UserId::from_proto(id))), + is_read: ActiveValue::NotSet, + created_at: ActiveValue::NotSet, + id: ActiveValue::NotSet, } .save(&*tx) .await?; Ok(proto::Notification { id: model.id.as_ref().to_proto(), - kind: *model.kind.as_ref() as u32, + kind: notification.kind.to_string(), timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, is_read: false, - entity_id_1: model.entity_id_1.as_ref().map(|id| id as u64), - entity_id_2: model.entity_id_2.as_ref().map(|id| id as u64), - entity_id_3: model.entity_id_3.as_ref().map(|id| id as u64), + content: notification.content, + actor_id: notification.actor_id, }) } } diff --git a/crates/collab/src/db/tables/notification.rs b/crates/collab/src/db/tables/notification.rs index 6a0abe9dc6..a35e00fb5b 100644 --- a/crates/collab/src/db/tables/notification.rs +++ b/crates/collab/src/db/tables/notification.rs @@ -1,4 +1,4 @@ -use crate::db::{NotificationId, UserId}; +use crate::db::{NotificationId, NotificationKindId, UserId}; use sea_orm::entity::prelude::*; use time::PrimitiveDateTime; @@ -7,13 +7,12 @@ use time::PrimitiveDateTime; pub struct Model { #[sea_orm(primary_key)] pub id: NotificationId, - pub recipient_id: UserId, - pub kind: i32, pub is_read: bool, pub created_at: PrimitiveDateTime, - pub entity_id_1: Option, - pub entity_id_2: Option, - pub entity_id_3: Option, + pub recipient_id: UserId, + pub actor_id: Option, + pub kind: NotificationKindId, + pub content: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tables/notification_kind.rs b/crates/collab/src/db/tables/notification_kind.rs index 32dfb2065a..865b5da04b 100644 --- a/crates/collab/src/db/tables/notification_kind.rs +++ b/crates/collab/src/db/tables/notification_kind.rs @@ -1,10 +1,11 @@ +use crate::db::NotificationKindId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "notification_kinds")] pub struct Model { #[sea_orm(primary_key)] - pub id: i32, + pub id: NotificationKindId, pub name: String, } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 6a91fd6ffe..465ff56444 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -31,7 +31,7 @@ impl TestDb { let mut db = runtime.block_on(async { let mut options = ConnectOptions::new(url); options.max_connections(5); - let db = Database::new(options, Executor::Deterministic(background)) + let mut db = Database::new(options, Executor::Deterministic(background)) .await .unwrap(); let sql = include_str!(concat!( @@ -45,6 +45,7 @@ impl TestDb { )) .await .unwrap(); + db.initialize_notification_enum().await.unwrap(); db }); @@ -79,11 +80,12 @@ impl TestDb { options .max_connections(5) .idle_timeout(Duration::from_secs(0)); - let db = Database::new(options, Executor::Deterministic(background)) + let mut db = Database::new(options, Executor::Deterministic(background)) .await .unwrap(); let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"); db.migrate(Path::new(migrations_path), false).await.unwrap(); + db.initialize_notification_enum().await.unwrap(); db }); diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 13fb8ed0eb..1722424217 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -119,7 +119,9 @@ impl AppState { pub async fn new(config: Config) -> Result> { let mut db_options = db::ConnectOptions::new(config.database_url.clone()); db_options.max_connections(config.database_max_connections); - let db = Database::new(db_options, Executor::Production).await?; + let mut db = Database::new(db_options, Executor::Production).await?; + db.initialize_notification_enum().await?; + let live_kit_client = if let Some(((server, key), secret)) = config .live_kit_server .as_ref() diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index eb123cf960..01da0dc88a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -291,8 +291,6 @@ impl Server { let pool = self.connection_pool.clone(); let live_kit_client = self.app_state.live_kit_client.clone(); - self.app_state.db.ensure_notification_kinds().await?; - let span = info_span!("start server"); self.executor.spawn_detached( async move { diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index a78caf5ff6..334d844cf5 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -185,18 +185,22 @@ impl NotificationPanel { let text; let actor; match entry.notification { - Notification::ContactRequest { requester_id } => { + Notification::ContactRequest { + actor_id: requester_id, + } => { actor = user_store.get_cached_user(requester_id)?; icon = "icons/plus.svg"; text = format!("{} wants to add you as a contact", actor.github_login); } - Notification::ContactRequestAccepted { contact_id } => { + Notification::ContactRequestAccepted { + actor_id: contact_id, + } => { actor = user_store.get_cached_user(contact_id)?; icon = "icons/plus.svg"; text = format!("{} accepted your contact invite", actor.github_login); } Notification::ChannelInvitation { - inviter_id, + actor_id: inviter_id, channel_id, } => { actor = user_store.get_cached_user(inviter_id)?; @@ -209,7 +213,7 @@ impl NotificationPanel { ); } Notification::ChannelMessageMention { - sender_id, + actor_id: sender_id, channel_id, message_id, } => { diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 9bfa67c76e..4ebbf46093 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -3,7 +3,7 @@ use channel::{ChannelMessage, ChannelMessageId, ChannelStore}; use client::{Client, UserStore}; use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; -use rpc::{proto, Notification, NotificationKind, TypedEnvelope}; +use rpc::{proto, AnyNotification, Notification, TypedEnvelope}; use std::{ops::Range, sync::Arc}; use sum_tree::{Bias, SumTree}; use time::OffsetDateTime; @@ -112,14 +112,11 @@ impl NotificationStore { is_read: message.is_read, timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) .ok()?, - notification: Notification::from_parts( - NotificationKind::from_i32(message.kind as i32)?, - [ - message.entity_id_1, - message.entity_id_2, - message.entity_id_3, - ], - )?, + notification: Notification::from_any(&AnyNotification { + actor_id: message.actor_id, + kind: message.kind.into(), + content: message.content, + })?, }) }) .collect::>(); @@ -129,17 +126,24 @@ impl NotificationStore { for entry in ¬ifications { match entry.notification { - Notification::ChannelInvitation { inviter_id, .. } => { + Notification::ChannelInvitation { + actor_id: inviter_id, + .. + } => { user_ids.push(inviter_id); } - Notification::ContactRequest { requester_id } => { + Notification::ContactRequest { + actor_id: requester_id, + } => { user_ids.push(requester_id); } - Notification::ContactRequestAccepted { contact_id } => { + Notification::ContactRequestAccepted { + actor_id: contact_id, + } => { user_ids.push(contact_id); } Notification::ChannelMessageMention { - sender_id, + actor_id: sender_id, message_id, .. } => { diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index bc750374dd..a2895e5f1b 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -17,6 +17,7 @@ clock = { path = "../clock" } collections = { path = "../collections" } gpui = { path = "../gpui", optional = true } util = { path = "../util" } + anyhow.workspace = true async-lock = "2.4" async-tungstenite = "0.16" @@ -27,6 +28,7 @@ prost.workspace = true rand.workspace = true rsa = "0.4" serde.workspace = true +serde_json.workspace = true serde_derive.workspace = true smol-timeout = "0.6" strum.workspace = true diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 4b5c17ae8b..f767189024 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1571,10 +1571,9 @@ message AddNotifications { message Notification { uint64 id = 1; - uint32 kind = 2; - uint64 timestamp = 3; - bool is_read = 4; - optional uint64 entity_id_1 = 5; - optional uint64 entity_id_2 = 6; - optional uint64 entity_id_3 = 7; + uint64 timestamp = 2; + bool is_read = 3; + string kind = 4; + string content = 5; + optional uint64 actor_id = 6; } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index fc6dc54d15..839966aea6 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -1,110 +1,94 @@ -use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::borrow::Cow; +use strum::{EnumVariantNames, IntoStaticStr, VariantNames as _}; -// An integer indicating a type of notification. The variants' numerical -// values are stored in the database, so they should never be removed -// or changed. -#[repr(i32)] -#[derive(Copy, Clone, Debug, EnumIter, EnumString, Display)] -pub enum NotificationKind { - ContactRequest = 0, - ContactRequestAccepted = 1, - ChannelInvitation = 2, - ChannelMessageMention = 3, -} +const KIND: &'static str = "kind"; +const ACTOR_ID: &'static str = "actor_id"; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr, Serialize, Deserialize)] +#[serde(tag = "kind")] pub enum Notification { ContactRequest { - requester_id: u64, + actor_id: u64, }, ContactRequestAccepted { - contact_id: u64, + actor_id: u64, }, ChannelInvitation { - inviter_id: u64, + actor_id: u64, channel_id: u64, }, ChannelMessageMention { - sender_id: u64, + actor_id: u64, channel_id: u64, message_id: u64, }, } +#[derive(Debug)] +pub struct AnyNotification { + pub kind: Cow<'static, str>, + pub actor_id: Option, + pub content: String, +} + impl Notification { - /// Load this notification from its generic representation, which is - /// used to represent it in the database, and in the wire protocol. - /// - /// The order in which a given notification type's fields are listed must - /// match the order they're listed in the `to_parts` method, and it must - /// not change, because they're stored in that order in the database. - pub fn from_parts(kind: NotificationKind, entity_ids: [Option; 3]) -> Option { - use NotificationKind::*; - Some(match kind { - ContactRequest => Self::ContactRequest { - requester_id: entity_ids[0]?, - }, - - ContactRequestAccepted => Self::ContactRequest { - requester_id: entity_ids[0]?, - }, - - ChannelInvitation => Self::ChannelInvitation { - inviter_id: entity_ids[0]?, - channel_id: entity_ids[1]?, - }, - - ChannelMessageMention => Self::ChannelMessageMention { - sender_id: entity_ids[0]?, - channel_id: entity_ids[1]?, - message_id: entity_ids[2]?, - }, - }) - } - - /// Convert this notification into its generic representation, which is - /// used to represent it in the database, and in the wire protocol. - /// - /// The order in which a given notification type's fields are listed must - /// match the order they're listed in the `from_parts` method, and it must - /// not change, because they're stored in that order in the database. - pub fn to_parts(&self) -> (NotificationKind, [Option; 3]) { - use NotificationKind::*; - match self { - Self::ContactRequest { requester_id } => { - (ContactRequest, [Some(*requester_id), None, None]) - } - - Self::ContactRequestAccepted { contact_id } => { - (ContactRequest, [Some(*contact_id), None, None]) - } - - Self::ChannelInvitation { - inviter_id, - channel_id, - } => ( - ChannelInvitation, - [Some(*inviter_id), Some(*channel_id), None], - ), - - Self::ChannelMessageMention { - sender_id, - channel_id, - message_id, - } => ( - ChannelMessageMention, - [Some(*sender_id), Some(*channel_id), Some(*message_id)], - ), + pub fn to_any(&self) -> AnyNotification { + let kind: &'static str = self.into(); + let mut value = serde_json::to_value(self).unwrap(); + let mut actor_id = None; + if let Some(value) = value.as_object_mut() { + value.remove("kind"); + actor_id = value + .remove("actor_id") + .and_then(|value| Some(value.as_i64()? as u64)); + } + AnyNotification { + kind: Cow::Borrowed(kind), + actor_id, + content: serde_json::to_string(&value).unwrap(), } } -} -impl NotificationKind { - pub fn all() -> impl Iterator { - Self::iter() + pub fn from_any(notification: &AnyNotification) -> Option { + let mut value = serde_json::from_str::(¬ification.content).ok()?; + let object = value.as_object_mut()?; + object.insert(KIND.into(), notification.kind.to_string().into()); + if let Some(actor_id) = notification.actor_id { + object.insert(ACTOR_ID.into(), actor_id.into()); + } + serde_json::from_value(value).ok() } - pub fn from_i32(i: i32) -> Option { - Self::iter().find(|kind| *kind as i32 == i) + pub fn all_kinds() -> &'static [&'static str] { + Self::VARIANTS } } + +#[test] +fn test_notification() { + // Notifications can be serialized and deserialized. + for notification in [ + Notification::ContactRequest { actor_id: 1 }, + Notification::ContactRequestAccepted { actor_id: 2 }, + Notification::ChannelInvitation { + actor_id: 0, + channel_id: 100, + }, + Notification::ChannelMessageMention { + actor_id: 200, + channel_id: 30, + message_id: 1, + }, + ] { + let serialized = notification.to_any(); + let deserialized = Notification::from_any(&serialized).unwrap(); + assert_eq!(deserialized, notification); + } + + // When notifications are serialized, redundant data is not stored + // in the JSON. + let notification = Notification::ContactRequest { actor_id: 1 }; + assert_eq!(notification.to_any().content, "{}"); +} From 034e9935d4b3792a6b8e1e0f439379caae2325eb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 12 Oct 2023 17:39:04 -0700 Subject: [PATCH 058/274] Remove old contact request notification mechanism, use notification instead --- crates/client/src/user.rs | 35 +++++---------- crates/collab/src/db.rs | 15 ++----- crates/collab/src/db/queries/contacts.rs | 5 --- crates/collab/src/db/tests/db_tests.rs | 19 +-------- crates/collab/src/rpc.rs | 54 ++++++++++-------------- crates/rpc/proto/zed.proto | 2 - crates/rpc/src/notification.rs | 12 +++++- crates/zed/src/zed.rs | 1 + 8 files changed, 49 insertions(+), 94 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 6aa41708e3..d02c22d797 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -293,21 +293,19 @@ impl UserStore { // No need to paralellize here let mut updated_contacts = Vec::new(); for contact in message.contacts { - let should_notify = contact.should_notify; - updated_contacts.push(( - Arc::new(Contact::from_proto(contact, &this, &mut cx).await?), - should_notify, + updated_contacts.push(Arc::new( + Contact::from_proto(contact, &this, &mut cx).await?, )); } let mut incoming_requests = Vec::new(); for request in message.incoming_requests { - incoming_requests.push({ - let user = this - .update(&mut cx, |this, cx| this.get_user(request.requester_id, cx)) - .await?; - (user, request.should_notify) - }); + incoming_requests.push( + this.update(&mut cx, |this, cx| { + this.get_user(request.requester_id, cx) + }) + .await?, + ); } let mut outgoing_requests = Vec::new(); @@ -330,13 +328,7 @@ impl UserStore { this.contacts .retain(|contact| !removed_contacts.contains(&contact.user.id)); // Update existing contacts and insert new ones - for (updated_contact, should_notify) in updated_contacts { - if should_notify { - cx.emit(Event::Contact { - user: updated_contact.user.clone(), - kind: ContactEventKind::Accepted, - }); - } + for updated_contact in updated_contacts { match this.contacts.binary_search_by_key( &&updated_contact.user.github_login, |contact| &contact.user.github_login, @@ -359,14 +351,7 @@ impl UserStore { } }); // Update existing incoming requests and insert new ones - for (user, should_notify) in incoming_requests { - if should_notify { - cx.emit(Event::Contact { - user: user.clone(), - kind: ContactEventKind::Requested, - }); - } - + for user in incoming_requests { match this .incoming_contact_requests .binary_search_by_key(&&user.github_login, |contact| { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9aea23ca84..67055d27ee 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -370,18 +370,9 @@ impl RoomGuard { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Contact { - Accepted { - user_id: UserId, - should_notify: bool, - busy: bool, - }, - Outgoing { - user_id: UserId, - }, - Incoming { - user_id: UserId, - should_notify: bool, - }, + Accepted { user_id: UserId, busy: bool }, + Outgoing { user_id: UserId }, + Incoming { user_id: UserId }, } impl Contact { diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index 083315e290..f02bae667a 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -8,7 +8,6 @@ impl Database { user_id_b: UserId, a_to_b: bool, accepted: bool, - should_notify: bool, user_a_busy: bool, user_b_busy: bool, } @@ -53,7 +52,6 @@ impl Database { if db_contact.accepted { contacts.push(Contact::Accepted { user_id: db_contact.user_id_b, - should_notify: db_contact.should_notify && db_contact.a_to_b, busy: db_contact.user_b_busy, }); } else if db_contact.a_to_b { @@ -63,19 +61,16 @@ impl Database { } else { contacts.push(Contact::Incoming { user_id: db_contact.user_id_b, - should_notify: db_contact.should_notify, }); } } else if db_contact.accepted { contacts.push(Contact::Accepted { user_id: db_contact.user_id_a, - should_notify: db_contact.should_notify && !db_contact.a_to_b, busy: db_contact.user_a_busy, }); } else if db_contact.a_to_b { contacts.push(Contact::Incoming { user_id: db_contact.user_id_a, - should_notify: db_contact.should_notify, }); } else { contacts.push(Contact::Outgoing { diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 1520e081c0..d175bd743d 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -264,10 +264,7 @@ async fn test_add_contacts(db: &Arc) { ); assert_eq!( db.get_contacts(user_2).await.unwrap(), - &[Contact::Incoming { - user_id: user_1, - should_notify: true - }] + &[Contact::Incoming { user_id: user_1 }] ); // User 2 dismisses the contact request notification without accepting or rejecting. @@ -280,10 +277,7 @@ async fn test_add_contacts(db: &Arc) { .unwrap(); assert_eq!( db.get_contacts(user_2).await.unwrap(), - &[Contact::Incoming { - user_id: user_1, - should_notify: false - }] + &[Contact::Incoming { user_id: user_1 }] ); // User can't accept their own contact request @@ -299,7 +293,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_1).await.unwrap(), &[Contact::Accepted { user_id: user_2, - should_notify: true, busy: false, }], ); @@ -309,7 +302,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_2).await.unwrap(), &[Contact::Accepted { user_id: user_1, - should_notify: false, busy: false, }] ); @@ -326,7 +318,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_1).await.unwrap(), &[Contact::Accepted { user_id: user_2, - should_notify: true, busy: false, }] ); @@ -339,7 +330,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_1).await.unwrap(), &[Contact::Accepted { user_id: user_2, - should_notify: false, busy: false, }] ); @@ -353,12 +343,10 @@ async fn test_add_contacts(db: &Arc) { &[ Contact::Accepted { user_id: user_2, - should_notify: false, busy: false, }, Contact::Accepted { user_id: user_3, - should_notify: false, busy: false, } ] @@ -367,7 +355,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_3).await.unwrap(), &[Contact::Accepted { user_id: user_1, - should_notify: false, busy: false, }], ); @@ -383,7 +370,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_2).await.unwrap(), &[Contact::Accepted { user_id: user_1, - should_notify: false, busy: false, }] ); @@ -391,7 +377,6 @@ async fn test_add_contacts(db: &Arc) { db.get_contacts(user_3).await.unwrap(), &[Contact::Accepted { user_id: user_1, - should_notify: false, busy: false, }], ); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01da0dc88a..60cdaeec70 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -388,7 +388,7 @@ impl Server { let contacts = app_state.db.get_contacts(user_id).await.trace_err(); if let Some((busy, contacts)) = busy.zip(contacts) { let pool = pool.lock(); - let updated_contact = contact_for_user(user_id, false, busy, &pool); + let updated_contact = contact_for_user(user_id, busy, &pool); for contact in contacts { if let db::Contact::Accepted { user_id: contact_user_id, @@ -690,7 +690,7 @@ impl Server { if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? { if let Some(code) = &user.invite_code { let pool = self.connection_pool.lock(); - let invitee_contact = contact_for_user(invitee_id, true, false, &pool); + let invitee_contact = contact_for_user(invitee_id, false, &pool); for connection_id in pool.user_connection_ids(inviter_id) { self.peer.send( connection_id, @@ -2090,7 +2090,6 @@ async fn request_contact( .incoming_requests .push(proto::IncomingContactRequest { requester_id: requester_id.to_proto(), - should_notify: true, }); for connection_id in session .connection_pool() @@ -2124,7 +2123,8 @@ async fn respond_to_contact_request( } else { let accept = request.response == proto::ContactRequestResponse::Accept as i32; - db.respond_to_contact_request(responder_id, requester_id, accept) + let notification = db + .respond_to_contact_request(responder_id, requester_id, accept) .await?; let requester_busy = db.is_user_busy(requester_id).await?; let responder_busy = db.is_user_busy(responder_id).await?; @@ -2135,7 +2135,7 @@ async fn respond_to_contact_request( if accept { update .contacts - .push(contact_for_user(requester_id, false, requester_busy, &pool)); + .push(contact_for_user(requester_id, requester_busy, &pool)); } update .remove_incoming_requests @@ -2149,13 +2149,19 @@ async fn respond_to_contact_request( if accept { update .contacts - .push(contact_for_user(responder_id, true, responder_busy, &pool)); + .push(contact_for_user(responder_id, responder_busy, &pool)); } update .remove_outgoing_requests .push(responder_id.to_proto()); for connection_id in pool.user_connection_ids(requester_id) { session.peer.send(connection_id, update.clone())?; + session.peer.send( + connection_id, + proto::AddNotifications { + notifications: vec![notification.clone()], + }, + )?; } } @@ -3127,42 +3133,28 @@ fn build_initial_contacts_update( for contact in contacts { match contact { - db::Contact::Accepted { - user_id, - should_notify, - busy, - } => { - update - .contacts - .push(contact_for_user(user_id, should_notify, busy, &pool)); + db::Contact::Accepted { user_id, busy } => { + update.contacts.push(contact_for_user(user_id, busy, &pool)); } db::Contact::Outgoing { user_id } => update.outgoing_requests.push(user_id.to_proto()), - db::Contact::Incoming { - user_id, - should_notify, - } => update - .incoming_requests - .push(proto::IncomingContactRequest { - requester_id: user_id.to_proto(), - should_notify, - }), + db::Contact::Incoming { user_id } => { + update + .incoming_requests + .push(proto::IncomingContactRequest { + requester_id: user_id.to_proto(), + }) + } } } update } -fn contact_for_user( - user_id: UserId, - should_notify: bool, - busy: bool, - pool: &ConnectionPool, -) -> proto::Contact { +fn contact_for_user(user_id: UserId, busy: bool, pool: &ConnectionPool) -> proto::Contact { proto::Contact { user_id: user_id.to_proto(), online: pool.is_user_online(user_id), busy, - should_notify, } } @@ -3223,7 +3215,7 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> let busy = db.is_user_busy(user_id).await?; let pool = session.connection_pool().await; - let updated_contact = contact_for_user(user_id, false, busy, &pool); + let updated_contact = contact_for_user(user_id, busy, &pool); for contact in contacts { if let db::Contact::Accepted { user_id: contact_user_id, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f767189024..8dca38bdfd 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1223,7 +1223,6 @@ message ShowContacts {} message IncomingContactRequest { uint64 requester_id = 1; - bool should_notify = 2; } message UpdateDiagnostics { @@ -1549,7 +1548,6 @@ message Contact { uint64 user_id = 1; bool online = 2; bool busy = 3; - bool should_notify = 4; } message WorktreeMetadata { diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 839966aea6..8aabb9b9df 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -6,6 +6,12 @@ use strum::{EnumVariantNames, IntoStaticStr, VariantNames as _}; const KIND: &'static str = "kind"; const ACTOR_ID: &'static str = "actor_id"; +/// A notification that can be stored, associated with a given user. +/// +/// This struct is stored in the collab database as JSON, so it shouldn't be +/// changed in a backward-incompatible way. +/// +/// For example, when renaming a variant, add a serde alias for the old name. #[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum Notification { @@ -26,6 +32,8 @@ pub enum Notification { }, } +/// The representation of a notification that is stored in the database and +/// sent over the wire. #[derive(Debug)] pub struct AnyNotification { pub kind: Cow<'static, str>, @@ -87,8 +95,8 @@ fn test_notification() { assert_eq!(deserialized, notification); } - // When notifications are serialized, redundant data is not stored - // in the JSON. + // When notifications are serialized, the `kind` and `actor_id` fields are + // stored separately, and do not appear redundantly in the JSON. let notification = Notification::ContactRequest { actor_id: 1 }; assert_eq!(notification.to_any().content, "{}"); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8caff21c5f..5226557235 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2445,6 +2445,7 @@ mod tests { audio::init((), cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); + notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); language::init(cx); From 1c3ecc4ad242047a700a602ceda0b3630b7118d0 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 21:00:31 -0400 Subject: [PATCH 059/274] Whooooops --- crates/editor/src/editor.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bdacf0be38..1a17f38f92 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3596,7 +3596,6 @@ impl Editor { let menu = menu.unwrap(); *context_menu = Some(ContextMenu::Completions(menu)); drop(context_menu); - this.completion_tasks.clear(); this.discard_copilot_suggestion(cx); cx.notify(); } else if this.completion_tasks.is_empty() { From a7db2aa39dfd5293c0569db22fa132887f53c63c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 12 Oct 2023 19:59:50 -0600 Subject: [PATCH 060/274] 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 ec4391b88e9a41e47de295896cb20764f007e053 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 12 Oct 2023 22:08:47 -0400 Subject: [PATCH 061/274] Add setting to disable completion docs --- assets/settings/default.json | 3 +++ crates/editor/src/editor.rs | 24 ++++++++++++++++++++++-- crates/editor/src/editor_settings.rs | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 8fb73a2ecb..8a3598eed1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -50,6 +50,9 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Whether to display inline and alongside documentation for items in the + // completions menu + "show_completion_documentation": true, // Whether to show wrap guides in the editor. Setting this to true will // show a guide at the 'preferred_line_length' value if softwrap is set to // 'preferred_line_length', and will show any additional guides as specified diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a17f38f92..bb6d693d82 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1000,6 +1000,11 @@ impl CompletionsMenu { project: Option>, cx: &mut ViewContext, ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + let Some(project) = project else { return; }; @@ -1083,6 +1088,11 @@ impl CompletionsMenu { project: Option<&ModelHandle>, cx: &mut ViewContext, ) { + let settings = settings::get::(cx); + if !settings.show_completion_documentation { + return; + } + let completion_index = self.matches[self.selected_item].candidate_id; let Some(project) = project else { return; @@ -1241,6 +1251,9 @@ impl CompletionsMenu { ) -> AnyElement { enum CompletionTag {} + let settings = settings::get::(cx); + let show_completion_documentation = settings.show_completion_documentation; + let widest_completion_ix = self .matches .iter() @@ -1252,7 +1265,9 @@ impl CompletionsMenu { let mut len = completion.label.text.chars().count(); if let Some(Documentation::SingleLine(text)) = documentation { - len += text.chars().count(); + if show_completion_documentation { + len += text.chars().count(); + } } len @@ -1273,7 +1288,12 @@ impl CompletionsMenu { let item_ix = start_ix + ix; let candidate_id = mat.candidate_id; let completion = &completions_guard[candidate_id]; - let documentation = &completion.documentation; + + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; items.push( MouseEventHandler::new::( diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index b06f23429a..75f8b800f9 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -7,6 +7,7 @@ pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, pub show_completions_on_input: bool, + pub show_completion_documentation: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, pub relative_line_numbers: bool, @@ -33,6 +34,7 @@ pub struct EditorSettingsContent { pub cursor_blink: Option, pub hover_popover_enabled: Option, pub show_completions_on_input: Option, + pub show_completion_documentation: Option, pub use_on_type_format: Option, pub scrollbar: Option, pub relative_line_numbers: Option, From da2b8082b36d704131e6ce9f2555b8a17ca6ca35 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 12 Oct 2023 20:42:42 -0600 Subject: [PATCH 062/274] 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 063/274] 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 525ff6bf7458a2f48747fc9e06dd10e83845554b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 13 Oct 2023 10:20:55 +0300 Subject: [PATCH 064/274] Remove zed -> ... -> semantic_index -> zed Cargo dependency cycle --- Cargo.lock | 2 +- crates/semantic_index/Cargo.toml | 4 ---- crates/zed/Cargo.toml | 4 ++++ .../examples/eval.rs => zed/examples/semantic_index_eval.rs} | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename crates/{semantic_index/examples/eval.rs => zed/examples/semantic_index_eval.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 01153ca0f8..85b80fa487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6956,7 +6956,6 @@ dependencies = [ "unindent", "util", "workspace", - "zed", ] [[package]] @@ -10049,6 +10048,7 @@ name = "zed" version = "0.109.0" dependencies = [ "activity_indicator", + "ai", "anyhow", "assistant", "async-compression", diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 34850f7035..1febb2af78 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"]} rust-embed = { version = "8.0", features = ["include-exclude"] } client = { path = "../client" } -zed = { path = "../zed"} node_runtime = { path = "../node_runtime"} pretty_assertions.workspace = true @@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true tree-sitter-lua.workspace = true tree-sitter-ruby.workspace = true tree-sitter-php.workspace = true - -[[example]] -name = "eval" diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4174f7d6d5..f9abcc1e91 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,6 +15,9 @@ doctest = false name = "Zed" path = "src/main.rs" +[[example]] +name = "semantic_index_eval" + [dependencies] audio = { path = "../audio" } activity_indicator = { path = "../activity_indicator" } @@ -141,6 +144,7 @@ urlencoding = "2.1.2" uuid.workspace = true [dev-dependencies] +ai = { path = "../ai" } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/semantic_index/examples/eval.rs b/crates/zed/examples/semantic_index_eval.rs similarity index 100% rename from crates/semantic_index/examples/eval.rs rename to crates/zed/examples/semantic_index_eval.rs From 803ab81eb6e1cc718679fdba862163c23d7ef174 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 13 Oct 2023 12:13:18 +0300 Subject: [PATCH 065/274] Update diagnostics indicator when diagnostics are udpated --- crates/diagnostics/src/items.rs | 4 ++++ crates/project/src/project.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index c3733018b6..8d3c2fedd6 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -38,6 +38,10 @@ impl DiagnosticIndicator { this.in_progress_checks.remove(language_server_id); cx.notify(); } + project::Event::DiagnosticsUpdated { .. } => { + this.summary = project.read(cx).diagnostic_summary(cx); + cx.notify(); + } _ => {} }) .detach(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9e1b1ce96..e3251d7483 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2934,8 +2934,8 @@ impl Project { move |mut params, mut cx| { let this = this; let adapter = adapter.clone(); - adapter.process_diagnostics(&mut params); if let Some(this) = this.upgrade(&cx) { + adapter.process_diagnostics(&mut params); this.update(&mut cx, |this, cx| { this.update_diagnostics( server_id, From bfbe4ae4b47140024cdaf8d9680956bd228d6b84 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 13 Oct 2023 18:58:59 +0200 Subject: [PATCH 066/274] Piotr/z 651 vue support (#3123) Release Notes: - Added Vue language support. --- Cargo.lock | 10 + Cargo.toml | 2 +- crates/language/src/language.rs | 3 - crates/project/src/project.rs | 25 ++- crates/util/src/github.rs | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 10 +- crates/zed/src/languages/vue.rs | 214 ++++++++++++++++++++ crates/zed/src/languages/vue/brackets.scm | 2 + crates/zed/src/languages/vue/config.toml | 14 ++ crates/zed/src/languages/vue/highlights.scm | 15 ++ crates/zed/src/languages/vue/injections.scm | 7 + 12 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 crates/zed/src/languages/vue.rs create mode 100644 crates/zed/src/languages/vue/brackets.scm create mode 100644 crates/zed/src/languages/vue/config.toml create mode 100644 crates/zed/src/languages/vue/highlights.scm create mode 100644 crates/zed/src/languages/vue/injections.scm diff --git a/Cargo.lock b/Cargo.lock index 85b80fa487..2ef86073ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8792,6 +8792,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-vue" +version = "0.0.1" +source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-yaml" version = "0.0.1" @@ -10161,6 +10170,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-vue", "tree-sitter-yaml", "unindent", "url", diff --git a/Cargo.toml b/Cargo.toml index 532610efd6..995cd15edd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,7 +149,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} - +tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"} [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bd389652a0..eb6f6e89f7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -110,7 +110,6 @@ pub struct LanguageServerName(pub Arc); pub struct CachedLspAdapter { pub name: LanguageServerName, pub short_name: &'static str, - pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, @@ -121,7 +120,6 @@ impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; let short_name = adapter.short_name(); - let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token().await; @@ -130,7 +128,6 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, short_name, - initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e3251d7483..875086a4e3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2751,15 +2751,6 @@ impl Project { let lsp = project_settings.lsp.get(&adapter.name.0); let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); - let mut initialization_options = adapter.initialization_options.clone(); - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - let server_id = pending_server.server_id; let container_dir = pending_server.container_dir.clone(); let state = LanguageServerState::Starting({ @@ -2771,7 +2762,7 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let result = Self::setup_and_insert_language_server( this, - initialization_options, + override_options, pending_server, adapter.clone(), language.clone(), @@ -2874,7 +2865,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModelHandle, - initialization_options: Option, + override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, language: Arc, @@ -2884,7 +2875,7 @@ impl Project { ) -> Result>> { let setup = Self::setup_pending_language_server( this, - initialization_options, + override_initialization_options, pending_server, adapter.clone(), server_id, @@ -2916,7 +2907,7 @@ impl Project { async fn setup_pending_language_server( this: WeakModelHandle, - initialization_options: Option, + override_options: Option, pending_server: PendingLanguageServer, adapter: Arc, server_id: LanguageServerId, @@ -3062,6 +3053,14 @@ impl Project { } }) .detach(); + let mut initialization_options = adapter.adapter.initialization_options().await; + match (&mut initialization_options, override_options) { + (Some(initialization_options), Some(override_options)) => { + merge_json_value_into(override_options, initialization_options); + } + (None, override_options) => initialization_options = override_options, + _ => {} + } let language_server = language_server.initialize(initialization_options).await?; diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index b1e981ae49..a3df4c996b 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -16,6 +16,7 @@ pub struct GithubRelease { pub pre_release: bool, pub assets: Vec, pub tarball_url: String, + pub zipball_url: String, } #[derive(Deserialize, Debug)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f9abcc1e91..aeabd4b453 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -138,6 +138,7 @@ tree-sitter-yaml.workspace = true tree-sitter-lua.workspace = true tree-sitter-nix.workspace = true tree-sitter-nu.workspace = true +tree-sitter-vue.workspace = true url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 04e5292a7d..caf3cbf7c9 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -24,6 +24,7 @@ mod rust; mod svelte; mod tailwind; mod typescript; +mod vue; mod yaml; // 1. Add tree-sitter-{language} parser to zed crate @@ -190,13 +191,20 @@ pub fn init( language( "php", tree_sitter_php::language(), - vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], + vec![Arc::new(php::IntelephenseLspAdapter::new( + node_runtime.clone(), + ))], ); language("elm", tree_sitter_elm::language(), vec![]); language("glsl", tree_sitter_glsl::language(), vec![]); language("nix", tree_sitter_nix::language(), vec![]); language("nu", tree_sitter_nu::language(), vec![]); + language( + "vue", + tree_sitter_vue::language(), + vec![Arc::new(vue::VueLspAdapter::new(node_runtime))], + ); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/vue.rs b/crates/zed/src/languages/vue.rs new file mode 100644 index 0000000000..f0374452df --- /dev/null +++ b/crates/zed/src/languages/vue.rs @@ -0,0 +1,214 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +pub use language::*; +use lsp::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use parking_lot::Mutex; +use serde_json::Value; +use smol::fs::{self}; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +pub struct VueLspVersion { + vue_version: String, + ts_version: String, +} + +pub struct VueLspAdapter { + node: Arc, + typescript_install_path: Mutex>, +} + +impl VueLspAdapter { + const SERVER_PATH: &'static str = + "node_modules/@vue/language-server/bin/vue-language-server.js"; + // TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options. + const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib"; + pub fn new(node: Arc) -> Self { + let typescript_install_path = Mutex::new(None); + Self { + node, + typescript_install_path, + } + } +} +#[async_trait] +impl super::LspAdapter for VueLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vue-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "vue-language-server" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(VueLspVersion { + vue_version: self + .node + .npm_package_latest_version("@vue/language-server") + .await?, + ts_version: self.node.npm_package_latest_version("typescript").await?, + }) as Box<_>) + } + async fn initialization_options(&self) -> Option { + let typescript_sdk_path = self.typescript_install_path.lock(); + let typescript_sdk_path = typescript_sdk_path + .as_ref() + .expect("initialization_options called without a container_dir for typescript"); + + Some(serde_json::json!({ + "typescript": { + "tsdk": typescript_sdk_path + } + })) + } + fn code_action_kinds(&self) -> Option> { + // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it + // sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec. + Some(vec![ + CodeActionKind::EMPTY, + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR_REWRITE, + ]) + } + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + let ts_path = container_dir.join(Self::TYPESCRIPT_PATH); + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("@vue/language-server", version.vue_version.as_str())], + ) + .await?; + } + assert!(fs::metadata(&server_path).await.is_ok()); + if fs::metadata(&ts_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + &[("typescript", version.ts_version.as_str())], + ) + .await?; + } + + assert!(fs::metadata(&ts_path).await.is_ok()); + *self.typescript_install_path.lock() = Some(ts_path); + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()) + .await + .map(|(mut binary, ts_path)| { + binary.arguments = vec!["--help".into()]; + (binary, ts_path) + })?; + *self.typescript_install_path.lock() = Some(ts_path); + Some(server) + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"), + Kind::VARIABLE => grammar.highlight_id_for_name("type"), + Kind::KEYWORD => grammar.highlight_id_for_name("keyword"), + Kind::VALUE => grammar.highlight_id_for_name("tag"), + _ => None, + }?; + + let text = match &item.detail { + Some(detail) => format!("{} {}", item.label, detail), + None => item.label.clone(), + }; + + Some(language::CodeLabel { + text, + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } +} + +fn vue_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +type TypescriptPath = PathBuf; +async fn get_cached_server_binary( + container_dir: PathBuf, + node: Arc, +) -> Option<(LanguageServerBinary, TypescriptPath)> { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH); + let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH); + if server_path.exists() && typescript_path.exists() { + Ok(( + LanguageServerBinary { + path: node.binary_path().await?, + arguments: vue_server_binary_arguments(&server_path), + }, + typescript_path, + )) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/vue/brackets.scm b/crates/zed/src/languages/vue/brackets.scm new file mode 100644 index 0000000000..2d12b17daa --- /dev/null +++ b/crates/zed/src/languages/vue/brackets.scm @@ -0,0 +1,2 @@ +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/vue/config.toml b/crates/zed/src/languages/vue/config.toml new file mode 100644 index 0000000000..c41a667b75 --- /dev/null +++ b/crates/zed/src/languages/vue/config.toml @@ -0,0 +1,14 @@ +name = "Vue.js" +path_suffixes = ["vue"] +block_comment = [""] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, + { start = "`", end = "`", close = true, newline = false, not_in = ["string"] }, +] +word_characters = ["-"] diff --git a/crates/zed/src/languages/vue/highlights.scm b/crates/zed/src/languages/vue/highlights.scm new file mode 100644 index 0000000000..1a80c84f68 --- /dev/null +++ b/crates/zed/src/languages/vue/highlights.scm @@ -0,0 +1,15 @@ +(attribute) @property +(directive_attribute) @property +(quoted_attribute_value) @string +(interpolation) @punctuation.special +(raw_text) @embedded + +((tag_name) @type + (#match? @type "^[A-Z]")) + +((directive_name) @keyword + (#match? @keyword "^v-")) + +(start_tag) @tag +(end_tag) @tag +(self_closing_tag) @tag diff --git a/crates/zed/src/languages/vue/injections.scm b/crates/zed/src/languages/vue/injections.scm new file mode 100644 index 0000000000..9084e373f2 --- /dev/null +++ b/crates/zed/src/languages/vue/injections.scm @@ -0,0 +1,7 @@ +(script_element + (raw_text) @content + (#set! "language" "javascript")) + +(style_element + (raw_text) @content + (#set! "language" "css")) From a8e352a473eac6d3581ba3ee39bd0ee6da814752 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 11:34:13 -0600 Subject: [PATCH 067/274] 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 8db86dcebfe4a2520d737e3c8f0889a3c0152343 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 Oct 2023 11:21:45 -0700 Subject: [PATCH 068/274] Connect notification panel to notification toasts --- Cargo.lock | 1 + crates/collab/src/db/queries/notifications.rs | 18 +- crates/collab/src/rpc.rs | 35 +++- crates/collab/src/tests/following_tests.rs | 2 +- crates/collab_ui/Cargo.toml | 4 +- crates/collab_ui/src/collab_titlebar_item.rs | 28 +-- crates/collab_ui/src/collab_ui.rs | 8 +- crates/collab_ui/src/notification_panel.rs | 37 +++- crates/collab_ui/src/notifications.rs | 12 +- .../contact_notification.rs | 15 +- .../incoming_call_notification.rs | 0 .../project_shared_notification.rs | 0 .../notifications/src/notification_store.rs | 58 ++++-- crates/rpc/proto/zed.proto | 45 +++-- crates/rpc/src/proto.rs | 187 +++++++++--------- 15 files changed, 272 insertions(+), 178 deletions(-) rename crates/collab_ui/src/{ => notifications}/contact_notification.rs (91%) rename crates/collab_ui/src/{ => notifications}/incoming_call_notification.rs (100%) rename crates/collab_ui/src/{ => notifications}/project_shared_notification.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index c6d7a5ef85..8ee5449f9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,6 +1566,7 @@ dependencies = [ "project", "recent_projects", "rich_text", + "rpc", "schemars", "serde", "serde_derive", diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 8c4c511299..7c48ad42cb 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -26,11 +26,19 @@ impl Database { &self, recipient_id: UserId, limit: usize, - ) -> Result { + before_id: Option, + ) -> Result> { self.transaction(|tx| async move { - let mut result = proto::AddNotifications::default(); + let mut result = Vec::new(); + let mut condition = + Condition::all().add(notification::Column::RecipientId.eq(recipient_id)); + + if let Some(before_id) = before_id { + condition = condition.add(notification::Column::Id.lt(before_id)); + } + let mut rows = notification::Entity::find() - .filter(notification::Column::RecipientId.eq(recipient_id)) + .filter(condition) .order_by_desc(notification::Column::Id) .limit(limit as u64) .stream(&*tx) @@ -40,7 +48,7 @@ impl Database { let Some(kind) = self.notification_kinds_by_id.get(&row.kind) else { continue; }; - result.notifications.push(proto::Notification { + result.push(proto::Notification { id: row.id.to_proto(), kind: kind.to_string(), timestamp: row.created_at.assume_utc().unix_timestamp() as u64, @@ -49,7 +57,7 @@ impl Database { actor_id: row.actor_id.map(|id| id.to_proto()), }); } - result.notifications.reverse(); + result.reverse(); Ok(result) }) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 60cdaeec70..abf7ac5857 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -70,7 +70,7 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; -const INITIAL_NOTIFICATION_COUNT: usize = 30; +const NOTIFICATION_COUNT_PER_PAGE: usize = 50; lazy_static! { static ref METRIC_CONNECTIONS: IntGauge = @@ -269,6 +269,7 @@ impl Server { .add_request_handler(send_channel_message) .add_request_handler(remove_channel_message) .add_request_handler(get_channel_messages) + .add_request_handler(get_notifications) .add_request_handler(link_channel) .add_request_handler(unlink_channel) .add_request_handler(move_channel) @@ -579,17 +580,15 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, channels_for_user, channel_invites, notifications) = future::try_join4( + let (contacts, channels_for_user, channel_invites) = future::try_join3( this.app_state.db.get_contacts(user_id), this.app_state.db.get_channels_for_user(user_id), this.app_state.db.get_channel_invites_for_user(user_id), - this.app_state.db.get_notifications(user_id, INITIAL_NOTIFICATION_COUNT) ).await?; { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); - this.peer.send(connection_id, notifications)?; this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_channels_update( channels_for_user, @@ -2099,8 +2098,8 @@ async fn request_contact( session.peer.send(connection_id, update.clone())?; session.peer.send( connection_id, - proto::AddNotifications { - notifications: vec![notification.clone()], + proto::NewNotification { + notification: Some(notification.clone()), }, )?; } @@ -2158,8 +2157,8 @@ async fn respond_to_contact_request( session.peer.send(connection_id, update.clone())?; session.peer.send( connection_id, - proto::AddNotifications { - notifications: vec![notification.clone()], + proto::NewNotification { + notification: Some(notification.clone()), }, )?; } @@ -3008,6 +3007,26 @@ async fn get_channel_messages( Ok(()) } +async fn get_notifications( + request: proto::GetNotifications, + response: Response, + session: Session, +) -> Result<()> { + let notifications = session + .db() + .await + .get_notifications( + session.user_id, + NOTIFICATION_COUNT_PER_PAGE, + request + .before_id + .map(|id| db::NotificationId::from_proto(id)), + ) + .await?; + response.send(proto::GetNotificationsResponse { notifications })?; + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index f3857e3db3..a28f2ae87f 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,6 +1,6 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; -use collab_ui::project_shared_notification::ProjectSharedNotification; +use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 25f2d9f91a..4a0f8c5e8b 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -41,7 +41,8 @@ notifications = { path = "../notifications" } rich_text = { path = "../rich_text" } picker = { path = "../picker" } project = { path = "../project" } -recent_projects = {path = "../recent_projects"} +recent_projects = { path = "../recent_projects" } +rpc = { path = "../rpc" } settings = { path = "../settings" } feature_flags = {path = "../feature_flags"} theme = { path = "../theme" } @@ -68,6 +69,7 @@ editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } notifications = { path = "../notifications", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 211ee863e8..dca8f892e4 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,10 +1,10 @@ use crate::{ - contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute, - toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, + face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, + ToggleDeafen, ToggleMute, ToggleScreenSharing, }; use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; -use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; +use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore}; use clock::ReplicaId; use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ @@ -151,28 +151,6 @@ impl CollabTitlebarItem { this.window_activation_changed(active, cx) })); subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify())); - subscriptions.push( - cx.subscribe(&user_store, move |this, user_store, event, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - if let client::Event::Contact { user, kind } = event { - if let ContactEventKind::Requested | ContactEventKind::Accepted = kind { - workspace.show_notification(user.id as usize, cx, |cx| { - cx.add_view(|cx| { - ContactNotification::new( - user.clone(), - *kind, - user_store, - cx, - ) - }) - }) - } - } - }); - } - }), - ); Self { workspace: workspace.weak_handle(), diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 0a22c063be..c9a758e0ad 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -2,13 +2,10 @@ pub mod channel_view; pub mod chat_panel; pub mod collab_panel; mod collab_titlebar_item; -mod contact_notification; mod face_pile; -mod incoming_call_notification; pub mod notification_panel; -mod notifications; +pub mod notifications; mod panel_settings; -pub mod project_shared_notification; mod sharing_status_indicator; use call::{report_call_event_for_room, ActiveCall, Room}; @@ -48,8 +45,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { collab_titlebar_item::init(cx); collab_panel::init(cx); chat_panel::init(cx); - incoming_call_notification::init(&app_state, cx); - project_shared_notification::init(&app_state, cx); + notifications::init(&app_state, cx); sharing_status_indicator::init(cx); cx.add_global_action(toggle_screen_sharing); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 334d844cf5..bae2f88bc6 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -1,5 +1,7 @@ use crate::{ - format_timestamp, is_channels_feature_enabled, render_avatar, NotificationPanelSettings, + format_timestamp, is_channels_feature_enabled, + notifications::contact_notification::ContactNotification, render_avatar, + NotificationPanelSettings, }; use anyhow::Result; use channel::ChannelStore; @@ -39,6 +41,7 @@ pub struct NotificationPanel { notification_list: ListState, pending_serialization: Task>, subscriptions: Vec, + workspace: WeakViewHandle, local_timezone: UtcOffset, has_focus: bool, } @@ -64,6 +67,7 @@ impl NotificationPanel { let fs = workspace.app_state().fs.clone(); let client = workspace.app_state().client.clone(); let user_store = workspace.app_state().user_store.clone(); + let workspace_handle = workspace.weak_handle(); let notification_list = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { @@ -96,6 +100,7 @@ impl NotificationPanel { notification_store: NotificationStore::global(cx), notification_list, pending_serialization: Task::ready(None), + workspace: workspace_handle, has_focus: false, subscriptions: Vec::new(), active: false, @@ -177,7 +182,7 @@ impl NotificationPanel { let notification_store = self.notification_store.read(cx); let user_store = self.user_store.read(cx); let channel_store = self.channel_store.read(cx); - let entry = notification_store.notification_at(ix).unwrap(); + let entry = notification_store.notification_at(ix)?; let now = OffsetDateTime::now_utc(); let timestamp = entry.timestamp; @@ -293,7 +298,7 @@ impl NotificationPanel { &mut self, _: ModelHandle, event: &NotificationEvent, - _: &mut ViewContext, + cx: &mut ViewContext, ) { match event { NotificationEvent::NotificationsUpdated { @@ -301,7 +306,33 @@ impl NotificationPanel { new_count, } => { self.notification_list.splice(old_range.clone(), *new_count); + cx.notify(); } + NotificationEvent::NewNotification { entry } => match entry.notification { + Notification::ContactRequest { actor_id } + | Notification::ContactRequestAccepted { actor_id } => { + let user_store = self.user_store.clone(); + let Some(user) = user_store.read(cx).get_cached_user(actor_id) else { + return; + }; + self.workspace + .update(cx, |workspace, cx| { + workspace.show_notification(actor_id as usize, cx, |cx| { + cx.add_view(|cx| { + ContactNotification::new( + user.clone(), + entry.notification.clone(), + user_store, + cx, + ) + }) + }) + }) + .ok(); + } + Notification::ChannelInvitation { .. } => {} + Notification::ChannelMessageMention { .. } => {} + }, } } } diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 5943e016cb..e4456163c6 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -2,13 +2,23 @@ use client::User; use gpui::{ elements::*, platform::{CursorStyle, MouseButton}, - AnyElement, Element, ViewContext, + AnyElement, AppContext, Element, ViewContext, }; use std::sync::Arc; +use workspace::AppState; + +pub mod contact_notification; +pub mod incoming_call_notification; +pub mod project_shared_notification; enum Dismiss {} enum Button {} +pub fn init(app_state: &Arc, cx: &mut AppContext) { + incoming_call_notification::init(app_state, cx); + project_shared_notification::init(app_state, cx); +} + pub fn render_user_notification( user: Arc, title: &'static str, diff --git a/crates/collab_ui/src/contact_notification.rs b/crates/collab_ui/src/notifications/contact_notification.rs similarity index 91% rename from crates/collab_ui/src/contact_notification.rs rename to crates/collab_ui/src/notifications/contact_notification.rs index a998be8efd..cbd5f237f8 100644 --- a/crates/collab_ui/src/contact_notification.rs +++ b/crates/collab_ui/src/notifications/contact_notification.rs @@ -1,14 +1,13 @@ -use std::sync::Arc; - use crate::notifications::render_user_notification; use client::{ContactEventKind, User, UserStore}; use gpui::{elements::*, Entity, ModelHandle, View, ViewContext}; +use std::sync::Arc; use workspace::notifications::Notification; pub struct ContactNotification { user_store: ModelHandle, user: Arc, - kind: client::ContactEventKind, + notification: rpc::Notification, } #[derive(Clone, PartialEq)] @@ -34,8 +33,8 @@ impl View for ContactNotification { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - match self.kind { - ContactEventKind::Requested => render_user_notification( + match self.notification { + rpc::Notification::ContactRequest { .. } => render_user_notification( self.user.clone(), "wants to add you as a contact", Some("They won't be alerted if you decline."), @@ -56,7 +55,7 @@ impl View for ContactNotification { ], cx, ), - ContactEventKind::Accepted => render_user_notification( + rpc::Notification::ContactRequestAccepted { .. } => render_user_notification( self.user.clone(), "accepted your contact request", None, @@ -78,7 +77,7 @@ impl Notification for ContactNotification { impl ContactNotification { pub fn new( user: Arc, - kind: client::ContactEventKind, + notification: rpc::Notification, user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { @@ -97,7 +96,7 @@ impl ContactNotification { Self { user, - kind, + notification, user_store, } } diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs similarity index 100% rename from crates/collab_ui/src/incoming_call_notification.rs rename to crates/collab_ui/src/notifications/incoming_call_notification.rs diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs similarity index 100% rename from crates/collab_ui/src/project_shared_notification.rs rename to crates/collab_ui/src/notifications/project_shared_notification.rs diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 4ebbf46093..6583b4a4c6 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -2,7 +2,7 @@ use anyhow::Result; use channel::{ChannelMessage, ChannelMessageId, ChannelStore}; use client::{Client, UserStore}; use collections::HashMap; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, AnyNotification, Notification, TypedEnvelope}; use std::{ops::Range, sync::Arc}; use sum_tree::{Bias, SumTree}; @@ -14,7 +14,7 @@ pub fn init(client: Arc, user_store: ModelHandle, cx: &mut Ap } pub struct NotificationStore { - _client: Arc, + client: Arc, user_store: ModelHandle, channel_messages: HashMap, channel_store: ModelHandle, @@ -27,6 +27,9 @@ pub enum NotificationEvent { old_range: Range, new_count: usize, }, + NewNotification { + entry: NotificationEntry, + }, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -63,16 +66,19 @@ impl NotificationStore { user_store: ModelHandle, cx: &mut ModelContext, ) -> Self { - Self { + let this = Self { channel_store: ChannelStore::global(cx), notifications: Default::default(), channel_messages: Default::default(), _subscriptions: vec![ - client.add_message_handler(cx.handle(), Self::handle_add_notifications) + client.add_message_handler(cx.handle(), Self::handle_new_notification) ], user_store, - _client: client, - } + client, + }; + + this.load_more_notifications(cx).detach(); + this } pub fn notification_count(&self) -> usize { @@ -93,18 +99,42 @@ impl NotificationStore { cursor.item() } - async fn handle_add_notifications( + pub fn load_more_notifications(&self, cx: &mut ModelContext) -> Task> { + let request = self + .client + .request(proto::GetNotifications { before_id: None }); + cx.spawn(|this, cx| async move { + let response = request.await?; + Self::add_notifications(this, false, response.notifications, cx).await?; + Ok(()) + }) + } + + async fn handle_new_notification( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, + cx: AsyncAppContext, + ) -> Result<()> { + Self::add_notifications( + this, + true, + envelope.payload.notification.into_iter().collect(), + cx, + ) + .await + } + + async fn add_notifications( + this: ModelHandle, + is_new: bool, + notifications: Vec, mut cx: AsyncAppContext, ) -> Result<()> { let mut user_ids = Vec::new(); let mut message_ids = Vec::new(); - let notifications = envelope - .payload - .notifications + let notifications = notifications .into_iter() .filter_map(|message| { Some(NotificationEntry { @@ -195,6 +225,12 @@ impl NotificationStore { cursor.next(&()); } + if is_new { + cx.emit(NotificationEvent::NewNotification { + entry: notification.clone(), + }); + } + new_notifications.push(notification, &()); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8dca38bdfd..3f47dfaab5 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -155,25 +155,28 @@ message Envelope { UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; RejoinChannelBuffers rejoin_channel_buffers = 129; RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - AckBufferOperation ack_buffer_operation = 143; + AckBufferOperation ack_buffer_operation = 131; - JoinChannelChat join_channel_chat = 131; - JoinChannelChatResponse join_channel_chat_response = 132; - LeaveChannelChat leave_channel_chat = 133; - SendChannelMessage send_channel_message = 134; - SendChannelMessageResponse send_channel_message_response = 135; - ChannelMessageSent channel_message_sent = 136; - GetChannelMessages get_channel_messages = 137; - GetChannelMessagesResponse get_channel_messages_response = 138; - RemoveChannelMessage remove_channel_message = 139; - AckChannelMessage ack_channel_message = 144; + JoinChannelChat join_channel_chat = 132; + JoinChannelChatResponse join_channel_chat_response = 133; + LeaveChannelChat leave_channel_chat = 134; + SendChannelMessage send_channel_message = 135; + SendChannelMessageResponse send_channel_message_response = 136; + ChannelMessageSent channel_message_sent = 137; + GetChannelMessages get_channel_messages = 138; + GetChannelMessagesResponse get_channel_messages_response = 139; + RemoveChannelMessage remove_channel_message = 140; + AckChannelMessage ack_channel_message = 141; + GetChannelMessagesById get_channel_messages_by_id = 142; - LinkChannel link_channel = 140; - UnlinkChannel unlink_channel = 141; - MoveChannel move_channel = 142; + LinkChannel link_channel = 143; + UnlinkChannel unlink_channel = 144; + MoveChannel move_channel = 145; + + NewNotification new_notification = 146; + GetNotifications get_notifications = 147; + GetNotificationsResponse get_notifications_response = 148; // Current max - AddNotifications add_notifications = 145; - GetChannelMessagesById get_channel_messages_by_id = 146; // Current max } } @@ -1563,7 +1566,15 @@ message UpdateDiffBase { optional string diff_base = 3; } -message AddNotifications { +message GetNotifications { + optional uint64 before_id = 1; +} + +message NewNotification { + Notification notification = 1; +} + +message GetNotificationsResponse { repeated Notification notifications = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 4d8f60c896..eb548efd39 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -133,7 +133,8 @@ impl fmt::Display for PeerId { messages!( (Ack, Foreground), - (AddNotifications, Foreground), + (AckBufferOperation, Background), + (AckChannelMessage, Background), (AddProjectCollaborator, Foreground), (ApplyCodeAction, Background), (ApplyCodeActionResponse, Background), @@ -144,58 +145,74 @@ messages!( (Call, Foreground), (CallCanceled, Foreground), (CancelCall, Foreground), + (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), (CreateChannel, Foreground), (CreateChannelResponse, Foreground), - (ChannelMessageSent, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), (DeclineCall, Foreground), + (DeleteChannel, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), (ExpandProjectEntry, Foreground), + (ExpandProjectEntryResponse, Foreground), (Follow, Foreground), (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (FuzzySearchUsers, Foreground), + (GetChannelMembers, Foreground), + (GetChannelMembersResponse, Foreground), + (GetChannelMessages, Background), + (GetChannelMessagesById, Background), + (GetChannelMessagesResponse, Background), (GetCodeActions, Background), (GetCodeActionsResponse, Background), - (GetHover, Background), - (GetHoverResponse, Background), - (GetChannelMessages, Background), - (GetChannelMessagesResponse, Background), - (GetChannelMessagesById, Background), - (SendChannelMessage, Background), - (SendChannelMessageResponse, Background), (GetCompletions, Background), (GetCompletionsResponse, Background), (GetDefinition, Background), (GetDefinitionResponse, Background), - (GetTypeDefinition, Background), - (GetTypeDefinitionResponse, Background), (GetDocumentHighlights, Background), (GetDocumentHighlightsResponse, Background), - (GetReferences, Background), - (GetReferencesResponse, Background), + (GetHover, Background), + (GetHoverResponse, Background), + (GetNotifications, Foreground), + (GetNotificationsResponse, Foreground), + (GetPrivateUserInfo, Foreground), + (GetPrivateUserInfoResponse, Foreground), (GetProjectSymbols, Background), (GetProjectSymbolsResponse, Background), + (GetReferences, Background), + (GetReferencesResponse, Background), + (GetTypeDefinition, Background), + (GetTypeDefinitionResponse, Background), (GetUsers, Foreground), (Hello, Foreground), (IncomingCall, Foreground), + (InlayHints, Background), + (InlayHintsResponse, Background), (InviteChannelMember, Foreground), - (UsersResponse, Foreground), + (JoinChannel, Foreground), + (JoinChannelBuffer, Foreground), + (JoinChannelBufferResponse, Foreground), + (JoinChannelChat, Foreground), + (JoinChannelChatResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), - (JoinChannelChat, Foreground), - (JoinChannelChatResponse, Foreground), + (LeaveChannelBuffer, Background), (LeaveChannelChat, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), + (LinkChannel, Foreground), + (MoveChannel, Foreground), + (NewNotification, Foreground), + (OnTypeFormatting, Background), + (OnTypeFormattingResponse, Background), (OpenBufferById, Background), (OpenBufferByPath, Background), (OpenBufferForSymbol, Background), @@ -203,58 +220,54 @@ messages!( (OpenBufferResponse, Background), (PerformRename, Background), (PerformRenameResponse, Background), - (OnTypeFormatting, Background), - (OnTypeFormattingResponse, Background), - (InlayHints, Background), - (InlayHintsResponse, Background), - (ResolveInlayHint, Background), - (ResolveInlayHintResponse, Background), - (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), (PrepareRenameResponse, Background), - (ExpandProjectEntryResponse, Foreground), (ProjectEntryResponse, Foreground), + (RefreshInlayHints, Foreground), + (RejoinChannelBuffers, Foreground), + (RejoinChannelBuffersResponse, Foreground), (RejoinRoom, Foreground), (RejoinRoomResponse, Foreground), - (RemoveContact, Foreground), - (RemoveChannelMember, Foreground), - (RemoveChannelMessage, Foreground), (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), + (RemoveChannelMember, Foreground), + (RemoveChannelMessage, Foreground), + (RemoveContact, Foreground), (RemoveProjectCollaborator, Foreground), - (RenameProjectEntry, Foreground), - (RequestContact, Foreground), - (RespondToContactRequest, Foreground), - (RespondToChannelInvite, Foreground), - (JoinChannel, Foreground), - (RoomUpdated, Foreground), - (SaveBuffer, Foreground), (RenameChannel, Foreground), (RenameChannelResponse, Foreground), - (SetChannelMemberAdmin, Foreground), + (RenameProjectEntry, Foreground), + (RequestContact, Foreground), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), + (RespondToChannelInvite, Foreground), + (RespondToContactRequest, Foreground), + (RoomUpdated, Foreground), + (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), + (SendChannelMessage, Background), + (SendChannelMessageResponse, Background), + (SetChannelMemberAdmin, Foreground), (ShareProject, Foreground), (ShareProjectResponse, Foreground), (ShowContacts, Foreground), (StartLanguageServer, Foreground), (SynchronizeBuffers, Foreground), (SynchronizeBuffersResponse, Foreground), - (RejoinChannelBuffers, Foreground), - (RejoinChannelBuffersResponse, Foreground), (Test, Foreground), (Unfollow, Foreground), + (UnlinkChannel, Foreground), (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), - (UpdateContacts, Foreground), - (DeleteChannel, Foreground), - (MoveChannel, Foreground), - (LinkChannel, Foreground), - (UnlinkChannel, Foreground), + (UpdateChannelBuffer, Foreground), + (UpdateChannelBufferCollaborators, Foreground), (UpdateChannels, Foreground), + (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), + (UpdateDiffBase, Foreground), (UpdateFollowers, Foreground), (UpdateInviteInfo, Foreground), (UpdateLanguageServer, Foreground), @@ -263,18 +276,7 @@ messages!( (UpdateProjectCollaborator, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), - (UpdateDiffBase, Foreground), - (GetPrivateUserInfo, Foreground), - (GetPrivateUserInfoResponse, Foreground), - (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground), - (JoinChannelBuffer, Foreground), - (JoinChannelBufferResponse, Foreground), - (LeaveChannelBuffer, Background), - (UpdateChannelBuffer, Foreground), - (UpdateChannelBufferCollaborators, Foreground), - (AckBufferOperation, Background), - (AckChannelMessage, Background), + (UsersResponse, Foreground), ); request_messages!( @@ -286,73 +288,74 @@ request_messages!( (Call, Ack), (CancelCall, Ack), (CopyProjectEntry, ProjectEntryResponse), + (CreateChannel, CreateChannelResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), - (CreateChannel, CreateChannelResponse), (DeclineCall, Ack), + (DeleteChannel, Ack), (DeleteProjectEntry, ProjectEntryResponse), (ExpandProjectEntry, ExpandProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), + (FuzzySearchUsers, UsersResponse), + (GetChannelMembers, GetChannelMembersResponse), + (GetChannelMessages, GetChannelMessagesResponse), + (GetChannelMessagesById, GetChannelMessagesResponse), (GetCodeActions, GetCodeActionsResponse), - (GetHover, GetHoverResponse), (GetCompletions, GetCompletionsResponse), (GetDefinition, GetDefinitionResponse), - (GetTypeDefinition, GetTypeDefinitionResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse), - (GetReferences, GetReferencesResponse), + (GetHover, GetHoverResponse), + (GetNotifications, GetNotificationsResponse), (GetPrivateUserInfo, GetPrivateUserInfoResponse), (GetProjectSymbols, GetProjectSymbolsResponse), - (FuzzySearchUsers, UsersResponse), + (GetReferences, GetReferencesResponse), + (GetTypeDefinition, GetTypeDefinitionResponse), (GetUsers, UsersResponse), + (IncomingCall, Ack), + (InlayHints, InlayHintsResponse), (InviteChannelMember, Ack), + (JoinChannel, JoinRoomResponse), + (JoinChannelBuffer, JoinChannelBufferResponse), + (JoinChannelChat, JoinChannelChatResponse), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), - (JoinChannelChat, JoinChannelChatResponse), + (LeaveChannelBuffer, Ack), (LeaveRoom, Ack), - (RejoinRoom, RejoinRoomResponse), - (IncomingCall, Ack), + (LinkChannel, Ack), + (MoveChannel, Ack), + (OnTypeFormatting, OnTypeFormattingResponse), (OpenBufferById, OpenBufferResponse), (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), - (Ping, Ack), (PerformRename, PerformRenameResponse), + (Ping, Ack), (PrepareRename, PrepareRenameResponse), - (OnTypeFormatting, OnTypeFormattingResponse), - (InlayHints, InlayHintsResponse), - (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), + (RejoinChannelBuffers, RejoinChannelBuffersResponse), + (RejoinRoom, RejoinRoomResponse), (ReloadBuffers, ReloadBuffersResponse), - (RequestContact, Ack), (RemoveChannelMember, Ack), - (RemoveContact, Ack), - (RespondToContactRequest, Ack), - (RespondToChannelInvite, Ack), - (SetChannelMemberAdmin, Ack), - (SendChannelMessage, SendChannelMessageResponse), - (GetChannelMessages, GetChannelMessagesResponse), - (GetChannelMessagesById, GetChannelMessagesResponse), - (GetChannelMembers, GetChannelMembersResponse), - (JoinChannel, JoinRoomResponse), (RemoveChannelMessage, Ack), - (DeleteChannel, Ack), - (RenameProjectEntry, ProjectEntryResponse), + (RemoveContact, Ack), (RenameChannel, RenameChannelResponse), - (LinkChannel, Ack), - (UnlinkChannel, Ack), - (MoveChannel, Ack), + (RenameProjectEntry, ProjectEntryResponse), + (RequestContact, Ack), + (ResolveInlayHint, ResolveInlayHintResponse), + (RespondToChannelInvite, Ack), + (RespondToContactRequest, Ack), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), + (SendChannelMessage, SendChannelMessageResponse), + (SetChannelMemberAdmin, Ack), (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), - (RejoinChannelBuffers, RejoinChannelBuffersResponse), (Test, Test), + (UnlinkChannel, Ack), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), - (JoinChannelBuffer, JoinChannelBufferResponse), - (LeaveChannelBuffer, Ack) ); entity_messages!( @@ -371,25 +374,25 @@ entity_messages!( GetCodeActions, GetCompletions, GetDefinition, - GetTypeDefinition, GetDocumentHighlights, GetHover, - GetReferences, GetProjectSymbols, + GetReferences, + GetTypeDefinition, + InlayHints, JoinProject, LeaveProject, + OnTypeFormatting, OpenBufferById, OpenBufferByPath, OpenBufferForSymbol, PerformRename, - OnTypeFormatting, - InlayHints, - ResolveInlayHint, - RefreshInlayHints, PrepareRename, + RefreshInlayHints, ReloadBuffers, RemoveProjectCollaborator, RenameProjectEntry, + ResolveInlayHint, SaveBuffer, SearchProject, StartLanguageServer, @@ -398,19 +401,19 @@ entity_messages!( UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateDiffBase, UpdateLanguageServer, UpdateProject, UpdateProjectCollaborator, UpdateWorktree, UpdateWorktreeSettings, - UpdateDiffBase ); entity_messages!( channel_id, ChannelMessageSent, - UpdateChannelBuffer, RemoveChannelMessage, + UpdateChannelBuffer, UpdateChannelBufferCollaborators, ); From bc6ba5f547496853ed37a3da709b830f83b22d0b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 Oct 2023 11:23:39 -0700 Subject: [PATCH 069/274] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 4bf90669b2..682ba6ac73 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 64; +pub const PROTOCOL_VERSION: u32 = 65; From 39e3ddb0803f77c2bd4e1600fd3b363ad6c38353 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 13 Oct 2023 15:00:32 -0400 Subject: [PATCH 070/274] Update bell.svg --- assets/icons/bell.svg | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg index 46b01b6b38..ea1c6dd42e 100644 --- a/assets/icons/bell.svg +++ b/assets/icons/bell.svg @@ -1,3 +1,8 @@ - - + + From 9c6f5de551ca9cf81ef08428d2ee5b24fe8e05a3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 13 Oct 2023 13:17:19 -0600 Subject: [PATCH 071/274] 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 072/274] 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 073/274] 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 074/274] 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 075/274] 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 076/274] 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 077/274] 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 83fb8d20b7b49e51209ae9582d0980e75577a4a8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 Oct 2023 15:37:08 -0700 Subject: [PATCH 078/274] Remove contact notifications when cancelling a contact request --- crates/channel/src/channel_store.rs | 3 - crates/collab/src/db/queries/contacts.rs | 22 ++- crates/collab/src/db/queries/notifications.rs | 45 ++++++- crates/collab/src/rpc.rs | 11 +- crates/collab_ui/src/notification_panel.rs | 66 +++++---- .../src/notifications/contact_notification.rs | 16 +-- .../notifications/src/notification_store.rs | 126 +++++++++++++----- crates/rpc/proto/zed.proto | 7 +- crates/rpc/src/notification.rs | 14 +- crates/rpc/src/proto.rs | 1 + 10 files changed, 224 insertions(+), 87 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 4a1292cdb2..918a1e1dc1 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -127,9 +127,6 @@ impl ChannelStore { this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx)); } } - if status.is_connected() { - } else { - } } Some(()) }); diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index f02bae667a..ddb7959ef2 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -185,7 +185,11 @@ impl Database { /// /// * `requester_id` - The user that initiates this request /// * `responder_id` - The user that will be removed - pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result { + pub async fn remove_contact( + &self, + requester_id: UserId, + responder_id: UserId, + ) -> Result<(bool, Option)> { self.transaction(|tx| async move { let (id_a, id_b) = if responder_id < requester_id { (responder_id, requester_id) @@ -204,7 +208,21 @@ impl Database { .ok_or_else(|| anyhow!("no such contact"))?; contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; - Ok(contact.accepted) + + let mut deleted_notification_id = None; + if !contact.accepted { + deleted_notification_id = self + .delete_notification( + responder_id, + rpc::Notification::ContactRequest { + actor_id: requester_id.to_proto(), + }, + &*tx, + ) + .await?; + } + + Ok((contact.accepted, deleted_notification_id)) }) .await } diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 7c48ad42cb..2ea5fd149f 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -3,12 +3,12 @@ use rpc::Notification; impl Database { pub async fn initialize_notification_enum(&mut self) -> Result<()> { - notification_kind::Entity::insert_many(Notification::all_kinds().iter().map(|kind| { - notification_kind::ActiveModel { + notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map( + |kind| notification_kind::ActiveModel { name: ActiveValue::Set(kind.to_string()), ..Default::default() - } - })) + }, + )) .on_conflict(OnConflict::new().do_nothing().to_owned()) .exec_without_returning(&self.pool) .await?; @@ -19,6 +19,12 @@ impl Database { self.notification_kinds_by_name.insert(row.name, row.id); } + for name in Notification::all_variant_names() { + if let Some(id) = self.notification_kinds_by_name.get(*name).copied() { + self.notification_kinds_by_id.insert(id, name); + } + } + Ok(()) } @@ -46,6 +52,7 @@ impl Database { while let Some(row) = rows.next().await { let row = row?; let Some(kind) = self.notification_kinds_by_id.get(&row.kind) else { + log::warn!("unknown notification kind {:?}", row.kind); continue; }; result.push(proto::Notification { @@ -96,4 +103,34 @@ impl Database { actor_id: notification.actor_id, }) } + + pub async fn delete_notification( + &self, + recipient_id: UserId, + notification: Notification, + tx: &DatabaseTransaction, + ) -> Result> { + let notification = notification.to_any(); + let kind = *self + .notification_kinds_by_name + .get(notification.kind.as_ref()) + .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification.kind))?; + let actor_id = notification.actor_id.map(|id| UserId::from_proto(id)); + let notification = notification::Entity::find() + .filter( + Condition::all() + .add(notification::Column::RecipientId.eq(recipient_id)) + .add(notification::Column::Kind.eq(kind)) + .add(notification::Column::ActorId.eq(actor_id)) + .add(notification::Column::Content.eq(notification.content)), + ) + .one(tx) + .await?; + if let Some(notification) = ¬ification { + notification::Entity::delete_by_id(notification.id) + .exec(tx) + .await?; + } + Ok(notification.map(|notification| notification.id)) + } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 921ebccfb1..7a3cdb13ab 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2177,7 +2177,8 @@ async fn remove_contact( let requester_id = session.user_id; let responder_id = UserId::from_proto(request.user_id); let db = session.db().await; - let contact_accepted = db.remove_contact(requester_id, responder_id).await?; + let (contact_accepted, deleted_notification_id) = + db.remove_contact(requester_id, responder_id).await?; let pool = session.connection_pool().await; // Update outgoing contact requests of requester @@ -2204,6 +2205,14 @@ async fn remove_contact( } for connection_id in pool.user_connection_ids(responder_id) { session.peer.send(connection_id, update.clone())?; + if let Some(notification_id) = deleted_notification_id { + session.peer.send( + connection_id, + proto::DeleteNotification { + notification_id: notification_id.to_proto(), + }, + )?; + } } response.send(proto::Ack {})?; diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index bae2f88bc6..978255a081 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -301,6 +301,8 @@ impl NotificationPanel { cx: &mut ViewContext, ) { match event { + NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx), + NotificationEvent::NotificationRemoved { entry } => self.remove_toast(entry, cx), NotificationEvent::NotificationsUpdated { old_range, new_count, @@ -308,31 +310,49 @@ impl NotificationPanel { self.notification_list.splice(old_range.clone(), *new_count); cx.notify(); } - NotificationEvent::NewNotification { entry } => match entry.notification { - Notification::ContactRequest { actor_id } - | Notification::ContactRequestAccepted { actor_id } => { - let user_store = self.user_store.clone(); - let Some(user) = user_store.read(cx).get_cached_user(actor_id) else { - return; - }; - self.workspace - .update(cx, |workspace, cx| { - workspace.show_notification(actor_id as usize, cx, |cx| { - cx.add_view(|cx| { - ContactNotification::new( - user.clone(), - entry.notification.clone(), - user_store, - cx, - ) - }) + } + } + + fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { + let id = entry.id as usize; + match entry.notification { + Notification::ContactRequest { actor_id } + | Notification::ContactRequestAccepted { actor_id } => { + let user_store = self.user_store.clone(); + let Some(user) = user_store.read(cx).get_cached_user(actor_id) else { + return; + }; + self.workspace + .update(cx, |workspace, cx| { + workspace.show_notification(id, cx, |cx| { + cx.add_view(|_| { + ContactNotification::new( + user, + entry.notification.clone(), + user_store, + ) }) }) - .ok(); - } - Notification::ChannelInvitation { .. } => {} - Notification::ChannelMessageMention { .. } => {} - }, + }) + .ok(); + } + Notification::ChannelInvitation { .. } => {} + Notification::ChannelMessageMention { .. } => {} + } + } + + fn remove_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { + let id = entry.id as usize; + match entry.notification { + Notification::ContactRequest { .. } | Notification::ContactRequestAccepted { .. } => { + self.workspace + .update(cx, |workspace, cx| { + workspace.dismiss_notification::(id, cx) + }) + .ok(); + } + Notification::ChannelInvitation { .. } => {} + Notification::ChannelMessageMention { .. } => {} } } } diff --git a/crates/collab_ui/src/notifications/contact_notification.rs b/crates/collab_ui/src/notifications/contact_notification.rs index cbd5f237f8..2e3c3ca58a 100644 --- a/crates/collab_ui/src/notifications/contact_notification.rs +++ b/crates/collab_ui/src/notifications/contact_notification.rs @@ -1,5 +1,5 @@ use crate::notifications::render_user_notification; -use client::{ContactEventKind, User, UserStore}; +use client::{User, UserStore}; use gpui::{elements::*, Entity, ModelHandle, View, ViewContext}; use std::sync::Arc; use workspace::notifications::Notification; @@ -79,21 +79,7 @@ impl ContactNotification { user: Arc, notification: rpc::Notification, user_store: ModelHandle, - cx: &mut ViewContext, ) -> Self { - cx.subscribe(&user_store, move |this, _, event, cx| { - if let client::Event::Contact { - kind: ContactEventKind::Cancelled, - user, - } = event - { - if user.id == this.user.id { - cx.emit(Event::Dismiss); - } - } - }) - .detach(); - Self { user, notification, diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 6583b4a4c6..087637a100 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -2,11 +2,13 @@ use anyhow::Result; use channel::{ChannelMessage, ChannelMessageId, ChannelStore}; use client::{Client, UserStore}; use collections::HashMap; +use db::smol::stream::StreamExt; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, AnyNotification, Notification, TypedEnvelope}; use std::{ops::Range, sync::Arc}; use sum_tree::{Bias, SumTree}; use time::OffsetDateTime; +use util::ResultExt; pub fn init(client: Arc, user_store: ModelHandle, cx: &mut AppContext) { let notification_store = cx.add_model(|cx| NotificationStore::new(client, user_store, cx)); @@ -19,6 +21,7 @@ pub struct NotificationStore { channel_messages: HashMap, channel_store: ModelHandle, notifications: SumTree, + _watch_connection_status: Task>, _subscriptions: Vec, } @@ -30,6 +33,9 @@ pub enum NotificationEvent { NewNotification { entry: NotificationEntry, }, + NotificationRemoved { + entry: NotificationEntry, + }, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -66,19 +72,34 @@ impl NotificationStore { user_store: ModelHandle, cx: &mut ModelContext, ) -> Self { - let this = Self { + let mut connection_status = client.status(); + let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { + while let Some(status) = connection_status.next().await { + let this = this.upgrade(&cx)?; + match status { + client::Status::Connected { .. } => { + this.update(&mut cx, |this, cx| this.handle_connect(cx)) + .await + .log_err()?; + } + _ => this.update(&mut cx, |this, cx| this.handle_disconnect(cx)), + } + } + Some(()) + }); + + Self { channel_store: ChannelStore::global(cx), notifications: Default::default(), channel_messages: Default::default(), + _watch_connection_status: watch_connection_status, _subscriptions: vec![ - client.add_message_handler(cx.handle(), Self::handle_new_notification) + client.add_message_handler(cx.handle(), Self::handle_new_notification), + client.add_message_handler(cx.handle(), Self::handle_delete_notification), ], user_store, client, - }; - - this.load_more_notifications(cx).detach(); - this + } } pub fn notification_count(&self) -> usize { @@ -110,6 +131,16 @@ impl NotificationStore { }) } + fn handle_connect(&mut self, cx: &mut ModelContext) -> Task> { + self.notifications = Default::default(); + self.channel_messages = Default::default(); + self.load_more_notifications(cx) + } + + fn handle_disconnect(&mut self, cx: &mut ModelContext) { + cx.notify() + } + async fn handle_new_notification( this: ModelHandle, envelope: TypedEnvelope, @@ -125,6 +156,18 @@ impl NotificationStore { .await } + async fn handle_delete_notification( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.splice_notifications([(envelope.payload.notification_id, None)], false, cx); + Ok(()) + }) + } + async fn add_notifications( this: ModelHandle, is_new: bool, @@ -205,26 +248,47 @@ impl NotificationStore { } })); - let mut cursor = this.notifications.cursor::<(NotificationId, Count)>(); - let mut new_notifications = SumTree::new(); - let mut old_range = 0..0; - for (i, notification) in notifications.into_iter().enumerate() { - new_notifications.append( - cursor.slice(&NotificationId(notification.id), Bias::Left, &()), - &(), - ); + this.splice_notifications( + notifications + .into_iter() + .map(|notification| (notification.id, Some(notification))), + is_new, + cx, + ); + }); - if i == 0 { - old_range.start = cursor.start().1 .0; - } + Ok(()) + } - if cursor - .item() - .map_or(true, |existing| existing.id != notification.id) - { + fn splice_notifications( + &mut self, + notifications: impl IntoIterator)>, + is_new: bool, + cx: &mut ModelContext<'_, NotificationStore>, + ) { + let mut cursor = self.notifications.cursor::<(NotificationId, Count)>(); + let mut new_notifications = SumTree::new(); + let mut old_range = 0..0; + + for (i, (id, new_notification)) in notifications.into_iter().enumerate() { + new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left, &()), &()); + + if i == 0 { + old_range.start = cursor.start().1 .0; + } + + if let Some(existing_notification) = cursor.item() { + if existing_notification.id == id { + if new_notification.is_none() { + cx.emit(NotificationEvent::NotificationRemoved { + entry: existing_notification.clone(), + }); + } cursor.next(&()); } + } + if let Some(notification) = new_notification { if is_new { cx.emit(NotificationEvent::NewNotification { entry: notification.clone(), @@ -233,20 +297,18 @@ impl NotificationStore { new_notifications.push(notification, &()); } + } - old_range.end = cursor.start().1 .0; - let new_count = new_notifications.summary().count; - new_notifications.append(cursor.suffix(&()), &()); - drop(cursor); + old_range.end = cursor.start().1 .0; + let new_count = new_notifications.summary().count - old_range.start; + new_notifications.append(cursor.suffix(&()), &()); + drop(cursor); - this.notifications = new_notifications; - cx.emit(NotificationEvent::NotificationsUpdated { - old_range, - new_count, - }); + self.notifications = new_notifications; + cx.emit(NotificationEvent::NotificationsUpdated { + old_range, + new_count, }); - - Ok(()) } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 30e43dc43b..d27bbade6f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -177,7 +177,8 @@ message Envelope { NewNotification new_notification = 148; GetNotifications get_notifications = 149; - GetNotificationsResponse get_notifications_response = 150; // Current max + GetNotificationsResponse get_notifications_response = 150; + DeleteNotification delete_notification = 151; // Current max } } @@ -1590,6 +1591,10 @@ message GetNotificationsResponse { repeated Notification notifications = 1; } +message DeleteNotification { + uint64 notification_id = 1; +} + message Notification { uint64 id = 1; uint64 timestamp = 2; diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 8aabb9b9df..8224c2696c 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{map, Value}; use std::borrow::Cow; use strum::{EnumVariantNames, IntoStaticStr, VariantNames as _}; @@ -47,10 +47,12 @@ impl Notification { let mut value = serde_json::to_value(self).unwrap(); let mut actor_id = None; if let Some(value) = value.as_object_mut() { - value.remove("kind"); - actor_id = value - .remove("actor_id") - .and_then(|value| Some(value.as_i64()? as u64)); + value.remove(KIND); + if let map::Entry::Occupied(e) = value.entry(ACTOR_ID) { + if e.get().is_u64() { + actor_id = e.remove().as_u64(); + } + } } AnyNotification { kind: Cow::Borrowed(kind), @@ -69,7 +71,7 @@ impl Notification { serde_json::from_value(value).ok() } - pub fn all_kinds() -> &'static [&'static str] { + pub fn all_variant_names() -> &'static [&'static str] { Self::VARIANTS } } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index bca56e9c77..b2a72c4ce1 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -155,6 +155,7 @@ messages!( (CreateRoomResponse, Foreground), (DeclineCall, Foreground), (DeleteChannel, Foreground), + (DeleteNotification, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), (ExpandProjectEntry, Foreground), From 5a0afcc83541a725b3dc140b1982b159f671abfd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 Oct 2023 15:49:31 -0700 Subject: [PATCH 079/274] Simplify notification serialization --- crates/collab/src/db.rs | 3 +- crates/collab/src/db/queries/notifications.rs | 8 +-- .../notifications/src/notification_store.rs | 8 +-- crates/rpc/src/notification.rs | 50 ++++++++----------- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 67055d27ee..1bf5c95f6b 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -13,6 +13,7 @@ use anyhow::anyhow; use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; +use queries::channels::ChannelGraph; use rand::{prelude::StdRng, Rng, SeedableRng}; use rpc::{ proto::{self}, @@ -47,8 +48,6 @@ pub use ids::*; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; -use self::queries::channels::ChannelGraph; - pub struct Database { options: ConnectOptions, pool: DatabaseConnection, diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 2ea5fd149f..bf9c9d74ef 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -76,10 +76,10 @@ impl Database { notification: Notification, tx: &DatabaseTransaction, ) -> Result { - let notification = notification.to_any(); + let notification = notification.to_proto(); let kind = *self .notification_kinds_by_name - .get(notification.kind.as_ref()) + .get(¬ification.kind) .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification.kind))?; let model = notification::ActiveModel { @@ -110,10 +110,10 @@ impl Database { notification: Notification, tx: &DatabaseTransaction, ) -> Result> { - let notification = notification.to_any(); + let notification = notification.to_proto(); let kind = *self .notification_kinds_by_name - .get(notification.kind.as_ref()) + .get(¬ification.kind) .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification.kind))?; let actor_id = notification.actor_id.map(|id| UserId::from_proto(id)); let notification = notification::Entity::find() diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 087637a100..af39941d2f 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -4,7 +4,7 @@ use client::{Client, UserStore}; use collections::HashMap; use db::smol::stream::StreamExt; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; -use rpc::{proto, AnyNotification, Notification, TypedEnvelope}; +use rpc::{proto, Notification, TypedEnvelope}; use std::{ops::Range, sync::Arc}; use sum_tree::{Bias, SumTree}; use time::OffsetDateTime; @@ -185,11 +185,7 @@ impl NotificationStore { is_read: message.is_read, timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) .ok()?, - notification: Notification::from_any(&AnyNotification { - actor_id: message.actor_id, - kind: message.kind.into(), - content: message.content, - })?, + notification: Notification::from_proto(&message)?, }) }) .collect::>(); diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 8224c2696c..6ff9660159 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -1,7 +1,7 @@ +use crate::proto; use serde::{Deserialize, Serialize}; use serde_json::{map, Value}; -use std::borrow::Cow; -use strum::{EnumVariantNames, IntoStaticStr, VariantNames as _}; +use strum::{EnumVariantNames, VariantNames as _}; const KIND: &'static str = "kind"; const ACTOR_ID: &'static str = "actor_id"; @@ -9,10 +9,12 @@ const ACTOR_ID: &'static str = "actor_id"; /// A notification that can be stored, associated with a given user. /// /// This struct is stored in the collab database as JSON, so it shouldn't be -/// changed in a backward-incompatible way. +/// changed in a backward-incompatible way. For example, when renaming a +/// variant, add a serde alias for the old name. /// -/// For example, when renaming a variant, add a serde alias for the old name. -#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr, Serialize, Deserialize)] +/// When a notification is initiated by a user, use the `actor_id` field +/// to store the user's id. +#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum Notification { ContactRequest { @@ -32,36 +34,28 @@ pub enum Notification { }, } -/// The representation of a notification that is stored in the database and -/// sent over the wire. -#[derive(Debug)] -pub struct AnyNotification { - pub kind: Cow<'static, str>, - pub actor_id: Option, - pub content: String, -} - impl Notification { - pub fn to_any(&self) -> AnyNotification { - let kind: &'static str = self.into(); + pub fn to_proto(&self) -> proto::Notification { let mut value = serde_json::to_value(self).unwrap(); let mut actor_id = None; - if let Some(value) = value.as_object_mut() { - value.remove(KIND); - if let map::Entry::Occupied(e) = value.entry(ACTOR_ID) { - if e.get().is_u64() { - actor_id = e.remove().as_u64(); - } + let value = value.as_object_mut().unwrap(); + let Some(Value::String(kind)) = value.remove(KIND) else { + unreachable!() + }; + if let map::Entry::Occupied(e) = value.entry(ACTOR_ID) { + if e.get().is_u64() { + actor_id = e.remove().as_u64(); } } - AnyNotification { - kind: Cow::Borrowed(kind), + proto::Notification { + kind, actor_id, content: serde_json::to_string(&value).unwrap(), + ..Default::default() } } - pub fn from_any(notification: &AnyNotification) -> Option { + pub fn from_proto(notification: &proto::Notification) -> Option { let mut value = serde_json::from_str::(¬ification.content).ok()?; let object = value.as_object_mut()?; object.insert(KIND.into(), notification.kind.to_string().into()); @@ -92,13 +86,13 @@ fn test_notification() { message_id: 1, }, ] { - let serialized = notification.to_any(); - let deserialized = Notification::from_any(&serialized).unwrap(); + let message = notification.to_proto(); + let deserialized = Notification::from_proto(&message).unwrap(); assert_eq!(deserialized, notification); } // When notifications are serialized, the `kind` and `actor_id` fields are // stored separately, and do not appear redundantly in the JSON. let notification = Notification::ContactRequest { actor_id: 1 }; - assert_eq!(notification.to_any().content, "{}"); + assert_eq!(notification.to_proto().content, "{}"); } From cb7b011d6be5ba45022c62446448a2a46afe6341 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 Oct 2023 16:57:28 -0700 Subject: [PATCH 080/274] Avoid creating duplicate invite notifications --- .../20221109000000_test_schema.sql | 2 +- crates/collab/src/db/queries/channels.rs | 13 ++- crates/collab/src/db/queries/contacts.rs | 8 +- crates/collab/src/db/queries/notifications.rs | 83 +++++++++++++------ crates/collab/src/rpc.rs | 40 ++++++--- crates/collab_ui/src/notification_panel.rs | 7 +- 6 files changed, 109 insertions(+), 44 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index a10155fd1d..4372d7dc8a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -330,4 +330,4 @@ CREATE TABLE "notifications" ( "content" TEXT ); -CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); +CREATE INDEX "index_notifications_on_recipient_id_is_read" ON "notifications" ("recipient_id", "is_read"); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index c576d2406b..d64b8028e3 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -161,7 +161,7 @@ impl Database { invitee_id: UserId, inviter_id: UserId, is_admin: bool, - ) -> Result<()> { + ) -> Result> { self.transaction(move |tx| async move { self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) .await?; @@ -176,7 +176,16 @@ impl Database { .insert(&*tx) .await?; - Ok(()) + self.create_notification( + invitee_id, + rpc::Notification::ChannelInvitation { + actor_id: inviter_id.to_proto(), + channel_id: channel_id.to_proto(), + }, + true, + &*tx, + ) + .await }) .await } diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index ddb7959ef2..709ed941f7 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -123,7 +123,7 @@ impl Database { &self, sender_id: UserId, receiver_id: UserId, - ) -> Result { + ) -> Result> { self.transaction(|tx| async move { let (id_a, id_b, a_to_b) = if sender_id < receiver_id { (sender_id, receiver_id, true) @@ -169,6 +169,7 @@ impl Database { rpc::Notification::ContactRequest { actor_id: sender_id.to_proto(), }, + true, &*tx, ) .await @@ -212,7 +213,7 @@ impl Database { let mut deleted_notification_id = None; if !contact.accepted { deleted_notification_id = self - .delete_notification( + .remove_notification( responder_id, rpc::Notification::ContactRequest { actor_id: requester_id.to_proto(), @@ -273,7 +274,7 @@ impl Database { responder_id: UserId, requester_id: UserId, accept: bool, - ) -> Result { + ) -> Result> { self.transaction(|tx| async move { let (id_a, id_b, a_to_b) = if responder_id < requester_id { (responder_id, requester_id, false) @@ -320,6 +321,7 @@ impl Database { rpc::Notification::ContactRequestAccepted { actor_id: responder_id.to_proto(), }, + true, &*tx, ) .await diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index bf9c9d74ef..b8b2a15421 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -51,18 +51,12 @@ impl Database { .await?; while let Some(row) = rows.next().await { let row = row?; - let Some(kind) = self.notification_kinds_by_id.get(&row.kind) else { - log::warn!("unknown notification kind {:?}", row.kind); - continue; - }; - result.push(proto::Notification { - id: row.id.to_proto(), - kind: kind.to_string(), - timestamp: row.created_at.assume_utc().unix_timestamp() as u64, - is_read: row.is_read, - content: row.content, - actor_id: row.actor_id.map(|id| id.to_proto()), - }); + let kind = row.kind; + if let Some(proto) = self.model_to_proto(row) { + result.push(proto); + } else { + log::warn!("unknown notification kind {:?}", kind); + } } result.reverse(); Ok(result) @@ -74,19 +68,48 @@ impl Database { &self, recipient_id: UserId, notification: Notification, + avoid_duplicates: bool, tx: &DatabaseTransaction, - ) -> Result { - let notification = notification.to_proto(); + ) -> Result> { + let notification_proto = notification.to_proto(); let kind = *self .notification_kinds_by_name - .get(¬ification.kind) - .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification.kind))?; + .get(¬ification_proto.kind) + .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification_proto.kind))?; + let actor_id = notification_proto.actor_id.map(|id| UserId::from_proto(id)); + + if avoid_duplicates { + let mut existing_notifications = notification::Entity::find() + .filter( + Condition::all() + .add(notification::Column::RecipientId.eq(recipient_id)) + .add(notification::Column::IsRead.eq(false)) + .add(notification::Column::Kind.eq(kind)) + .add(notification::Column::ActorId.eq(actor_id)), + ) + .stream(&*tx) + .await?; + + // Check if this notification already exists. Don't rely on the + // JSON serialization being identical, in case the notification enum + // is changed in backward-compatible ways over time. + while let Some(row) = existing_notifications.next().await { + let row = row?; + if let Some(proto) = self.model_to_proto(row) { + if let Some(existing) = Notification::from_proto(&proto) { + if existing == notification { + return Ok(None); + } + } + } + } + } let model = notification::ActiveModel { recipient_id: ActiveValue::Set(recipient_id), kind: ActiveValue::Set(kind), - content: ActiveValue::Set(notification.content.clone()), - actor_id: ActiveValue::Set(notification.actor_id.map(|id| UserId::from_proto(id))), + content: ActiveValue::Set(notification_proto.content.clone()), + actor_id: ActiveValue::Set(actor_id), is_read: ActiveValue::NotSet, created_at: ActiveValue::NotSet, id: ActiveValue::NotSet, @@ -94,17 +117,17 @@ impl Database { .save(&*tx) .await?; - Ok(proto::Notification { + Ok(Some(proto::Notification { id: model.id.as_ref().to_proto(), - kind: notification.kind.to_string(), + kind: notification_proto.kind.to_string(), timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, is_read: false, - content: notification.content, - actor_id: notification.actor_id, - }) + content: notification_proto.content, + actor_id: notification_proto.actor_id, + })) } - pub async fn delete_notification( + pub async fn remove_notification( &self, recipient_id: UserId, notification: Notification, @@ -133,4 +156,16 @@ impl Database { } Ok(notification.map(|notification| notification.id)) } + + fn model_to_proto(&self, row: notification::Model) -> Option { + let kind = self.notification_kinds_by_id.get(&row.kind)?; + Some(proto::Notification { + id: row.id.to_proto(), + kind: kind.to_string(), + timestamp: row.created_at.assume_utc().unix_timestamp() as u64, + is_read: row.is_read, + content: row.content, + actor_id: row.actor_id.map(|id| id.to_proto()), + }) + } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7a3cdb13ab..cd82490649 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2097,12 +2097,14 @@ async fn request_contact( .user_connection_ids(responder_id) { session.peer.send(connection_id, update.clone())?; - session.peer.send( - connection_id, - proto::NewNotification { - notification: Some(notification.clone()), - }, - )?; + if let Some(notification) = ¬ification { + session.peer.send( + connection_id, + proto::NewNotification { + notification: Some(notification.clone()), + }, + )?; + } } response.send(proto::Ack {})?; @@ -2156,12 +2158,14 @@ async fn respond_to_contact_request( .push(responder_id.to_proto()); for connection_id in pool.user_connection_ids(requester_id) { session.peer.send(connection_id, update.clone())?; - session.peer.send( - connection_id, - proto::NewNotification { - notification: Some(notification.clone()), - }, - )?; + if let Some(notification) = ¬ification { + session.peer.send( + connection_id, + proto::NewNotification { + notification: Some(notification.clone()), + }, + )?; + } } } @@ -2306,7 +2310,8 @@ 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 notification = db + .invite_channel_member(channel_id, invitee_id, session.user_id, request.admin) .await?; let (channel, _) = db @@ -2319,12 +2324,21 @@ async fn invite_channel_member( id: channel.id.to_proto(), name: channel.name, }); + for connection_id in session .connection_pool() .await .user_connection_ids(invitee_id) { session.peer.send(connection_id, update.clone())?; + if let Some(notification) = ¬ification { + session.peer.send( + connection_id, + proto::NewNotification { + notification: Some(notification.clone()), + }, + )?; + } } response.send(proto::Ack {})?; diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 978255a081..9f69b7144c 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -209,7 +209,12 @@ impl NotificationPanel { channel_id, } => { actor = user_store.get_cached_user(inviter_id)?; - let channel = channel_store.channel_for_id(channel_id)?; + let channel = channel_store.channel_for_id(channel_id).or_else(|| { + channel_store + .channel_invitations() + .iter() + .find(|c| c.id == channel_id) + })?; icon = "icons/hash.svg"; text = format!( From ff245c61d2eb9bf2da51c8f9feb2cd091b697554 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 13 Oct 2023 17:10:46 -0700 Subject: [PATCH 081/274] Reduce duplication in notification queries --- crates/collab/src/db/queries/notifications.rs | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index b8b2a15421..50e961957c 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -71,6 +71,16 @@ impl Database { avoid_duplicates: bool, tx: &DatabaseTransaction, ) -> Result> { + if avoid_duplicates { + if self + .find_notification(recipient_id, ¬ification, tx) + .await? + .is_some() + { + return Ok(None); + } + } + let notification_proto = notification.to_proto(); let kind = *self .notification_kinds_by_name @@ -78,33 +88,6 @@ impl Database { .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification_proto.kind))?; let actor_id = notification_proto.actor_id.map(|id| UserId::from_proto(id)); - if avoid_duplicates { - let mut existing_notifications = notification::Entity::find() - .filter( - Condition::all() - .add(notification::Column::RecipientId.eq(recipient_id)) - .add(notification::Column::IsRead.eq(false)) - .add(notification::Column::Kind.eq(kind)) - .add(notification::Column::ActorId.eq(actor_id)), - ) - .stream(&*tx) - .await?; - - // Check if this notification already exists. Don't rely on the - // JSON serialization being identical, in case the notification enum - // is changed in backward-compatible ways over time. - while let Some(row) = existing_notifications.next().await { - let row = row?; - if let Some(proto) = self.model_to_proto(row) { - if let Some(existing) = Notification::from_proto(&proto) { - if existing == notification { - return Ok(None); - } - } - } - } - } - let model = notification::ActiveModel { recipient_id: ActiveValue::Set(recipient_id), kind: ActiveValue::Set(kind), @@ -119,7 +102,7 @@ impl Database { Ok(Some(proto::Notification { id: model.id.as_ref().to_proto(), - kind: notification_proto.kind.to_string(), + kind: notification_proto.kind, timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, is_read: false, content: notification_proto.content, @@ -133,28 +116,52 @@ impl Database { notification: Notification, tx: &DatabaseTransaction, ) -> Result> { - let notification = notification.to_proto(); + let id = self + .find_notification(recipient_id, ¬ification, tx) + .await?; + if let Some(id) = id { + notification::Entity::delete_by_id(id).exec(tx).await?; + } + Ok(id) + } + + pub async fn find_notification( + &self, + recipient_id: UserId, + notification: &Notification, + tx: &DatabaseTransaction, + ) -> Result> { + let proto = notification.to_proto(); let kind = *self .notification_kinds_by_name - .get(¬ification.kind) - .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification.kind))?; - let actor_id = notification.actor_id.map(|id| UserId::from_proto(id)); - let notification = notification::Entity::find() + .get(&proto.kind) + .ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?; + let mut rows = notification::Entity::find() .filter( Condition::all() .add(notification::Column::RecipientId.eq(recipient_id)) + .add(notification::Column::IsRead.eq(false)) .add(notification::Column::Kind.eq(kind)) - .add(notification::Column::ActorId.eq(actor_id)) - .add(notification::Column::Content.eq(notification.content)), + .add(notification::Column::ActorId.eq(proto.actor_id)), ) - .one(tx) + .stream(&*tx) .await?; - if let Some(notification) = ¬ification { - notification::Entity::delete_by_id(notification.id) - .exec(tx) - .await?; + + // Don't rely on the JSON serialization being identical, in case the + // notification type is changed in backward-compatible ways. + while let Some(row) = rows.next().await { + let row = row?; + let id = row.id; + if let Some(proto) = self.model_to_proto(row) { + if let Some(existing) = Notification::from_proto(&proto) { + if existing == *notification { + return Ok(Some(id)); + } + } + } } - Ok(notification.map(|notification| notification.id)) + + Ok(None) } fn model_to_proto(&self, row: notification::Model) -> Option { From 6f4008ebabd33793cd70a8f042c2c9510680a89b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 15 Oct 2023 17:27:36 +0200 Subject: [PATCH 082/274] copilot: Propagate action if suggest_next is not possible. (#3129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One of our users ran into an issue where typing "true quote" characters (option-[ for „ and option-] for ‚) was not possible; I've narrowed it down to a collision with Copilot's NextSuggestion and PreviousSuggestion action default keybinds. I explicitly did not want to alter the key bindings, so I've went with a more neutral fix - one that propagates the keystroke if there's no Copilot action to be taken (user is not using Copilot etc). Note however that typing true quotes while using a Copilot is still not possible, as for that we'd have to change a keybind. Fixes zed-industries/community#2072 Release Notes: - Fixed Copilot's "Suggest next" and "Suggest previous" actions colliding with true quotes key bindings (`option-[` and `option-]`). The keystrokes are now propagated if there's no Copilot action to be taken at cursor's position. --- crates/editor/src/editor.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bb6d693d82..7aca4ab98f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4158,7 +4158,10 @@ impl Editor { if self.has_active_copilot_suggestion(cx) { self.cycle_copilot_suggestions(Direction::Next, cx); } else { - self.refresh_copilot_suggestions(false, cx); + let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); + if is_copilot_disabled { + cx.propagate_action(); + } } } @@ -4170,7 +4173,10 @@ impl Editor { if self.has_active_copilot_suggestion(cx) { self.cycle_copilot_suggestions(Direction::Prev, cx); } else { - self.refresh_copilot_suggestions(false, cx); + let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none(); + if is_copilot_disabled { + cx.propagate_action(); + } } } From cc335db9e0e7ce677591d544140760ed8c080eec Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:17:44 +0200 Subject: [PATCH 083/274] editor/language: hoist out non-generic parts of edit functions. (#3130) This reduces LLVM IR size of editor (that's one of the heaviest crates to build) by almost 5%. LLVM IR size of `editor` before this PR: 3280386 LLVM IR size with `editor::edit` changed: 3227092 LLVM IR size with `editor::edit` and `language::edit` changed: 3146807 Release Notes: - N/A --- crates/editor/src/multi_buffer.rs | 140 +++++++++++++++------------- crates/language/src/buffer.rs | 149 ++++++++++++++++-------------- 2 files changed, 158 insertions(+), 131 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c5d17dfd2e..23a117405c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -498,77 +498,91 @@ impl MultiBuffer { } } - for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|edit| edit.range.start); - self.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = "".into(); - while let Some(BufferEdit { - mut range, - new_text, - mut is_insertion, - original_indent_column, - }) = edits.next() - { + drop(cursor); + drop(snapshot); + // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. + fn tail( + this: &mut MultiBuffer, + buffer_edits: HashMap>, + autoindent_mode: Option, + edited_excerpt_ids: Vec, + cx: &mut ModelContext, + ) { + for (buffer_id, mut edits) in buffer_edits { + edits.sort_unstable_by_key(|edit| edit.range.start); + this.buffers.borrow()[&buffer_id] + .buffer + .update(cx, |buffer, cx| { + let mut edits = edits.into_iter().peekable(); + let mut insertions = Vec::new(); + let mut original_indent_columns = Vec::new(); + let mut deletions = Vec::new(); + let empty_str: Arc = "".into(); while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - .. - }) = edits.peek() + mut range, + new_text, + mut is_insertion, + original_indent_column, + }) = edits.next() { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - edits.next(); - } else { - break; + while let Some(BufferEdit { + range: next_range, + is_insertion: next_is_insertion, + .. + }) = edits.peek() + { + if range.end >= next_range.start { + range.end = cmp::max(next_range.end, range.end); + is_insertion |= *next_is_insertion; + edits.next(); + } else { + break; + } + } + + if is_insertion { + original_indent_columns.push(original_indent_column); + insertions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + new_text.clone(), + )); + } else if !range.is_empty() { + deletions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + empty_str.clone(), + )); } } - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } - } + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns: Default::default(), + }) + } else { + None + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + None + }; - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - None - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - None - }; + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); + }) + } - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); - }) + cx.emit(Event::ExcerptsEdited { + ids: edited_excerpt_ids, + }); } - - cx.emit(Event::ExcerptsEdited { - ids: edited_excerpt_ids, - }); + tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx); } pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d8ebc1d445..78562ba8c4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1448,82 +1448,95 @@ impl Buffer { return None; } - self.start_transaction(); - self.pending_autoindent.take(); - let autoindent_request = autoindent_mode - .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); + // Non-generic part hoisted out to reduce LLVM IR size. + fn tail( + this: &mut Buffer, + edits: Vec<(Range, Arc)>, + autoindent_mode: Option, + cx: &mut ModelContext, + ) -> Option { + this.start_transaction(); + this.pending_autoindent.take(); + let autoindent_request = autoindent_mode + .and_then(|mode| this.language.as_ref().map(|_| (this.snapshot(), mode))); - let edit_operation = self.text.edit(edits.iter().cloned()); - let edit_id = edit_operation.timestamp(); + let edit_operation = this.text.edit(edits.iter().cloned()); + let edit_id = edit_operation.timestamp(); - if let Some((before_edit, mode)) = autoindent_request { - let mut delta = 0isize; - let entries = edits - .into_iter() - .enumerate() - .zip(&edit_operation.as_edit().unwrap().new_text) - .map(|((ix, (range, _)), new_text)| { - let new_text_length = new_text.len(); - let old_start = range.start.to_point(&before_edit); - let new_start = (delta + range.start as isize) as usize; - delta += new_text_length as isize - (range.end as isize - range.start as isize); + if let Some((before_edit, mode)) = autoindent_request { + let mut delta = 0isize; + let entries = edits + .into_iter() + .enumerate() + .zip(&edit_operation.as_edit().unwrap().new_text) + .map(|((ix, (range, _)), new_text)| { + let new_text_length = new_text.len(); + let old_start = range.start.to_point(&before_edit); + let new_start = (delta + range.start as isize) as usize; + delta += + new_text_length as isize - (range.end as isize - range.start as isize); - let mut range_of_insertion_to_indent = 0..new_text_length; - let mut first_line_is_new = false; - let mut original_indent_column = None; + let mut range_of_insertion_to_indent = 0..new_text_length; + let mut first_line_is_new = false; + let mut original_indent_column = None; - // When inserting an entire line at the beginning of an existing line, - // treat the insertion as new. - if new_text.contains('\n') - && old_start.column <= before_edit.indent_size_for_line(old_start.row).len - { - first_line_is_new = true; - } - - // When inserting text starting with a newline, avoid auto-indenting the - // previous line. - if new_text.starts_with('\n') { - range_of_insertion_to_indent.start += 1; - first_line_is_new = true; - } - - // Avoid auto-indenting after the insertion. - if let AutoindentMode::Block { - original_indent_columns, - } = &mode - { - original_indent_column = - Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| { - indent_size_for_text( - new_text[range_of_insertion_to_indent.clone()].chars(), - ) - .len - })); - if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { - range_of_insertion_to_indent.end -= 1; + // When inserting an entire line at the beginning of an existing line, + // treat the insertion as new. + if new_text.contains('\n') + && old_start.column + <= before_edit.indent_size_for_line(old_start.row).len + { + first_line_is_new = true; } - } - AutoindentRequestEntry { - first_line_is_new, - original_indent_column, - indent_size: before_edit.language_indent_size_at(range.start, cx), - range: self.anchor_before(new_start + range_of_insertion_to_indent.start) - ..self.anchor_after(new_start + range_of_insertion_to_indent.end), - } - }) - .collect(); + // When inserting text starting with a newline, avoid auto-indenting the + // previous line. + if new_text.starts_with('\n') { + range_of_insertion_to_indent.start += 1; + first_line_is_new = true; + } - self.autoindent_requests.push(Arc::new(AutoindentRequest { - before_edit, - entries, - is_block_mode: matches!(mode, AutoindentMode::Block { .. }), - })); + // Avoid auto-indenting after the insertion. + if let AutoindentMode::Block { + original_indent_columns, + } = &mode + { + original_indent_column = Some( + original_indent_columns.get(ix).copied().unwrap_or_else(|| { + indent_size_for_text( + new_text[range_of_insertion_to_indent.clone()].chars(), + ) + .len + }), + ); + if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { + range_of_insertion_to_indent.end -= 1; + } + } + + AutoindentRequestEntry { + first_line_is_new, + original_indent_column, + indent_size: before_edit.language_indent_size_at(range.start, cx), + range: this + .anchor_before(new_start + range_of_insertion_to_indent.start) + ..this.anchor_after(new_start + range_of_insertion_to_indent.end), + } + }) + .collect(); + + this.autoindent_requests.push(Arc::new(AutoindentRequest { + before_edit, + entries, + is_block_mode: matches!(mode, AutoindentMode::Block { .. }), + })); + } + + this.end_transaction(cx); + this.send_operation(Operation::Buffer(edit_operation), cx); + Some(edit_id) } - - self.end_transaction(cx); - self.send_operation(Operation::Buffer(edit_operation), cx); - Some(edit_id) + tail(self, edits, autoindent_mode, cx) } fn did_edit( From 5e1e0b475936872077126419b29418c0e51231ff Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 16 Oct 2023 09:55:45 -0400 Subject: [PATCH 084/274] 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 085/274] 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 40755961ea0d0f3e252e2248b027fdbf21a2f659 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 16 Oct 2023 11:54:32 -0400 Subject: [PATCH 086/274] added initial template outline --- crates/ai/src/ai.rs | 1 + crates/ai/src/templates.rs | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 crates/ai/src/templates.rs diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 5256a6a643..04e9e14536 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,2 +1,3 @@ pub mod completion; pub mod embedding; +pub mod templates; diff --git a/crates/ai/src/templates.rs b/crates/ai/src/templates.rs new file mode 100644 index 0000000000..d9771ce569 --- /dev/null +++ b/crates/ai/src/templates.rs @@ -0,0 +1,76 @@ +use std::fmt::Write; + +pub struct PromptCodeSnippet { + path: Option, + language_name: Option, + content: String, +} + +enum PromptFileType { + Text, + Code, +} + +#[derive(Default)] +struct PromptArguments { + pub language_name: Option, + pub project_name: Option, + pub snippets: Vec, +} + +impl PromptArguments { + pub fn get_file_type(&self) -> PromptFileType { + if self + .language_name + .as_ref() + .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) + .unwrap_or(true) + { + PromptFileType::Code + } else { + PromptFileType::Text + } + } +} + +trait PromptTemplate { + fn generate(args: PromptArguments) -> String; +} + +struct EngineerPreamble {} + +impl PromptTemplate for EngineerPreamble { + fn generate(args: PromptArguments) -> String { + let mut prompt = String::new(); + + match args.get_file_type() { + PromptFileType::Code => { + writeln!( + prompt, + "You are an expert {} engineer.", + args.language_name.unwrap_or("".to_string()) + ) + .unwrap(); + } + PromptFileType::Text => { + writeln!(prompt, "You are an expert engineer.").unwrap(); + } + } + + if let Some(project_name) = args.project_name { + writeln!( + prompt, + "You are currently working inside the '{project_name}' in Zed the code editor." + ) + .unwrap(); + } + + prompt + } +} + +struct RepositorySnippets {} + +impl PromptTemplate for RepositorySnippets { + fn generate(args: PromptArguments) -> String {} +} From 75fbf2ca78a35f80fd0b0b263f8c856d5f173b00 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 16 Oct 2023 12:45:01 -0400 Subject: [PATCH 087/274] Fix telemetry-related crash on start up --- crates/client/src/telemetry.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 70878bf2e4..fd93aaeec8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -4,7 +4,9 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; -use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; +use sysinfo::{ + CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt, +}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; @@ -166,8 +168,16 @@ impl Telemetry { let this = self.clone(); cx.spawn(|mut cx| async move { - let mut system = System::new_all(); - system.refresh_all(); + // Avoiding calling `System::new_all()`, as there have been crashes related to it + let refresh_kind = RefreshKind::new() + .with_memory() // For memory usage + .with_processes(ProcessRefreshKind::everything()) // For process usage + .with_cpu(CpuRefreshKind::everything()); // For core count + + let mut system = System::new_with_specifics(refresh_kind); + + // Avoiding calling `refresh_all()`, just update what we need + system.refresh_specifics(refresh_kind); loop { // Waiting some amount of time before the first query is important to get a reasonable value @@ -175,8 +185,7 @@ impl Telemetry { const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; - system.refresh_memory(); - system.refresh_processes(); + system.refresh_specifics(refresh_kind); let current_process = Pid::from_u32(std::process::id()); let Some(process) = system.processes().get(¤t_process) else { From 247728b723d752ed1b2e00dcbd79f8bf8bb356c2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 16 Oct 2023 15:53:29 -0400 Subject: [PATCH 088/274] 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 c66385f0f9099b89f07f2a6997da182218b4f69e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 16 Oct 2023 12:54:44 -0700 Subject: [PATCH 089/274] Add an empty state to the notification panel --- crates/collab_ui/src/notification_panel.rs | 21 ++++++++++++++++++--- crates/gpui/src/elements/list.rs | 4 ++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 9f69b7144c..7bf5000ec8 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -299,6 +299,19 @@ impl NotificationPanel { .into_any() } + fn render_empty_state( + &self, + theme: &Arc, + _cx: &mut ViewContext, + ) -> AnyElement { + Label::new( + "You have no notifications".to_string(), + theme.chat_panel.sign_in_prompt.default.clone(), + ) + .aligned() + .into_any() + } + fn on_notification_event( &mut self, _: ModelHandle, @@ -373,13 +386,15 @@ impl View for NotificationPanel { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx); - let element = if self.client.user_id().is_some() { + let element = if self.client.user_id().is_none() { + self.render_sign_in_prompt(&theme, cx) + } else if self.notification_list.item_count() == 0 { + self.render_empty_state(&theme, cx) + } else { List::new(self.notification_list.clone()) .contained() .with_style(theme.chat_panel.list) .into_any() - } else { - self.render_sign_in_prompt(&theme, cx) }; element .contained() diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index a23b6fc5e3..eaa09a0392 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -378,6 +378,10 @@ impl ListState { .extend((0..element_count).map(|_| ListItem::Unrendered), &()); } + pub fn item_count(&self) -> usize { + self.0.borrow().items.summary().count + } + pub fn splice(&self, old_range: Range, count: usize) { let state = &mut *self.0.borrow_mut(); From 4e7b35c917745e8946bed4052351d93b45f0d3f8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 16 Oct 2023 13:27:05 -0600 Subject: [PATCH 090/274] 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 091/274] 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 500af6d7754adf1a60f245200271e4dd40d7fb8f Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 16 Oct 2023 18:47:10 -0400 Subject: [PATCH 092/274] progress on prompt chains --- Cargo.lock | 1 + crates/ai/Cargo.toml | 1 + crates/ai/src/prompts.rs | 149 ++++++++++++++++++ crates/ai/src/templates.rs | 76 --------- crates/ai/src/templates/base.rs | 112 +++++++++++++ crates/ai/src/templates/mod.rs | 3 + crates/ai/src/templates/preamble.rs | 34 ++++ crates/ai/src/templates/repository_context.rs | 49 ++++++ 8 files changed, 349 insertions(+), 76 deletions(-) create mode 100644 crates/ai/src/prompts.rs delete mode 100644 crates/ai/src/templates.rs create mode 100644 crates/ai/src/templates/base.rs create mode 100644 crates/ai/src/templates/mod.rs create mode 100644 crates/ai/src/templates/preamble.rs create mode 100644 crates/ai/src/templates/repository_context.rs diff --git a/Cargo.lock b/Cargo.lock index cd9dee0bda..9938c5d2fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,7 @@ dependencies = [ "futures 0.3.28", "gpui", "isahc", + "language", "lazy_static", "log", "matrixmultiply", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 542d7f422f..b24c4e5ece 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -11,6 +11,7 @@ doctest = false [dependencies] gpui = { path = "../gpui" } util = { path = "../util" } +language = { path = "../language" } async-trait.workspace = true anyhow.workspace = true futures.workspace = true diff --git a/crates/ai/src/prompts.rs b/crates/ai/src/prompts.rs new file mode 100644 index 0000000000..6d2c0629fa --- /dev/null +++ b/crates/ai/src/prompts.rs @@ -0,0 +1,149 @@ +use gpui::{AsyncAppContext, ModelHandle}; +use language::{Anchor, Buffer}; +use std::{fmt::Write, ops::Range, path::PathBuf}; + +pub struct PromptCodeSnippet { + path: Option, + language_name: Option, + content: String, +} + +impl PromptCodeSnippet { + pub fn new(buffer: ModelHandle, range: Range, cx: &AsyncAppContext) -> Self { + let (content, language_name, file_path) = buffer.read_with(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let content = snapshot.text_for_range(range.clone()).collect::(); + + let language_name = buffer + .language() + .and_then(|language| Some(language.name().to_string())); + + let file_path = buffer + .file() + .and_then(|file| Some(file.path().to_path_buf())); + + (content, language_name, file_path) + }); + + PromptCodeSnippet { + path: file_path, + language_name, + content, + } + } +} + +impl ToString for PromptCodeSnippet { + fn to_string(&self) -> String { + let path = self + .path + .as_ref() + .and_then(|path| Some(path.to_string_lossy().to_string())) + .unwrap_or("".to_string()); + let language_name = self.language_name.clone().unwrap_or("".to_string()); + let content = self.content.clone(); + + format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") + } +} + +enum PromptFileType { + Text, + Code, +} + +#[derive(Default)] +struct PromptArguments { + pub language_name: Option, + pub project_name: Option, + pub snippets: Vec, + pub model_name: String, +} + +impl PromptArguments { + pub fn get_file_type(&self) -> PromptFileType { + if self + .language_name + .as_ref() + .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) + .unwrap_or(true) + { + PromptFileType::Code + } else { + PromptFileType::Text + } + } +} + +trait PromptTemplate { + fn generate(args: PromptArguments, max_token_length: Option) -> String; +} + +struct EngineerPreamble {} + +impl PromptTemplate for EngineerPreamble { + fn generate(args: PromptArguments, max_token_length: Option) -> String { + let mut prompt = String::new(); + + match args.get_file_type() { + PromptFileType::Code => { + writeln!( + prompt, + "You are an expert {} engineer.", + args.language_name.unwrap_or("".to_string()) + ) + .unwrap(); + } + PromptFileType::Text => { + writeln!(prompt, "You are an expert engineer.").unwrap(); + } + } + + if let Some(project_name) = args.project_name { + writeln!( + prompt, + "You are currently working inside the '{project_name}' in Zed the code editor." + ) + .unwrap(); + } + + prompt + } +} + +struct RepositorySnippets {} + +impl PromptTemplate for RepositorySnippets { + fn generate(args: PromptArguments, max_token_length: Option) -> String { + const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; + let mut template = "You are working inside a large repository, here are a few code snippets that may be useful"; + let mut prompt = String::new(); + + if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(args.model_name.as_str()) { + let default_token_count = + tiktoken_rs::model::get_context_size(args.model_name.as_str()); + let mut remaining_token_count = max_token_length.unwrap_or(default_token_count); + + for snippet in args.snippets { + let mut snippet_prompt = template.to_string(); + let content = snippet.to_string(); + writeln!(snippet_prompt, "{content}").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 { + writeln!(prompt, "{snippet_prompt}").unwrap(); + remaining_token_count -= token_count; + template = ""; + } + } else { + break; + } + } + } + + prompt + } +} diff --git a/crates/ai/src/templates.rs b/crates/ai/src/templates.rs deleted file mode 100644 index d9771ce569..0000000000 --- a/crates/ai/src/templates.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::fmt::Write; - -pub struct PromptCodeSnippet { - path: Option, - language_name: Option, - content: String, -} - -enum PromptFileType { - Text, - Code, -} - -#[derive(Default)] -struct PromptArguments { - pub language_name: Option, - pub project_name: Option, - pub snippets: Vec, -} - -impl PromptArguments { - pub fn get_file_type(&self) -> PromptFileType { - if self - .language_name - .as_ref() - .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) - .unwrap_or(true) - { - PromptFileType::Code - } else { - PromptFileType::Text - } - } -} - -trait PromptTemplate { - fn generate(args: PromptArguments) -> String; -} - -struct EngineerPreamble {} - -impl PromptTemplate for EngineerPreamble { - fn generate(args: PromptArguments) -> String { - let mut prompt = String::new(); - - match args.get_file_type() { - PromptFileType::Code => { - writeln!( - prompt, - "You are an expert {} engineer.", - args.language_name.unwrap_or("".to_string()) - ) - .unwrap(); - } - PromptFileType::Text => { - writeln!(prompt, "You are an expert engineer.").unwrap(); - } - } - - if let Some(project_name) = args.project_name { - writeln!( - prompt, - "You are currently working inside the '{project_name}' in Zed the code editor." - ) - .unwrap(); - } - - prompt - } -} - -struct RepositorySnippets {} - -impl PromptTemplate for RepositorySnippets { - fn generate(args: PromptArguments) -> String {} -} diff --git a/crates/ai/src/templates/base.rs b/crates/ai/src/templates/base.rs new file mode 100644 index 0000000000..3d8479e512 --- /dev/null +++ b/crates/ai/src/templates/base.rs @@ -0,0 +1,112 @@ +use std::cmp::Reverse; + +use crate::templates::repository_context::PromptCodeSnippet; + +pub(crate) enum PromptFileType { + Text, + Code, +} + +#[derive(Default)] +pub struct PromptArguments { + pub model_name: String, + pub language_name: Option, + pub project_name: Option, + pub snippets: Vec, + pub reserved_tokens: usize, +} + +impl PromptArguments { + pub(crate) fn get_file_type(&self) -> PromptFileType { + if self + .language_name + .as_ref() + .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) + .unwrap_or(true) + { + PromptFileType::Code + } else { + PromptFileType::Text + } + } +} + +pub trait PromptTemplate { + fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String; +} + +#[repr(i8)] +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub enum PromptPriority { + Low, + Medium, + High, +} + +pub struct PromptChain { + args: PromptArguments, + templates: Vec<(PromptPriority, Box)>, +} + +impl PromptChain { + pub fn new( + args: PromptArguments, + templates: Vec<(PromptPriority, Box)>, + ) -> Self { + // templates.sort_by(|a, b| a.0.cmp(&b.0)); + + PromptChain { args, templates } + } + + pub fn generate(&self, truncate: bool) -> anyhow::Result { + // Argsort based on Prompt Priority + let mut sorted_indices = (0..self.templates.len()).collect::>(); + sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); + + println!("{:?}", sorted_indices); + + let mut prompts = Vec::new(); + for (_, template) in &self.templates { + prompts.push(template.generate(&self.args, None)); + } + + anyhow::Ok(prompts.join("\n")) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + #[test] + pub fn test_prompt_chain() { + struct TestPromptTemplate {} + impl PromptTemplate for TestPromptTemplate { + fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String { + "This is a test prompt template".to_string() + } + } + + struct TestLowPriorityTemplate {} + impl PromptTemplate for TestLowPriorityTemplate { + fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String { + "This is a low priority test prompt template".to_string() + } + } + + let args = PromptArguments { + model_name: "gpt-4".to_string(), + ..Default::default() + }; + + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::High, Box::new(TestPromptTemplate {})), + (PromptPriority::Medium, Box::new(TestLowPriorityTemplate {})), + ]; + let chain = PromptChain::new(args, templates); + + let prompt = chain.generate(false); + println!("{:?}", prompt); + panic!(); + } +} diff --git a/crates/ai/src/templates/mod.rs b/crates/ai/src/templates/mod.rs new file mode 100644 index 0000000000..62cb600eca --- /dev/null +++ b/crates/ai/src/templates/mod.rs @@ -0,0 +1,3 @@ +pub mod base; +pub mod preamble; +pub mod repository_context; diff --git a/crates/ai/src/templates/preamble.rs b/crates/ai/src/templates/preamble.rs new file mode 100644 index 0000000000..b1d33f885e --- /dev/null +++ b/crates/ai/src/templates/preamble.rs @@ -0,0 +1,34 @@ +use crate::templates::base::{PromptArguments, PromptFileType, PromptTemplate}; +use std::fmt::Write; + +struct EngineerPreamble {} + +impl PromptTemplate for EngineerPreamble { + fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String { + let mut prompt = String::new(); + + match args.get_file_type() { + PromptFileType::Code => { + writeln!( + prompt, + "You are an expert {} engineer.", + args.language_name.clone().unwrap_or("".to_string()) + ) + .unwrap(); + } + PromptFileType::Text => { + writeln!(prompt, "You are an expert engineer.").unwrap(); + } + } + + if let Some(project_name) = args.project_name.clone() { + writeln!( + prompt, + "You are currently working inside the '{project_name}' in Zed the code editor." + ) + .unwrap(); + } + + prompt + } +} diff --git a/crates/ai/src/templates/repository_context.rs b/crates/ai/src/templates/repository_context.rs new file mode 100644 index 0000000000..f9c2253c65 --- /dev/null +++ b/crates/ai/src/templates/repository_context.rs @@ -0,0 +1,49 @@ +use std::{ops::Range, path::PathBuf}; + +use gpui::{AsyncAppContext, ModelHandle}; +use language::{Anchor, Buffer}; + +pub struct PromptCodeSnippet { + path: Option, + language_name: Option, + content: String, +} + +impl PromptCodeSnippet { + pub fn new(buffer: ModelHandle, range: Range, cx: &AsyncAppContext) -> Self { + let (content, language_name, file_path) = buffer.read_with(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let content = snapshot.text_for_range(range.clone()).collect::(); + + let language_name = buffer + .language() + .and_then(|language| Some(language.name().to_string())); + + let file_path = buffer + .file() + .and_then(|file| Some(file.path().to_path_buf())); + + (content, language_name, file_path) + }); + + PromptCodeSnippet { + path: file_path, + language_name, + content, + } + } +} + +impl ToString for PromptCodeSnippet { + fn to_string(&self) -> String { + let path = self + .path + .as_ref() + .and_then(|path| Some(path.to_string_lossy().to_string())) + .unwrap_or("".to_string()); + let language_name = self.language_name.clone().unwrap_or("".to_string()); + let content = self.content.clone(); + + format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") + } +} From 6ffbc3a0f52bd94751393ad1f0217b9692cfa230 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 16 Oct 2023 20:03:44 -0600 Subject: [PATCH 093/274] 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 094/274] 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 zcmdUV36v9Mwm;qMy9n+ApmQE$9ByP|uj)3dn z3L=UNBkt?CF>X(B14Us35oZK-6dV=xF$^yMTS>1VGw*-SJM-S_bLw>JtFON0e)s;Bv8iJeA|;wE2DJ4rANdbH~h{LAY>V*i9-5 zLP{(t+jGgm)XB=dI%D~) z+)-4zi}?aqEY%z^SO>h&885}#V4CLWgO&rW@l>odcg*P78Ll=LrlSZao8@LJUE>}t=Qd|-GCQ{YDQP^UU9XK4#=yZXI z!x>=)j;8GECQptRxyGObTk2m<9B$^S5w)g6g>u|e0AVer8XNNHXm41u zp0lHx4_I<0f9MvG)6Y&#CgfigfJ;; z@$=rGF6~H@4jRogo?%HE4+Ln*nv6N=G%2}!q(rBwI!(~JV52Q8w^e)Ew81ovocaR8Q3e0XH*`iFAxl7s;;0wRy_oUW4tqfp)~D6JVHDh5ThDQh@~)_ z2ge%wV1v$N+#*z>={z6I1~MRza>N>_$E~(PBt(nyeYmFO?c|`b3$%xpNTx?7Xu;|C z`Bed-K>LJ(hxQ83s07dLXdcd;A)JYl}ik2rn|UUK9mgbK|ET_r&C#X%^V1nY_*KZ)n1A@eHA8eBBSYWDds>qlP|7R zsnwV-o5)qOMF%PgU4cN_W{z+gl+)Vc4oQk&Dvu#mBWt0wI-5z07KF4~<7yR4mLhDe zLe;Hi0>`W+YdT%WRP|5}F;js^&|QmKi9&?3Ib+pC$ma{XC{-yC60)3RucJJTp{OOz z^OO&44JYa-sG2ii^+0>VNTO*J8jP~;#wm#j(-@1I&9RVyb+|RwS~%|^c%?&cb$IJF zZ8h$vNz{VNw#WF*m_%Y2!GM0F62|jIj3><)6O+(zIKx^!NQ}r=9BNZKra{Z$roEKI zYE!AFO`>b6Oe7R3L8O>$*n8jR+TQht(u!l`$eE+kZ?W_wyuUSqA0`)S@Ap z4})z`7H0+83k_S$(Xf^<5B4HB@I%%jMb$8#2G-Dg?-1;YS#8O%#shOU=Yx4lk)RHV z)wGh64^wt2jA4x5it=R8o=qZgE2?+YOq#eRc97f$;i3>8fQgPe8f@r=*Wb_!f9(UNAK@dt|*hGQ`jtxM6cCQx(4(vf725EFjCMDUGsbfb(_DH}G*a}6EC zQ%wvAs~HRGB}tNz+@L!(+K1}kl4wG{Fu+1M+kt%SVcSMX*YebgR5JXDw^c69KDZ#0FHysaH~XA1^@%pm1N z#K1+-6m7GFxVYWUGFYKftprs$N5V{29a^Jah0zgByy`dlbdo(EY_F%Y1|R8mur?ebjHc|2Swn#C_Hw+MW!h~A;kr^g!uXJgn#;BF{vr>W0r0qB!k7g1|F=xk0YO*Vjs6_^$5F4XsG-yek zG;tK?jVVoWPfVHhSzXnzE?wm5Ixv-nn+E#OOG0{>1VMT_NSU&6@ebfsiIgyij5IvE z(>pwlYofydyz} zDO?RGMxgN^San_Y)bJh#nnB}$KYqV69|<~(Fg|D8RQx_+%sr#)Sw9UW)+AfC{pgwRuQgG8**L zxT+vTaKOB5K+OgN8XC*cW;0fcY9fW08oY4MiA60{QbxJ3I#8D_g2oVGXw5P6FiVRVBk&s^)-aP9I%!21#AS z7te;%sDZ!&^7#l+i%9}~8pHyDF-a89@E%;L$$ETxmNf}oAzM4G$CHM#FXF=kRWZTl zv(aoxWi>|)SXkmYKOAGA49Jf}dy!g$V=5F_P87%LX$%K94*G@kqjSSM_OaCxJMg@D zAiM!m!+=ke;2O;#)S?cEy%^EvBLwX-1W z2E3Pvs#QrgsH+nw*#(QHDfQvs1+SBgdR2u8ctO}3=tH1(h#tcEo3kZ>3p(@Rpip)M z&1qThV>ML{&RP|-0;A{Qhz9LP+Xc8lx$0o!85$U&)s_bpMHvdmagA9?rM*>uIY6U% z@c7_CXsrp%ngAHDTIy(|ml2bhhW-eY*;a{a7@k2$-r>cpbe+u7ICvb07H~<5_cC`z z9jG<;(ZiTi7#0tC@_+{gU<6gNj(VD7=Yfg$jNN3{ajsu_aq9uxq)&Ct)N<57+7#{<6 zm3Fxq3d4ARk!Ki`;+-iJ^@jCcqlvNFGlmGAiseb4)rl}d!fMVVx~c|af@;Pf6nKAE zT^^4ha;eS3u~ zSJa}I>8hjd21Ds#wwMnPTgF>d+k79`s!{@{08u8EZm^r4eFH<0oOzVM1T+mUs6B^O zu?Pp|i&&hwl0oSa2_Ca}4e5lK345xH(xl6E(Rnsftcw((vei`?V+1K%iixfi9YE7q zDU#1KwG!i0kxFOUPq&w-Lb~8{mbGdPYc|9)R%6m<{$8CP#0n3Mmir?^JJ@^g*tTevF4K%C;mvt1q+|>D>nI04r|PT& zt4mYzRj1Q77TVyy%=Ezcds&{98n7O^e?4ZW4p_Ai^qUy zk*HngRapp2N)zQva5_dtFlueZG;6S?6NHttX}WUhe9qLRiQ%+`tW$2Xpj2k9beFbJ zvoNN3uH-2=Dcqr9+rzlsV~qRt@puiN;T}*?olYlGm_EMj( z!?2uz*L8`}0Ax%C`8u5bUu-MQOc&?_LDs;wYrr#mGF>sr*~osuGl=ejY0O1?^OC)p zc_Rqi4DqMO`_FhGWp)-KYF#0kjs5@mV{=9#rLa}b9M;naa0!T#o4>&)#0Z8*NuNW7 zSL>WDB$@RzuF**rvXbjkt1v(BQkA;`dcout!9)uJlZud5qDYj25sM*J7Bk*ZUG1QJ zsLIvlateI8j@zkVK9j5I^e!Cr8{u5dkP(y`ey7KVaT-Mrv0pC3>0MMlU?cgIM^v|` ztN9F9XFxqO5+kp-?4gi`6f)%!p4SPZ=cHoD8npWDxpa()8*BkGnX>uKF0&BAD&{CDhDy;|4%b$J^M>RM7$RrDeOUYd zVta>F{Xm?8%x)03cu})}N^(r^hh(m`(8y-@XMAmF9WwX5Sv&~=A4*DAbYN~EW2PA+ z@MYi&KC}v)T??_Bi+pj$nd)2Lj4{h)rxl1gE-UD5)liyivh%S*WIvzs_2^HkDP$INJoL^E1H$3g`u z970`g-mEIB-DQ&P%IGlB5SDU@lFDwfn1X6AqQxnT%b&9P!i2`s?oK&szEZ*!swd1s zQp;)>ja5qdxri-N!e}qmm)#lN|2GDHHfmmOH-jg+Sf;OZ(_7~0$pDeCAPDucP}=G* z@AMSSU<`Sj=0H$?>#4-WdrL!|tSIJVzaVJG!U{U;T)Y$Nc96C@xNbLS8mP6!lCZK4HWuSVv4bz>%Y^~U?5kwK zHhU_6111`@`o83uER%mOm=eck|7&|0C>}XKH?UqwRtu@(VXK5%m0Tr6jEAcd{)<8< z6g=}4h*R=8sDR2h&E((U>Stdc5fua|38p}Xy$3V9R4n(`3w^NT{sL)VpFZ&(D%=4( z@0V;tXXUXDw|XiUFUxD>#4{9BF4j446uGYIOJzA<-jS;GCo)sH6327Jxq6)enXT-- z>(PI|pCbBJ+p~E+qS9#+dP9ea)#*F5MxCyMjYicSI*m@F*0Cnekcb{UZ=hKQ?;EOj z8r6Uqdrx<^vQev^yQ+J_y-4>kKhiyP&Z?oUEv>Cn&boNPn7_T(ddFvbKD=q`8~2~K z;@i|RWPD%oy=5#^Qv-pxpC~|sm;dz zDlQzj0xg%4`C_V6Qyq;=k^2puJigh_lh2nFIEqX=X1p3vsa1NFNn_OMBgc%_$hS!I z58`AMfnbd{$DP%DGng+5aS7~K(N{WFcsQt3!-S&ZaP!Nb8OZA#*J{!A?PT1*o%Hl* z>=o&5X&Wewmf=H&cDGD~4-{I)w05_&47vThZPO3@h+4ueJ)8@t} zC2swC-nzBg=kGgXMsCZrWiu{6qx9m0BiQ?9$7~ndcCIgP+4$0mQPvC6PoMtC>xrM% zFFyL+M~BVserAF2$uDDyyYK(`th{yI$|aNN8!meEf@_tnCpv$m*8M5}glk>()onW- zef#yTiAiTgz8Fo4SGwPKev%x1@wV~`d&`5{c5cj;=pEj7HXm>7KJ%U1cAj_o1&=KJ z^7NTw56r)C(UAwfiMmF7?s#|l*6hl!#BJ3X<8Lc$H3zRfpTF!yQ5v%*8h!G?!lGx( zCtdkK?LR-e>;0cDJykvCtdp-lS8>ORca;l{UH;MRv7bNj)1C#viN~xwyZwrp&+cm- z+A^eN^}?@_g?o@AL9kONx3v5+Y!osA{6E9LK5Qm(3Ubn_8LOr*n%eVftSAh4Q|{dfO~_Qa|Ab*j51Vq_lAErwoy%5( z-{1Wk-I0q;qjo-AMkdIQC(1h+239fxIby_^#y2am_=wh)VXa^igDnJfqGd?r72Ta% zKO3{sbLWumHtFr6^4k4v4^27ct^3Ac^RIvD`SR4=jyI0_^y17pN6b5LE?FA?P5q+Z zUbkUKYzcPdrei-w9}BGbACH!S;Z2vyyvEsbiCxMO6J$u>H z#z^(SNmkFZ<8@1x?RcwnYw`zb{<8b`u6gXJMc2Ob7J@H&@6$pn^Z3FS*4*^|mtQ`- zbo4o28-Mrc+igR4^EkJj1YmgK%4qn%4@G1hQtQ+zEuv|lhz?OBrk>k> z3zGdS$k=t=??+F+?!##o_wM0e&8uD3v0Hy<(Ba>+wEp$jxqo+$ ze(w@=&CUDYeIv2r=e~cmLG$J-6X^|8Qt!TgT=9qV zCTzas{N;O38TyR>8v4BP-^O((9c4c%JoT|<`~Gvq%8l-4UJSYUYeqe;*!SlBXDIT8 zC(mv{mOh1VoOs)u@7k%;j$i-th9{n$uyx*vTi%@W#F-C#KXJm4>&HGa0;dq+X`C&_6(z2ig%@&hMJ%6-r<%SkpHP%V?uyk>6c%6`3bko zu^9ILe)&09pYi@9*B<%7y6?s1f7=wF_-4(e{ou~I`>VfX){oqCk?GPUUd4OHS04G} zt*$YdU)p~R-#dKtg~yx|m^7{Q-cw6XI?J+pbN=K{cE9^n;p2(qUuM4XZMOQ==T97S z%$n^Nx9Q$EW%JgFTQcw8dGbk<_T6#+HR8hguby!3Xyx^f{++OXGvB!Ks2}E>$}1LY z()-4DZ``qawQ|2VeQx}Pm`2)n=EySwySGi8F=5*?;&HAgw(2i=@e2RedvAGKjUD)4 zbJxm?@9;cz!t=kwPCjzhYZIqFNw1g}TW?=*jQhUDo44)y@tx5l4G)*gGm!4#O8~;e zCVU-n6r9SjubyjITf4sV*37!|2Z~JQME^s4M5ak<#E7WXDwPp20!>ou5u*l}65JXh zhzGt;JOlK7*wAsO&}>ZM@+v~VO1u@`6fYC;{{{}U_tf)3qX1Aeznx&%aqWm*-P0eM zxoPORp7b+UzC7#u6TA0!dzVZexik9VOYd&i44Kh+`fK=|o3M|jUNmaN?!alw)~&|w zKOr|~!^9gupQ$_j?#Wkt`og}J)27W$uKI$q;g9|0II z@~##4bgaw0KI#wD+~b~GaKh8u7md+wdFg{upFR1_;G(8aQ}O?4684+HF^DU7KEJe0AfZm-l1# z@7BNA18A*Aq`Os%jOzoiR%9^$`Bkb>=!FyDKXD|AboAaMt&3*#k{B>(aR7~ha%7R$ zK~P#mRDIoB7M=1>=~DA0@Q&Rr4fZ1g;uN?!0D3h$-N~sXtH$4B zJhHuhy!+EO`&VmTctO$LzU$4<**{HK^YHv#w;V`5F!Sqocg}uz=_!k6-8lEJUn|Cb zGWq*8x=+U3wtVTlz0a)~G5SAF`_tAp|KPb~`U@X%S%v1t6NX>j(H*$1@SA(h!Oj~W zd*#6m3vSc39pClG3$)X>T@t;f?d017V@*fTd*S9$$1VKgy{9i+T=?kjpH$0)d#tv6kd)%~Qn(wOD|JLyze_gsX`+~u@v-;-RD=uB+-TJXTe%Wb+`uEp8ycG$HSj|Xw@?_@7^R;&a=&$JXO`*dM((~Qus6+ zS$O6D5ZtyN>IV8hQeU-jJ92Dapgel0YHZ)l$QIpnko-TU%R)EC5f|f8TNrGP3#E{m8-Xrww#?Fa@B+kk5x}Zul8} z^X6wSoWJVoyFa_|%>BQ)(OBFu>-j5QU-Ry|z`{OVBK?0sv0{_eT#iJ!ds!|F@#B{oVeFaLP+XzzumZ`kv| z&X=y78!-Q)>*w~beoOB>YVQXR{WLA|$Mv~sTh>0aeG+@oPd`t+?B%(aZT!}>X7?rA zF4+HQXv15Ii@V&*zx(u*U01EYb?)Z*H}9L|U$P a*7NeWFDQ&(klM8L$X$DlFNp3R9ByoeQ zVg;;Akyb%OQA^zw(c(hg_XS0)fYR2jRj?@4QcA7wGs#^+TK{<8{(kTMeCBiWJoD^x z&i8!JISb&zSQbto23dLkDJHJgg7i!Z8Sw0s_X2tn!Ou*J*X*++xn0eDNFB*vW za7{jwEXo=mgSZJ))7GKY!ufng&}y*&rt!H6e?SAq&}wl{o2D%x%lUJ)T18Rja%!u& zd`?mu7PY=YE-w`1sv8W}0Xoc$$+0#tO=I*y%K^2qBrh)*GkRWHs4f805txUQgn3%E z2x^OR0W5X_E5zn$dw%z~%_MnF23sRyLRL=3lS08)5aP*7UvI8Z;06vuQYKT7b$eK9 z;Aq;uX6mG9L8uRkadQ7MVxX`<59tjZI;cZu2KxT=L*s> z3+UU3*B?Iq)I>21K8`?gSx!_p+;awTE>je=y=R08Wg(*;Fbd8XY#JPaQ0!~lyOq)N zz`D2vBth6@3`G%VRfH7+fklFFCIqkD8`o!9Qo*Q*)0Jb1M49)%6z)bSSP=raOrk|Q ztYdIkQPQ+MsAof!GH(x72|R-nxF^GB={zfCQjsv7kLryg6%In2-CE_sw2Aab6ek?P zf)wS(5FNrhkRt0xFgEOjxDcC<=xyM?wM5`HUCa(4x9Paqfyr7PZ02CvWG8JR2s9YB^FhiY|Q6*m;sB6`blh6FBc-0*o%0 z$&-Oho+fEXu`Bg=E)1bLHbklWPRGI?2%JrZuPVnq2T{b6Bn;V6f0x})L=6~)BQV9l zu#rG81y;a&7n}p%ox(hE$;M?xA{+6=1YXQR5!&Pp#**$zI^O94`i}-gC{*b(paE^n zmC}b~9T7!|G)HGpL7Owuk**+TuXIs)%8Y8=SeCbB&Hik`U`%B>!EZAQj-oe17mJ)R zjlc#dNasUtH(4}h@^UWb*T<`wY&>O&k=9zqCfA@^*;+*ma>1ofcywsMU9E(bn!z1r zttFX)ZDg0g=fa7g7(%0kTq-H~soz;w{#w;oi7_M& z+fh~bD8_~;ID%jlkZ)K)M6rN~xDBCr1&&10oP&mVELV2xtts9Bmm&>)DMj>Fxu;Gc ztGYBBX7elt>%rO(hbn(GBqHE9*eMFrb@>46O?M^INxN0=Nh1`lh;gGL^5lSZ5GYC5 z^%!BVF+eA{kPXbkh^pQL{mt`LBsEaCfvzV7Kaz-M(;{!TRb!5{7zul7v5*ePGLnes z0e(2>L{#!*2)k7x9CQx!0r(Lt5q6ux+bb$YBvt+A4Qe|&!AlgFD_D(&NihO+gRpzb zpgv#M#ViTd5)pyEWZdX6Yf++lFrERdq0#Rc?BX5HM8rUYIUDnVd1N+Zg7~UY@rV(^ zr9=>fVjQrDhg?zuiaB7jyJ|JW4E#~&J_r{xkpYVp6*1mAXGTMBB(s>r}cV+pD>umq;ykhx&Wb#-P0R}k_k zH7H~1%*(i>OL*+&vcKj`8x)ttlE{&E-e)#u@uU_j`f#G1;O%nM>hX&Cpj)3P6&w&! zPT)SB-|4AJon|b^&`Ju02+3E`IuMMrrH};f#{AJFjEl~8)Fyy7-m%sPZ*4Y3Mi378aDF4SI2Nu2@Q=jN)EAENVRw3N2l z{g$dBqwVy_)?xulOJ>4XtWk!Nhj)Yly1PoTibSqn4$nJ84r z$0P=Y6H*tj@ah;vz#62e!6}n41BSH*LKqytV3U*AB~np6oe8K26N+V67+5n5W`R|t zc*&~g!XCgjOf@Q0DXOdle6I`-P*MxD2ZJjZF9jRcIZcEAOc)CQW)OfYWDPfZ0!)?E zqV6c%4h&m6LBb0(g3{Eg?$R_uk1M_d#fkIG9m~lAEfI$^mhRy?&t8yVHU_*de zWM~s_XGkPXJDGG43u15+FC5s2Gtd8N!BCqXst5>p?#WFp(;-50X{l zLX`|)aHJjTL(@9r@`vb5w87jmd_Gj9T(^jQ3{~a^?p;(ugTA%j1T1BwBmmiUXYv`ahXRJOgT`YA zuwb`jtMS0{0!s)1wkET7s~ibsO1z>Vt`rbN)N}m4SvnI5K}WCWXxYnQ2wPyonG|QY z<)h%N>sHf1%B;azj)jpL3B(T@%rZ-^E)*z#5wJcdrds?e2ee1TQCJk^O1w%y#h^PA zs9PRj@l#$O1*}I6$&z$Yu{YQ$h9S;g#_o^mtq@@Q2!TQ(T_Y>b6r64_E_S z?$^b&Ok7SmlVLuaPuOH1iCgH9h*?86k<7;#pBsl9bj737=Y?=aZ>fa6P$3&Gie78d z6!c^SiKEO6m8;|+z&!z=t7~@jFpN?sCgg%*E$Qq^C-YgMTrd<0)`HvVDk_-jjnGh_ z!7v!$N7OOa+vsnpvjr9U!9EpKWk`*E18U5sbV-}k?AhDmb1gYY_34$R7P$}0ox|&`@COL zF_hZfR+2-FN3UIBYj_j@Z(UY^=j-p46-Kg_V<_O_e$`6ywf~xj8{k#%wpxE`M78eW z)(B9C1Lbcpx=;$K4p!>aecn3I4Pe9m-|K}ft>DTaz37z{ z45oOD0=j`nrF2lTTC>$ST}yG^bV3f*f|Q=c3(kx^$+j2MV!Y(mg54lW2#2$sLmQa@$vDQ$;iow3R%=UO=NC4m=V9m#PWbTq; z=(0sfW0K1gU96!1sGnKORjd92iAM%_LaIlWQ@p^V8o=IFkF4uQdSr=uG$gEI6rGBM zfGlBr-8*zBz|#XV>2|?gITT2Azo(JnC8gi4)jhlh9syflj@dm4wnmmB24|5(p!!&t zNQDrDHq)IsV=}>{{ixGki<*7*B;DzO$u2Gc|DY}d40AQ2L9rAIM^K!iP&FcSw2rd? z5m=Z|Y*aO$DsttFlquv*THt_DSSVC2F)}Sya)HCM^SC(^z!+sSYNKb*Ac%MzSS#~nd$Hq6Ujx80SGwpYX~ z?@w!kP9J8e76@SLNV}@%g??K{vF%1LSFr0MsiSP&|7Kl5j19huQ6PE+bUP*LyuqAW zXCN^K@J%(!#VDjLrvy@X7m13!IF$(MVmOnmbH84WoR3Pp*vB#bNPr8)N?gbUY<*uO z>POgPgjKyoqzXJs(r5)-0_Z}|Zv}V25CJ22)k+n4Lo#SVgcK~d^GL#NGRra*BrrMJ zl@apsK&QRHxmbpfXhbn9ok58%84Cp`p0QX#Kt}Ndx>6}N;w~!5q^-;8w3Gx8QL*_{ z4&(nZ5fw+?QA#CUHa|@e>8Piivv5vF5rV7vJc+Ao9vD#pX9&2(QTm(t(fX?goH77( z72r#z;faHYirot$ByR8!M@LhH8e$qS7Ybxv#_Te~*)uf|#TXck0MQUo=b+1k-I*ak zR8frtvSHv8f#00Ur$Hozm8@05&gww)q|WIVdBm;;!1Ph=HDJ<1V%5b)Sw&7B&|4KOi;gm|mF7DSD1pDAj@?MPS6QmeIF zemMF7Tl)M-5PcwV<%e*Cz-WLQG)##8jLuqdvhD)ali_ zN+ciZ6ulM*CG1JeWO3;#x`YnR8x6Qas0L{wk_;k&1RGSMl~BcP>kL`)N(uO`h+g$? zV|D*miT^LwcfiR9;?%Kzx(h|j0XWhpPYQ&f6j?zB`}_!rD1B!SqQ6cA1Uvv=t^(ex z0ACGoXhuh-JRFJU2DJnBKSm;e>wyeFgkT9c#9#>Urw|HF490sxaSxg^jgx)?C$Orm zLRT$aPOkvjqK-r*$h`;Cs84X80Vm0VJo+Dwf*B+cwc`Aa73M~=| z*a?*d$f0-I+EY@+PwTwKTDGc3-BB5-w9^(?%w)U;tmu_vAvlS{WkqMm!+M7g10dC2 zkZ6;(Ogby=Wz67)p^%5-ize6&21CgCeUY5e{eNS?&w5_V=L4rI@aeuZO>au4C)$I9 z^py;uyp86IzxvM;^G~g(F%3&vAEF^k6KN- z=rO6_U?aobH&pL5x&brxe%yK5dK!AchVBW!g1Uz>Q1{UJ8-_MFH8)Q>@6x4X{<5R_ zw$JwO`NghR?>&3n*U8n;_`bY)(^!ybkJj_3t}h6Z$7Yi+damo?w&I}``#I=r(ZcoerCc6+}-nd=cUe%UoO4(*0bwI zIW9_VJ?EiU;@`czV)EM$51ZHh_|nWr-;XKmyZ2w`nc z+wXE)yxS@-?%wO&A4Ha=C*ZjYnPt7_QQE&|Fq@1{YyhF zr>sA}{i<0{9BLlgG^AfKdSA!*kGFb(+>;v!I#KnH#2WXk9+7rz>B`4Ctx0o(rm=X=?ik!zK@# zG-c&4u5m8nDxq)g{E6wL71mK7KUjh$sIObpoeTq7G6Fhb#F+YZ6h1bhxoKE4P>I1B z0%W3T2>ZP0_FbQiSx?_Sq`OUdqoBR+aN7fuX1;#+7-Y!}&puU}w$J_Q$)8@DUU*r~B@>Z0hIhE`M+J zh3=?zh3Ec>*Ia`B=7YueZaa11!OvReTrlzHUtc6_dN$$M`~4$p){Z-(H?Lnm{>BR} zV^*$t=cLSIE64uqcMJCCn(ls+#Fy;3qAMQ#bcz7IvEqB&elTTc+)xuWpKhZ%yG2XOt_1@Rzn-kv>OIF`|aMR|KmtVL2bqHO)FTQwi)#!x>EI)tvjkcltL{xYg2f*?K0G5pnU}-U9(HBE9eGb1 z)cogD7yR8fddFq(re7X@`_=fm8R-+ZZ+zmD(`P@|dgJ=qxI64C7ri^V)9~8!E##Y% zl5f8zL`xlU*lArfpt*=x^)RzvX-UnXpg1cGM%9 zL$BRCN0ZAxdVUkM>M``KmRsikwVgQow3q+&=9aA!b}gRz-fQ!>oO|ClEfa>^F!rJ8 zljq7i=Z)VJoqW&6t{d!r=9vey@aDfC`8fZ^XB*x4rLFF?Zw-9)^Kx|T375lH8CEQN z2nz3Khv`=(HeWk!uTcD6Tr50EykEZ6FiE&gS^Y{!12VdsY5+vb$EaG1s(N)084e8v z8aO~ze-JFIjjB|s2;RoHLQEqI1qpP>#x#5`8%s89TQ06C6@@v2Cd%hP zUv)CjvTuIX^yUj3F9RB-jwv_T_S~aXvkqTBG2i6ws66)NsF=IVlA&#y~g)%UvRkcefs5*`!BIxzS6JR zVR`<(cQ%H}1^M_-NnTkL7>gg8y;Wt6xi% zFaNaVlv6f+aA}+A)tNhYwY-;p_x2fQo_OfCd#^1nTk_)Ri$-g2c=+#_^(TKj z|143n!jL*NzWc4c`!;G1`%{ZzPxA)l(77Y$1o!Q3nLc6nsL2z z)gwpV-`TbP(%b0APJim>$c&R_ztl4AQF7g4{${T z;*;*y2WGu9bP=6;{Oadse>1iFaJPTu#E~CI_dNUd2ZkZj=brNtdiy)bhtn<@HDX`z z?A6;gBKMx2o&RRbnm^4lopa~Jt3G}DP}A9GEJ$qloN%I>|2X6HX@3~~_=Tx&*Kcm^ z`o~FuH%H#F?yin)*;hvWhFCD=$)%@n{b2bR<9pA(KkBnbznXaAo^K{S@!UVx(YC7RQKFB>AcQ)^KZTQ62_L_dt}GqukXA4{5xL47ddvnbZ6(v z+8^UPAA!*YjOI{IP@bHbyzgM%IjvUBRzKc4X3uR8pLNduKj_=Px@Y$_yY5M?w!HY( z^5+gCuD`zgOb?(nTfiMvBQ&lLz?z}K{O1R$M!pwL0RM?WQK+N03^gyG-b-SDLKg?n z7?9B`h$i5R79d?;^QPr9|1Di=bOP3~yQ$88R6v{r3MYVP+#o>w*U=z1?*C7aGxwei zfc4ow4D0<+DR8|m3IwTNuyxou5TKJA0G(Xk+WQ>_k;$H~FqlrR*biSh#U9XJRTaP9 zJNlbRZQBKJN+%#hJ3upgs*K5C_FKd1JLqXCQ)|@{4%8u^f_4%Lt z$`+ctX7ls+zq#}lQ`>1>zrD!Vy8E)|wQVzQ4UV-=Ui|bgM@?Dw`Hrm@ugHIR=Xbi* znY$eO4w5$?*t8*3az0{RcShivSN_uR!k<^IlAi7id|Y|$o~tfj?%(x$SL}+jG5xQv z-}>f~{n;}{4L|?lE5_ZVc}l@&eGYGYN&MNiyMI6Y(FZm>KI_hRl=216*%PPfx|^>9 z`m_q%`i7QW{XYb^&BvO7{`=<}mVE%7+UF>b9;zGLR~Xp@P38*umiwgLVJ z@JS)MziEpOhfrwDh*6CT^g~Ai@$3EF+}u25DX?Vy<)($u$i|KOk%Q~c9%%5u6aX!T zZ0$M}yYaO7uWhn0w@lb|?UtVp{dHs%G@kExb?u2?zOeA|8Gm_e?p3DZCD(NPwEIlY z_^GXXcC4A=W)7^KIivf1^q%Wq^v_u8tIXPd&AzcQL(|tCBS%bvhrIK}x{XVBv|RgL z_LYm{vkrXz-HMZDe{{NM{XMQL=NYem;mFv(z4pPDU$#VBXTCG9>+th;=^r+K|Bs8N zJ{e3hD6#NcdHJNPY`cg2c254gr|w-v&3^m)=hwgd5pG(j``sV+jh!-b!Tpb&*T!sX zdFqCBMs&@&$-w$e-%j0g_Vd?Y$XN^fjIV!w%`Mkm@kOQd(F;2t{bcJ{WyCGIOWz-A cUAg3u$o2~kcb@V%dGX4G?XS|brIU642YUB#a{vGU 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 095/274] 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 096/274] 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 097/274] 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 098/274] 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 ad92fe49c7deeb098dcd442bc996602630f4f056 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 17 Oct 2023 11:58:45 -0400 Subject: [PATCH 099/274] implement initial concept of prompt chain --- crates/ai/src/templates/base.rs | 229 +++++++++++++++++++++++++++++--- 1 file changed, 208 insertions(+), 21 deletions(-) diff --git a/crates/ai/src/templates/base.rs b/crates/ai/src/templates/base.rs index 3d8479e512..74a4c424ae 100644 --- a/crates/ai/src/templates/base.rs +++ b/crates/ai/src/templates/base.rs @@ -1,15 +1,25 @@ -use std::cmp::Reverse; +use std::fmt::Write; +use std::{cmp::Reverse, sync::Arc}; + +use util::ResultExt; use crate::templates::repository_context::PromptCodeSnippet; +pub trait LanguageModel { + fn name(&self) -> String; + fn count_tokens(&self, content: &str) -> usize; + fn truncate(&self, content: &str, length: usize) -> String; + fn capacity(&self) -> usize; +} + pub(crate) enum PromptFileType { Text, Code, } -#[derive(Default)] +// TODO: Set this up to manage for defaults well pub struct PromptArguments { - pub model_name: String, + pub model: Arc, pub language_name: Option, pub project_name: Option, pub snippets: Vec, @@ -32,7 +42,11 @@ impl PromptArguments { } pub trait PromptTemplate { - fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String; + fn generate( + &self, + args: &PromptArguments, + max_token_length: Option, + ) -> anyhow::Result<(String, usize)>; } #[repr(i8)] @@ -53,24 +67,52 @@ impl PromptChain { args: PromptArguments, templates: Vec<(PromptPriority, Box)>, ) -> Self { - // templates.sort_by(|a, b| a.0.cmp(&b.0)); - PromptChain { args, templates } } - pub fn generate(&self, truncate: bool) -> anyhow::Result { + pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> { // Argsort based on Prompt Priority + let seperator = "\n"; + let seperator_tokens = self.args.model.count_tokens(seperator); let mut sorted_indices = (0..self.templates.len()).collect::>(); sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); - println!("{:?}", sorted_indices); - let mut prompts = Vec::new(); - for (_, template) in &self.templates { - prompts.push(template.generate(&self.args, None)); + + // If Truncate + let mut tokens_outstanding = if truncate { + Some(self.args.model.capacity() - self.args.reserved_tokens) + } else { + None + }; + + for idx in sorted_indices { + let (_, template) = &self.templates[idx]; + if let Some((template_prompt, prompt_token_count)) = + template.generate(&self.args, tokens_outstanding).log_err() + { + println!( + "GENERATED PROMPT ({:?}): {:?}", + &prompt_token_count, &template_prompt + ); + if template_prompt != "" { + prompts.push(template_prompt); + + if let Some(remaining_tokens) = tokens_outstanding { + let new_tokens = prompt_token_count + seperator_tokens; + tokens_outstanding = if remaining_tokens > new_tokens { + Some(remaining_tokens - new_tokens) + } else { + Some(0) + }; + } + } + } } - anyhow::Ok(prompts.join("\n")) + let full_prompt = prompts.join(seperator); + let total_token_count = self.args.model.count_tokens(&full_prompt); + anyhow::Ok((prompts.join(seperator), total_token_count)) } } @@ -82,21 +124,81 @@ pub(crate) mod tests { pub fn test_prompt_chain() { struct TestPromptTemplate {} impl PromptTemplate for TestPromptTemplate { - fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String { - "This is a test prompt template".to_string() + fn generate( + &self, + args: &PromptArguments, + max_token_length: Option, + ) -> anyhow::Result<(String, usize)> { + let mut content = "This is a test prompt template".to_string(); + + let mut token_count = args.model.count_tokens(&content); + if let Some(max_token_length) = max_token_length { + if token_count > max_token_length { + content = args.model.truncate(&content, max_token_length); + token_count = max_token_length; + } + } + + anyhow::Ok((content, token_count)) } } struct TestLowPriorityTemplate {} impl PromptTemplate for TestLowPriorityTemplate { - fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String { - "This is a low priority test prompt template".to_string() + fn generate( + &self, + args: &PromptArguments, + max_token_length: Option, + ) -> anyhow::Result<(String, usize)> { + let mut content = "This is a low priority test prompt template".to_string(); + + let mut token_count = args.model.count_tokens(&content); + if let Some(max_token_length) = max_token_length { + if token_count > max_token_length { + content = args.model.truncate(&content, max_token_length); + token_count = max_token_length; + } + } + + anyhow::Ok((content, token_count)) } } + #[derive(Clone)] + struct DummyLanguageModel { + capacity: usize, + } + + impl DummyLanguageModel { + fn set_capacity(&mut self, capacity: usize) { + self.capacity = capacity + } + } + + impl LanguageModel for DummyLanguageModel { + fn name(&self) -> String { + "dummy".to_string() + } + fn count_tokens(&self, content: &str) -> usize { + content.chars().collect::>().len() + } + fn truncate(&self, content: &str, length: usize) -> String { + content.chars().collect::>()[..length] + .into_iter() + .collect::() + } + fn capacity(&self) -> usize { + self.capacity + } + } + + let model: Arc = Arc::new(DummyLanguageModel { capacity: 100 }); let args = PromptArguments { - model_name: "gpt-4".to_string(), - ..Default::default() + model: model.clone(), + language_name: None, + project_name: None, + snippets: Vec::new(), + reserved_tokens: 0, }; let templates: Vec<(PromptPriority, Box)> = vec![ @@ -105,8 +207,93 @@ pub(crate) mod tests { ]; let chain = PromptChain::new(args, templates); - let prompt = chain.generate(false); - println!("{:?}", prompt); - panic!(); + let (prompt, token_count) = chain.generate(false).unwrap(); + + assert_eq!( + prompt, + "This is a test prompt template\nThis is a low priority test prompt template" + .to_string() + ); + + assert_eq!(model.count_tokens(&prompt), token_count); + + // Testing with Truncation Off + // Should ignore capacity and return all prompts + let model: Arc = Arc::new(DummyLanguageModel { capacity: 20 }); + let args = PromptArguments { + model: model.clone(), + language_name: None, + project_name: None, + snippets: Vec::new(), + reserved_tokens: 0, + }; + + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::High, Box::new(TestPromptTemplate {})), + (PromptPriority::Medium, Box::new(TestLowPriorityTemplate {})), + ]; + let chain = PromptChain::new(args, templates); + + let (prompt, token_count) = chain.generate(false).unwrap(); + + assert_eq!( + prompt, + "This is a test prompt template\nThis is a low priority test prompt template" + .to_string() + ); + + assert_eq!(model.count_tokens(&prompt), token_count); + + // Testing with Truncation Off + // Should ignore capacity and return all prompts + let capacity = 20; + let model: Arc = Arc::new(DummyLanguageModel { capacity }); + let args = PromptArguments { + model: model.clone(), + language_name: None, + project_name: None, + snippets: Vec::new(), + reserved_tokens: 0, + }; + + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::High, Box::new(TestPromptTemplate {})), + (PromptPriority::Medium, Box::new(TestLowPriorityTemplate {})), + (PromptPriority::Low, Box::new(TestLowPriorityTemplate {})), + ]; + let chain = PromptChain::new(args, templates); + + let (prompt, token_count) = chain.generate(true).unwrap(); + + assert_eq!(prompt, "This is a test promp".to_string()); + assert_eq!(token_count, capacity); + + // Change Ordering of Prompts Based on Priority + let capacity = 120; + let reserved_tokens = 10; + let model: Arc = Arc::new(DummyLanguageModel { capacity }); + let args = PromptArguments { + model: model.clone(), + language_name: None, + project_name: None, + snippets: Vec::new(), + reserved_tokens, + }; + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::Medium, Box::new(TestPromptTemplate {})), + (PromptPriority::High, Box::new(TestLowPriorityTemplate {})), + (PromptPriority::Low, Box::new(TestLowPriorityTemplate {})), + ]; + let chain = PromptChain::new(args, templates); + + let (prompt, token_count) = chain.generate(true).unwrap(); + println!("TOKEN COUNT: {:?}", token_count); + + assert_eq!( + prompt, + "This is a low priority test prompt template\nThis is a test prompt template\nThis is a low priority test prompt " + .to_string() + ); + assert_eq!(token_count, capacity - reserved_tokens); } } From 2456c077f698d72d411543f33fc1d3da3df14c95 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 10:01:31 -0600 Subject: [PATCH 100/274] 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 f225039d360e21a84eda2d6c157103d4169af83e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 17 Oct 2023 09:12:55 -0700 Subject: [PATCH 101/274] Display invite response buttons inline in notification panel --- crates/channel/src/channel_store.rs | 7 +- .../20221109000000_test_schema.sql | 5 +- .../20231004130100_create_notifications.sql | 5 +- crates/collab/src/db.rs | 2 + crates/collab/src/db/queries/channels.rs | 57 ++++--- crates/collab/src/db/queries/contacts.rs | 62 ++++--- crates/collab/src/db/queries/notifications.rs | 84 +++++++--- crates/collab/src/db/tables/notification.rs | 3 +- crates/collab/src/rpc.rs | 82 +++++----- crates/collab/src/tests/channel_tests.rs | 8 +- crates/collab/src/tests/test_server.rs | 8 +- crates/collab_ui/src/collab_panel.rs | 9 +- crates/collab_ui/src/notification_panel.rs | 154 ++++++++++++++---- .../notifications/src/notification_store.rs | 9 +- crates/rpc/proto/zed.proto | 9 +- crates/rpc/src/notification.rs | 11 +- crates/theme/src/theme.rs | 16 ++ styles/src/style_tree/app.ts | 2 + styles/src/style_tree/notification_panel.ts | 57 +++++++ 19 files changed, 421 insertions(+), 169 deletions(-) create mode 100644 styles/src/style_tree/notification_panel.ts diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 918a1e1dc1..d8dc7896ea 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -673,14 +673,15 @@ impl ChannelStore { &mut self, channel_id: ChannelId, accept: bool, - ) -> impl Future> { + cx: &mut ModelContext, + ) -> Task> { let client = self.client.clone(); - async move { + cx.background().spawn(async move { client .request(proto::RespondToChannelInvite { channel_id, accept }) .await?; Ok(()) - } + }) } pub fn get_channel_member_details( diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 4372d7dc8a..8e714f1444 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -322,12 +322,13 @@ CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ( CREATE TABLE "notifications" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "created_at" TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "actor_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "content" TEXT + "content" TEXT, + "is_read" BOOLEAN NOT NULL DEFAULT FALSE, + "response" BOOLEAN ); CREATE INDEX "index_notifications_on_recipient_id_is_read" ON "notifications" ("recipient_id", "is_read"); diff --git a/crates/collab/migrations/20231004130100_create_notifications.sql b/crates/collab/migrations/20231004130100_create_notifications.sql index 83cfd43978..277f16f4e3 100644 --- a/crates/collab/migrations/20231004130100_create_notifications.sql +++ b/crates/collab/migrations/20231004130100_create_notifications.sql @@ -7,12 +7,13 @@ CREATE UNIQUE INDEX "index_notification_kinds_on_name" ON "notification_kinds" ( CREATE TABLE notifications ( "id" SERIAL PRIMARY KEY, - "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "actor_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), - "content" TEXT + "content" TEXT, + "is_read" BOOLEAN NOT NULL DEFAULT FALSE, + "response" BOOLEAN ); CREATE INDEX "index_notifications_on_recipient_id" ON "notifications" ("recipient_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 1bf5c95f6b..852d3645dd 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -384,6 +384,8 @@ impl Contact { } } +pub type NotificationBatch = Vec<(UserId, proto::Notification)>; + #[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] pub struct Invite { pub email_address: String, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index d64b8028e3..9754c2ac83 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -161,7 +161,7 @@ impl Database { invitee_id: UserId, inviter_id: UserId, is_admin: bool, - ) -> Result> { + ) -> Result { self.transaction(move |tx| async move { self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) .await?; @@ -176,16 +176,18 @@ impl Database { .insert(&*tx) .await?; - self.create_notification( - invitee_id, - rpc::Notification::ChannelInvitation { - actor_id: inviter_id.to_proto(), - channel_id: channel_id.to_proto(), - }, - true, - &*tx, - ) - .await + Ok(self + .create_notification( + invitee_id, + rpc::Notification::ChannelInvitation { + channel_id: channel_id.to_proto(), + }, + true, + &*tx, + ) + .await? + .into_iter() + .collect()) }) .await } @@ -228,7 +230,7 @@ impl Database { channel_id: ChannelId, user_id: UserId, accept: bool, - ) -> Result<()> { + ) -> Result { self.transaction(move |tx| async move { let rows_affected = if accept { channel_member::Entity::update_many() @@ -246,21 +248,34 @@ impl Database { .await? .rows_affected } else { - channel_member::ActiveModel { - channel_id: ActiveValue::Unchanged(channel_id), - user_id: ActiveValue::Unchanged(user_id), - ..Default::default() - } - .delete(&*tx) - .await? - .rows_affected + channel_member::Entity::delete_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Accepted.eq(false)), + ) + .exec(&*tx) + .await? + .rows_affected }; if rows_affected == 0 { Err(anyhow!("no such invitation"))?; } - Ok(()) + Ok(self + .respond_to_notification( + user_id, + &rpc::Notification::ChannelInvitation { + channel_id: channel_id.to_proto(), + }, + accept, + &*tx, + ) + .await? + .into_iter() + .collect()) }) .await } diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index 709ed941f7..4509bb8495 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -123,7 +123,7 @@ impl Database { &self, sender_id: UserId, receiver_id: UserId, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { let (id_a, id_b, a_to_b) = if sender_id < receiver_id { (sender_id, receiver_id, true) @@ -164,15 +164,18 @@ impl Database { Err(anyhow!("contact already requested"))?; } - self.create_notification( - receiver_id, - rpc::Notification::ContactRequest { - actor_id: sender_id.to_proto(), - }, - true, - &*tx, - ) - .await + Ok(self + .create_notification( + receiver_id, + rpc::Notification::ContactRequest { + actor_id: sender_id.to_proto(), + }, + true, + &*tx, + ) + .await? + .into_iter() + .collect()) }) .await } @@ -274,7 +277,7 @@ impl Database { responder_id: UserId, requester_id: UserId, accept: bool, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { let (id_a, id_b, a_to_b) = if responder_id < requester_id { (responder_id, requester_id, false) @@ -316,15 +319,34 @@ impl Database { Err(anyhow!("no such contact request"))? } - self.create_notification( - requester_id, - rpc::Notification::ContactRequestAccepted { - actor_id: responder_id.to_proto(), - }, - true, - &*tx, - ) - .await + let mut notifications = Vec::new(); + notifications.extend( + self.respond_to_notification( + responder_id, + &rpc::Notification::ContactRequest { + actor_id: requester_id.to_proto(), + }, + accept, + &*tx, + ) + .await?, + ); + + if accept { + notifications.extend( + self.create_notification( + requester_id, + rpc::Notification::ContactRequestAccepted { + actor_id: responder_id.to_proto(), + }, + true, + &*tx, + ) + .await?, + ); + } + + Ok(notifications) }) .await } diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 50e961957c..d4024232b0 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -52,7 +52,7 @@ impl Database { while let Some(row) = rows.next().await { let row = row?; let kind = row.kind; - if let Some(proto) = self.model_to_proto(row) { + if let Some(proto) = model_to_proto(self, row) { result.push(proto); } else { log::warn!("unknown notification kind {:?}", kind); @@ -70,7 +70,7 @@ impl Database { notification: Notification, avoid_duplicates: bool, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result> { if avoid_duplicates { if self .find_notification(recipient_id, ¬ification, tx) @@ -94,20 +94,25 @@ impl Database { content: ActiveValue::Set(notification_proto.content.clone()), actor_id: ActiveValue::Set(actor_id), is_read: ActiveValue::NotSet, + response: ActiveValue::NotSet, created_at: ActiveValue::NotSet, id: ActiveValue::NotSet, } .save(&*tx) .await?; - Ok(Some(proto::Notification { - id: model.id.as_ref().to_proto(), - kind: notification_proto.kind, - timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, - is_read: false, - content: notification_proto.content, - actor_id: notification_proto.actor_id, - })) + Ok(Some(( + recipient_id, + proto::Notification { + id: model.id.as_ref().to_proto(), + kind: notification_proto.kind, + timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, + is_read: false, + response: None, + content: notification_proto.content, + actor_id: notification_proto.actor_id, + }, + ))) } pub async fn remove_notification( @@ -125,6 +130,32 @@ impl Database { Ok(id) } + pub async fn respond_to_notification( + &self, + recipient_id: UserId, + notification: &Notification, + response: bool, + tx: &DatabaseTransaction, + ) -> Result> { + if let Some(id) = self + .find_notification(recipient_id, notification, tx) + .await? + { + let row = notification::Entity::update(notification::ActiveModel { + id: ActiveValue::Unchanged(id), + recipient_id: ActiveValue::Unchanged(recipient_id), + response: ActiveValue::Set(Some(response)), + is_read: ActiveValue::Set(true), + ..Default::default() + }) + .exec(tx) + .await?; + Ok(model_to_proto(self, row).map(|notification| (recipient_id, notification))) + } else { + Ok(None) + } + } + pub async fn find_notification( &self, recipient_id: UserId, @@ -142,7 +173,11 @@ impl Database { .add(notification::Column::RecipientId.eq(recipient_id)) .add(notification::Column::IsRead.eq(false)) .add(notification::Column::Kind.eq(kind)) - .add(notification::Column::ActorId.eq(proto.actor_id)), + .add(if proto.actor_id.is_some() { + notification::Column::ActorId.eq(proto.actor_id) + } else { + notification::Column::ActorId.is_null() + }), ) .stream(&*tx) .await?; @@ -152,7 +187,7 @@ impl Database { while let Some(row) = rows.next().await { let row = row?; let id = row.id; - if let Some(proto) = self.model_to_proto(row) { + if let Some(proto) = model_to_proto(self, row) { if let Some(existing) = Notification::from_proto(&proto) { if existing == *notification { return Ok(Some(id)); @@ -163,16 +198,17 @@ impl Database { Ok(None) } - - fn model_to_proto(&self, row: notification::Model) -> Option { - let kind = self.notification_kinds_by_id.get(&row.kind)?; - Some(proto::Notification { - id: row.id.to_proto(), - kind: kind.to_string(), - timestamp: row.created_at.assume_utc().unix_timestamp() as u64, - is_read: row.is_read, - content: row.content, - actor_id: row.actor_id.map(|id| id.to_proto()), - }) - } +} + +fn model_to_proto(this: &Database, row: notification::Model) -> Option { + let kind = this.notification_kinds_by_id.get(&row.kind)?; + Some(proto::Notification { + id: row.id.to_proto(), + kind: kind.to_string(), + timestamp: row.created_at.assume_utc().unix_timestamp() as u64, + is_read: row.is_read, + response: row.response, + content: row.content, + actor_id: row.actor_id.map(|id| id.to_proto()), + }) } diff --git a/crates/collab/src/db/tables/notification.rs b/crates/collab/src/db/tables/notification.rs index a35e00fb5b..12517c04f6 100644 --- a/crates/collab/src/db/tables/notification.rs +++ b/crates/collab/src/db/tables/notification.rs @@ -7,12 +7,13 @@ use time::PrimitiveDateTime; pub struct Model { #[sea_orm(primary_key)] pub id: NotificationId, - pub is_read: bool, pub created_at: PrimitiveDateTime, pub recipient_id: UserId, pub actor_id: Option, pub kind: NotificationKindId, pub content: String, + pub is_read: bool, + pub response: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index cd82490649..9f3c22ce97 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2067,7 +2067,7 @@ async fn request_contact( return Err(anyhow!("cannot add yourself as a contact"))?; } - let notification = session + let notifications = session .db() .await .send_contact_request(requester_id, responder_id) @@ -2091,22 +2091,13 @@ async fn request_contact( .push(proto::IncomingContactRequest { requester_id: requester_id.to_proto(), }); - for connection_id in session - .connection_pool() - .await - .user_connection_ids(responder_id) - { + let connection_pool = session.connection_pool().await; + for connection_id in connection_pool.user_connection_ids(responder_id) { session.peer.send(connection_id, update.clone())?; - if let Some(notification) = ¬ification { - session.peer.send( - connection_id, - proto::NewNotification { - notification: Some(notification.clone()), - }, - )?; - } } + send_notifications(&*connection_pool, &session.peer, notifications); + response.send(proto::Ack {})?; Ok(()) } @@ -2125,7 +2116,7 @@ async fn respond_to_contact_request( } else { let accept = request.response == proto::ContactRequestResponse::Accept as i32; - let notification = db + let notifications = db .respond_to_contact_request(responder_id, requester_id, accept) .await?; let requester_busy = db.is_user_busy(requester_id).await?; @@ -2156,17 +2147,12 @@ async fn respond_to_contact_request( update .remove_outgoing_requests .push(responder_id.to_proto()); + for connection_id in pool.user_connection_ids(requester_id) { session.peer.send(connection_id, update.clone())?; - if let Some(notification) = ¬ification { - session.peer.send( - connection_id, - proto::NewNotification { - notification: Some(notification.clone()), - }, - )?; - } } + + send_notifications(&*pool, &session.peer, notifications); } response.send(proto::Ack {})?; @@ -2310,7 +2296,7 @@ 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 notification = db + let notifications = db .invite_channel_member(channel_id, invitee_id, session.user_id, request.admin) .await?; @@ -2325,22 +2311,13 @@ async fn invite_channel_member( name: channel.name, }); - for connection_id in session - .connection_pool() - .await - .user_connection_ids(invitee_id) - { + let pool = session.connection_pool().await; + for connection_id in pool.user_connection_ids(invitee_id) { session.peer.send(connection_id, update.clone())?; - if let Some(notification) = ¬ification { - session.peer.send( - connection_id, - proto::NewNotification { - notification: Some(notification.clone()), - }, - )?; - } } + send_notifications(&*pool, &session.peer, notifications); + response.send(proto::Ack {})?; Ok(()) } @@ -2588,7 +2565,8 @@ async fn respond_to_channel_invite( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - db.respond_to_channel_invite(channel_id, session.user_id, request.accept) + let notifications = db + .respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; let mut update = proto::UpdateChannels::default(); @@ -2636,6 +2614,11 @@ async fn respond_to_channel_invite( ); } session.peer.send(session.connection_id, update)?; + send_notifications( + &*session.connection_pool().await, + &session.peer, + notifications, + ); response.send(proto::Ack {})?; Ok(()) @@ -2853,6 +2836,29 @@ fn channel_buffer_updated( }); } +fn send_notifications( + connection_pool: &ConnectionPool, + peer: &Peer, + notifications: db::NotificationBatch, +) { + for (user_id, notification) in notifications { + for connection_id in connection_pool.user_connection_ids(user_id) { + if let Err(error) = peer.send( + connection_id, + proto::NewNotification { + notification: Some(notification.clone()), + }, + ) { + tracing::error!( + "failed to send notification to {:?} {}", + connection_id, + error + ); + } + } + } +} + async fn send_channel_message( request: proto::SendChannelMessage, response: Response, diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 7cfcce832b..fa82f55b39 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -117,8 +117,8 @@ async fn test_core_channels( // Client B accepts the invitation. client_b .channel_store() - .update(cx_b, |channels, _| { - channels.respond_to_channel_invite(channel_a_id, true) + .update(cx_b, |channels, cx| { + channels.respond_to_channel_invite(channel_a_id, true, cx) }) .await .unwrap(); @@ -856,8 +856,8 @@ async fn test_lost_channel_creation( // Client B accepts the invite client_b .channel_store() - .update(cx_b, |channel_store, _| { - channel_store.respond_to_channel_invite(channel_id, true) + .update(cx_b, |channel_store, cx| { + channel_store.respond_to_channel_invite(channel_id, true, cx) }) .await .unwrap(); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 9d03d1e17e..2dddd5961b 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -339,8 +339,8 @@ impl TestServer { member_cx .read(ChannelStore::global) - .update(*member_cx, |channels, _| { - channels.respond_to_channel_invite(channel_id, true) + .update(*member_cx, |channels, cx| { + channels.respond_to_channel_invite(channel_id, true, cx) }) .await .unwrap(); @@ -626,8 +626,8 @@ impl TestClient { other_cx .read(ChannelStore::global) - .update(other_cx, |channel_store, _| { - channel_store.respond_to_channel_invite(channel, true) + .update(other_cx, |channel_store, cx| { + channel_store.respond_to_channel_invite(channel, true, cx) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 30505b0876..911b94ae93 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3181,10 +3181,11 @@ impl CollabPanel { accept: bool, cx: &mut ViewContext, ) { - let respond = self.channel_store.update(cx, |store, _| { - store.respond_to_channel_invite(channel_id, accept) - }); - cx.foreground().spawn(respond).detach(); + self.channel_store + .update(cx, |store, cx| { + store.respond_to_channel_invite(channel_id, accept, cx) + }) + .detach(); } fn call( diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 7bf5000ec8..73c07949d0 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -183,32 +183,31 @@ impl NotificationPanel { let user_store = self.user_store.read(cx); let channel_store = self.channel_store.read(cx); let entry = notification_store.notification_at(ix)?; + let notification = entry.notification.clone(); let now = OffsetDateTime::now_utc(); let timestamp = entry.timestamp; let icon; let text; let actor; - match entry.notification { - Notification::ContactRequest { - actor_id: requester_id, - } => { - actor = user_store.get_cached_user(requester_id)?; + let needs_acceptance; + match notification { + Notification::ContactRequest { actor_id } => { + let requester = user_store.get_cached_user(actor_id)?; icon = "icons/plus.svg"; - text = format!("{} wants to add you as a contact", actor.github_login); + text = format!("{} wants to add you as a contact", requester.github_login); + needs_acceptance = true; + actor = Some(requester); } - Notification::ContactRequestAccepted { - actor_id: contact_id, - } => { - actor = user_store.get_cached_user(contact_id)?; + Notification::ContactRequestAccepted { actor_id } => { + let responder = user_store.get_cached_user(actor_id)?; icon = "icons/plus.svg"; - text = format!("{} accepted your contact invite", actor.github_login); + text = format!("{} accepted your contact invite", responder.github_login); + needs_acceptance = false; + actor = Some(responder); } - Notification::ChannelInvitation { - actor_id: inviter_id, - channel_id, - } => { - actor = user_store.get_cached_user(inviter_id)?; + Notification::ChannelInvitation { channel_id } => { + actor = None; let channel = channel_store.channel_for_id(channel_id).or_else(|| { channel_store .channel_invitations() @@ -217,39 +216,51 @@ impl NotificationPanel { })?; icon = "icons/hash.svg"; - text = format!( - "{} invited you to join the #{} channel", - actor.github_login, channel.name - ); + text = format!("you were invited to join the #{} channel", channel.name); + needs_acceptance = true; } Notification::ChannelMessageMention { - actor_id: sender_id, + actor_id, channel_id, message_id, } => { - actor = user_store.get_cached_user(sender_id)?; + let sender = user_store.get_cached_user(actor_id)?; let channel = channel_store.channel_for_id(channel_id)?; let message = notification_store.channel_message_for_id(message_id)?; icon = "icons/conversations.svg"; text = format!( "{} mentioned you in the #{} channel:\n{}", - actor.github_login, channel.name, message.body, + sender.github_login, channel.name, message.body, ); + needs_acceptance = false; + actor = Some(sender); } } let theme = theme::current(cx); - let style = &theme.chat_panel.message; + let style = &theme.notification_panel; + let response = entry.response; + + let message_style = if entry.is_read { + style.read_text.clone() + } else { + style.unread_text.clone() + }; + + enum Decline {} + enum Accept {} Some( - MouseEventHandler::new::(ix, cx, |state, _| { - let container = style.container.style_for(state); + MouseEventHandler::new::(ix, cx, |_, cx| { + let container = message_style.container; Flex::column() .with_child( Flex::row() - .with_child(render_avatar(actor.avatar.clone(), &theme)) + .with_children( + actor.map(|actor| render_avatar(actor.avatar.clone(), &theme)), + ) .with_child(render_icon_button(&theme.chat_panel.icon_button, icon)) .with_child( Label::new( @@ -261,9 +272,69 @@ impl NotificationPanel { ) .align_children_center(), ) - .with_child(Text::new(text, style.body.clone())) + .with_child(Text::new(text, message_style.text.clone())) + .with_children(if let Some(is_accepted) = response { + Some( + Label::new( + if is_accepted { "Accepted" } else { "Declined" }, + style.button.text.clone(), + ) + .into_any(), + ) + } else if needs_acceptance { + Some( + Flex::row() + .with_children([ + MouseEventHandler::new::(ix, cx, |state, _| { + let button = style.button.style_for(state); + Label::new("Decline", button.text.clone()) + .contained() + .with_style(button.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click( + MouseButton::Left, + { + let notification = notification.clone(); + move |_, view, cx| { + view.respond_to_notification( + notification.clone(), + false, + cx, + ); + } + }, + ), + MouseEventHandler::new::(ix, cx, |state, _| { + let button = style.button.style_for(state); + Label::new("Accept", button.text.clone()) + .contained() + .with_style(button.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click( + MouseButton::Left, + { + let notification = notification.clone(); + move |_, view, cx| { + view.respond_to_notification( + notification.clone(), + true, + cx, + ); + } + }, + ), + ]) + .aligned() + .right() + .into_any(), + ) + } else { + None + }) .contained() - .with_style(*container) + .with_style(container) .into_any() }) .into_any(), @@ -373,6 +444,31 @@ impl NotificationPanel { Notification::ChannelMessageMention { .. } => {} } } + + fn respond_to_notification( + &mut self, + notification: Notification, + response: bool, + cx: &mut ViewContext, + ) { + match notification { + Notification::ContactRequest { actor_id } => { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(actor_id, response, cx) + }) + .detach(); + } + Notification::ChannelInvitation { channel_id, .. } => { + self.channel_store + .update(cx, |store, cx| { + store.respond_to_channel_invite(channel_id, response, cx) + }) + .detach(); + } + _ => {} + } + } } impl Entity for NotificationPanel { diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index af39941d2f..d0691db106 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -44,6 +44,7 @@ pub struct NotificationEntry { pub notification: Notification, pub timestamp: OffsetDateTime, pub is_read: bool, + pub response: Option, } #[derive(Clone, Debug, Default)] @@ -186,6 +187,7 @@ impl NotificationStore { timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) .ok()?, notification: Notification::from_proto(&message)?, + response: message.response, }) }) .collect::>(); @@ -195,12 +197,7 @@ impl NotificationStore { for entry in ¬ifications { match entry.notification { - Notification::ChannelInvitation { - actor_id: inviter_id, - .. - } => { - user_ids.push(inviter_id); - } + Notification::ChannelInvitation { .. } => {} Notification::ContactRequest { actor_id: requester_id, } => { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d27bbade6f..46db82047e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1598,8 +1598,9 @@ message DeleteNotification { message Notification { uint64 id = 1; uint64 timestamp = 2; - bool is_read = 3; - string kind = 4; - string content = 5; - optional uint64 actor_id = 6; + string kind = 3; + string content = 4; + optional uint64 actor_id = 5; + bool is_read = 6; + optional bool response = 7; } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 6ff9660159..b03e928197 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -13,7 +13,8 @@ const ACTOR_ID: &'static str = "actor_id"; /// variant, add a serde alias for the old name. /// /// When a notification is initiated by a user, use the `actor_id` field -/// to store the user's id. +/// to store the user's id. This is value is stored in a dedicated column +/// in the database, so it can be queried more efficiently. #[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum Notification { @@ -24,7 +25,6 @@ pub enum Notification { actor_id: u64, }, ChannelInvitation { - actor_id: u64, channel_id: u64, }, ChannelMessageMention { @@ -40,7 +40,7 @@ impl Notification { let mut actor_id = None; let value = value.as_object_mut().unwrap(); let Some(Value::String(kind)) = value.remove(KIND) else { - unreachable!() + unreachable!("kind is the enum tag") }; if let map::Entry::Occupied(e) = value.entry(ACTOR_ID) { if e.get().is_u64() { @@ -76,10 +76,7 @@ fn test_notification() { for notification in [ Notification::ContactRequest { actor_id: 1 }, Notification::ContactRequestAccepted { actor_id: 2 }, - Notification::ChannelInvitation { - actor_id: 0, - channel_id: 100, - }, + Notification::ChannelInvitation { channel_id: 100 }, Notification::ChannelMessageMention { actor_id: 200, channel_id: 30, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f335444b58..389d15ef05 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -53,6 +53,7 @@ pub struct Theme { pub collab_panel: CollabPanel, pub project_panel: ProjectPanel, pub chat_panel: ChatPanel, + pub notification_panel: NotificationPanel, pub command_palette: CommandPalette, pub picker: Picker, pub editor: Editor, @@ -644,6 +645,21 @@ pub struct ChatPanel { pub icon_button: Interactive, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct NotificationPanel { + #[serde(flatten)] + pub container: ContainerStyle, + pub list: ContainerStyle, + pub avatar: AvatarStyle, + pub avatar_container: ContainerStyle, + pub sign_in_prompt: Interactive, + pub icon_button: Interactive, + pub unread_text: ContainedText, + pub read_text: ContainedText, + pub timestamp: ContainedText, + pub button: Interactive, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct ChatMessage { #[serde(flatten)] diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index 3233909fd0..aff934e9c6 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -13,6 +13,7 @@ import project_shared_notification from "./project_shared_notification" import tooltip from "./tooltip" import terminal from "./terminal" import chat_panel from "./chat_panel" +import notification_panel from "./notification_panel" import collab_panel from "./collab_panel" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" @@ -57,6 +58,7 @@ export default function app(): any { assistant: assistant(), feedback: feedback(), chat_panel: chat_panel(), + notification_panel: notification_panel(), component_test: component_test(), } } diff --git a/styles/src/style_tree/notification_panel.ts b/styles/src/style_tree/notification_panel.ts new file mode 100644 index 0000000000..9afdf1e00a --- /dev/null +++ b/styles/src/style_tree/notification_panel.ts @@ -0,0 +1,57 @@ +import { background, text } from "./components" +import { icon_button } from "../component/icon_button" +import { useTheme } from "../theme" +import { interactive } from "../element" + +export default function chat_panel(): any { + const theme = useTheme() + const layer = theme.middle + + return { + background: background(layer), + avatar: { + icon_width: 24, + icon_height: 24, + corner_radius: 4, + outer_width: 24, + outer_corner_radius: 16, + }, + read_text: text(layer, "sans", "base"), + unread_text: text(layer, "sans", "base"), + button: interactive({ + base: { + ...text(theme.lowest, "sans", "on", { size: "xs" }), + background: background(theme.lowest, "on"), + padding: 4, + corner_radius: 6, + margin: { left: 6 }, + }, + + state: { + hovered: { + background: background(theme.lowest, "on", "hovered"), + }, + }, + }), + timestamp: text(layer, "sans", "base", "disabled"), + avatar_container: { + padding: { + right: 6, + left: 2, + top: 2, + bottom: 2, + } + }, + list: { + + }, + icon_button: icon_button({ + variant: "ghost", + color: "variant", + size: "sm", + }), + sign_in_prompt: { + default: text(layer, "sans", "base"), + } + } +} From 3412becfc53ad2551412f586ef58b2c589fe3810 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 10:15:20 -0600 Subject: [PATCH 102/274] 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 103/274] 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 104/274] 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 f2d36a47ae5df75b4e52f027b3a1835740bce678 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 17 Oct 2023 10:34:50 -0700 Subject: [PATCH 105/274] Generalize notifications' actor id to entity id This way, we can retrieve channel invite notifications when responding to the invites. --- .../20221109000000_test_schema.sql | 2 +- .../20231004130100_create_notifications.sql | 2 +- crates/collab/src/db.rs | 2 +- crates/collab/src/db/queries/channels.rs | 7 ++ crates/collab/src/db/queries/contacts.rs | 8 +- crates/collab/src/db/queries/notifications.rs | 91 ++++++++++--------- crates/collab/src/db/tables/notification.rs | 2 +- crates/collab/src/db/tests.rs | 4 +- crates/collab/src/lib.rs | 2 +- crates/collab_ui/src/notification_panel.rs | 37 ++++---- .../notifications/src/notification_store.rs | 6 +- crates/rpc/proto/zed.proto | 4 +- crates/rpc/src/notification.rs | 46 ++++++---- 13 files changed, 115 insertions(+), 98 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 8e714f1444..1efd14e6eb 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -324,8 +324,8 @@ CREATE TABLE "notifications" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "created_at" TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "actor_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), + "entity_id" INTEGER, "content" TEXT, "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "response" BOOLEAN diff --git a/crates/collab/migrations/20231004130100_create_notifications.sql b/crates/collab/migrations/20231004130100_create_notifications.sql index 277f16f4e3..cdc6674ff1 100644 --- a/crates/collab/migrations/20231004130100_create_notifications.sql +++ b/crates/collab/migrations/20231004130100_create_notifications.sql @@ -9,8 +9,8 @@ CREATE TABLE notifications ( "id" SERIAL PRIMARY KEY, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "recipient_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "actor_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "kind" INTEGER NOT NULL REFERENCES notification_kinds (id), + "entity_id" INTEGER, "content" TEXT, "is_read" BOOLEAN NOT NULL DEFAULT FALSE, "response" BOOLEAN diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 852d3645dd..4c9e47a270 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -125,7 +125,7 @@ impl Database { } pub async fn initialize_static_data(&mut self) -> Result<()> { - self.initialize_notification_enum().await?; + self.initialize_notification_kinds().await?; Ok(()) } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 9754c2ac83..745bd6e3ab 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -166,6 +166,11 @@ impl Database { self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) .await?; + let channel = channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such channel"))?; + channel_member::ActiveModel { channel_id: ActiveValue::Set(channel_id), user_id: ActiveValue::Set(invitee_id), @@ -181,6 +186,7 @@ impl Database { invitee_id, rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), + channel_name: channel.name, }, true, &*tx, @@ -269,6 +275,7 @@ impl Database { user_id, &rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), + channel_name: Default::default(), }, accept, &*tx, diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index 4509bb8495..841f9faa20 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -168,7 +168,7 @@ impl Database { .create_notification( receiver_id, rpc::Notification::ContactRequest { - actor_id: sender_id.to_proto(), + sender_id: sender_id.to_proto(), }, true, &*tx, @@ -219,7 +219,7 @@ impl Database { .remove_notification( responder_id, rpc::Notification::ContactRequest { - actor_id: requester_id.to_proto(), + sender_id: requester_id.to_proto(), }, &*tx, ) @@ -324,7 +324,7 @@ impl Database { self.respond_to_notification( responder_id, &rpc::Notification::ContactRequest { - actor_id: requester_id.to_proto(), + sender_id: requester_id.to_proto(), }, accept, &*tx, @@ -337,7 +337,7 @@ impl Database { self.create_notification( requester_id, rpc::Notification::ContactRequestAccepted { - actor_id: responder_id.to_proto(), + responder_id: responder_id.to_proto(), }, true, &*tx, diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index d4024232b0..893bedb72b 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -2,7 +2,7 @@ use super::*; use rpc::Notification; impl Database { - pub async fn initialize_notification_enum(&mut self) -> Result<()> { + pub async fn initialize_notification_kinds(&mut self) -> Result<()> { notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map( |kind| notification_kind::ActiveModel { name: ActiveValue::Set(kind.to_string()), @@ -64,6 +64,9 @@ impl Database { .await } + /// Create a notification. If `avoid_duplicates` is set to true, then avoid + /// creating a new notification if the given recipient already has an + /// unread notification with the given kind and entity id. pub async fn create_notification( &self, recipient_id: UserId, @@ -81,22 +84,14 @@ impl Database { } } - let notification_proto = notification.to_proto(); - let kind = *self - .notification_kinds_by_name - .get(¬ification_proto.kind) - .ok_or_else(|| anyhow!("invalid notification kind {:?}", notification_proto.kind))?; - let actor_id = notification_proto.actor_id.map(|id| UserId::from_proto(id)); - + let proto = notification.to_proto(); + let kind = notification_kind_from_proto(self, &proto)?; let model = notification::ActiveModel { recipient_id: ActiveValue::Set(recipient_id), kind: ActiveValue::Set(kind), - content: ActiveValue::Set(notification_proto.content.clone()), - actor_id: ActiveValue::Set(actor_id), - is_read: ActiveValue::NotSet, - response: ActiveValue::NotSet, - created_at: ActiveValue::NotSet, - id: ActiveValue::NotSet, + entity_id: ActiveValue::Set(proto.entity_id.map(|id| id as i32)), + content: ActiveValue::Set(proto.content.clone()), + ..Default::default() } .save(&*tx) .await?; @@ -105,16 +100,18 @@ impl Database { recipient_id, proto::Notification { id: model.id.as_ref().to_proto(), - kind: notification_proto.kind, + kind: proto.kind, timestamp: model.created_at.as_ref().assume_utc().unix_timestamp() as u64, is_read: false, response: None, - content: notification_proto.content, - actor_id: notification_proto.actor_id, + content: proto.content, + entity_id: proto.entity_id, }, ))) } + /// Remove an unread notification with the given recipient, kind and + /// entity id. pub async fn remove_notification( &self, recipient_id: UserId, @@ -130,6 +127,8 @@ impl Database { Ok(id) } + /// Populate the response for the notification with the given kind and + /// entity id. pub async fn respond_to_notification( &self, recipient_id: UserId, @@ -156,47 +155,38 @@ impl Database { } } - pub async fn find_notification( + /// Find an unread notification by its recipient, kind and entity id. + async fn find_notification( &self, recipient_id: UserId, notification: &Notification, tx: &DatabaseTransaction, ) -> Result> { let proto = notification.to_proto(); - let kind = *self - .notification_kinds_by_name - .get(&proto.kind) - .ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?; - let mut rows = notification::Entity::find() + let kind = notification_kind_from_proto(self, &proto)?; + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryIds { + Id, + } + + Ok(notification::Entity::find() + .select_only() + .column(notification::Column::Id) .filter( Condition::all() .add(notification::Column::RecipientId.eq(recipient_id)) .add(notification::Column::IsRead.eq(false)) .add(notification::Column::Kind.eq(kind)) - .add(if proto.actor_id.is_some() { - notification::Column::ActorId.eq(proto.actor_id) + .add(if proto.entity_id.is_some() { + notification::Column::EntityId.eq(proto.entity_id) } else { - notification::Column::ActorId.is_null() + notification::Column::EntityId.is_null() }), ) - .stream(&*tx) - .await?; - - // Don't rely on the JSON serialization being identical, in case the - // notification type is changed in backward-compatible ways. - while let Some(row) = rows.next().await { - let row = row?; - let id = row.id; - if let Some(proto) = model_to_proto(self, row) { - if let Some(existing) = Notification::from_proto(&proto) { - if existing == *notification { - return Ok(Some(id)); - } - } - } - } - - Ok(None) + .into_values::<_, QueryIds>() + .one(&*tx) + .await?) } } @@ -209,6 +199,17 @@ fn model_to_proto(this: &Database, row: notification::Model) -> Option Result { + Ok(this + .notification_kinds_by_name + .get(&proto.kind) + .copied() + .ok_or_else(|| anyhow!("invalid notification kind {:?}", proto.kind))?) +} diff --git a/crates/collab/src/db/tables/notification.rs b/crates/collab/src/db/tables/notification.rs index 12517c04f6..3105198fa2 100644 --- a/crates/collab/src/db/tables/notification.rs +++ b/crates/collab/src/db/tables/notification.rs @@ -9,8 +9,8 @@ pub struct Model { pub id: NotificationId, pub created_at: PrimitiveDateTime, pub recipient_id: UserId, - pub actor_id: Option, pub kind: NotificationKindId, + pub entity_id: Option, pub content: String, pub is_read: bool, pub response: Option, diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 465ff56444..f05a4cbebb 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -45,7 +45,7 @@ impl TestDb { )) .await .unwrap(); - db.initialize_notification_enum().await.unwrap(); + db.initialize_notification_kinds().await.unwrap(); db }); @@ -85,7 +85,7 @@ impl TestDb { .unwrap(); let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"); db.migrate(Path::new(migrations_path), false).await.unwrap(); - db.initialize_notification_enum().await.unwrap(); + db.initialize_notification_kinds().await.unwrap(); db }); diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 1722424217..85216525b0 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -120,7 +120,7 @@ impl AppState { let mut db_options = db::ConnectOptions::new(config.database_url.clone()); db_options.max_connections(config.database_max_connections); let mut db = Database::new(db_options, Executor::Production).await?; - db.initialize_notification_enum().await?; + db.initialize_notification_kinds().await?; let live_kit_client = if let Some(((server, key), secret)) = config .live_kit_server diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 73c07949d0..3f1bafb10e 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -192,39 +192,34 @@ impl NotificationPanel { let actor; let needs_acceptance; match notification { - Notification::ContactRequest { actor_id } => { - let requester = user_store.get_cached_user(actor_id)?; + Notification::ContactRequest { sender_id } => { + let requester = user_store.get_cached_user(sender_id)?; icon = "icons/plus.svg"; text = format!("{} wants to add you as a contact", requester.github_login); needs_acceptance = true; actor = Some(requester); } - Notification::ContactRequestAccepted { actor_id } => { - let responder = user_store.get_cached_user(actor_id)?; + Notification::ContactRequestAccepted { responder_id } => { + let responder = user_store.get_cached_user(responder_id)?; icon = "icons/plus.svg"; text = format!("{} accepted your contact invite", responder.github_login); needs_acceptance = false; actor = Some(responder); } - Notification::ChannelInvitation { channel_id } => { + Notification::ChannelInvitation { + ref channel_name, .. + } => { actor = None; - let channel = channel_store.channel_for_id(channel_id).or_else(|| { - channel_store - .channel_invitations() - .iter() - .find(|c| c.id == channel_id) - })?; - icon = "icons/hash.svg"; - text = format!("you were invited to join the #{} channel", channel.name); + text = format!("you were invited to join the #{channel_name} channel"); needs_acceptance = true; } Notification::ChannelMessageMention { - actor_id, + sender_id, channel_id, message_id, } => { - let sender = user_store.get_cached_user(actor_id)?; + let sender = user_store.get_cached_user(sender_id)?; let channel = channel_store.channel_for_id(channel_id)?; let message = notification_store.channel_message_for_id(message_id)?; @@ -405,8 +400,12 @@ impl NotificationPanel { fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { let id = entry.id as usize; match entry.notification { - Notification::ContactRequest { actor_id } - | Notification::ContactRequestAccepted { actor_id } => { + Notification::ContactRequest { + sender_id: actor_id, + } + | Notification::ContactRequestAccepted { + responder_id: actor_id, + } => { let user_store = self.user_store.clone(); let Some(user) = user_store.read(cx).get_cached_user(actor_id) else { return; @@ -452,7 +451,9 @@ impl NotificationPanel { cx: &mut ViewContext, ) { match notification { - Notification::ContactRequest { actor_id } => { + Notification::ContactRequest { + sender_id: actor_id, + } => { self.user_store .update(cx, |store, cx| { store.respond_to_contact_request(actor_id, response, cx) diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index d0691db106..43afb8181a 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -199,17 +199,17 @@ impl NotificationStore { match entry.notification { Notification::ChannelInvitation { .. } => {} Notification::ContactRequest { - actor_id: requester_id, + sender_id: requester_id, } => { user_ids.push(requester_id); } Notification::ContactRequestAccepted { - actor_id: contact_id, + responder_id: contact_id, } => { user_ids.push(contact_id); } Notification::ChannelMessageMention { - actor_id: sender_id, + sender_id, message_id, .. } => { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 46db82047e..a5ba1c1cf7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1599,8 +1599,8 @@ message Notification { uint64 id = 1; uint64 timestamp = 2; string kind = 3; - string content = 4; - optional uint64 actor_id = 5; + optional uint64 entity_id = 4; + string content = 5; bool is_read = 6; optional bool response = 7; } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index b03e928197..06dff82b75 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -4,32 +4,37 @@ use serde_json::{map, Value}; use strum::{EnumVariantNames, VariantNames as _}; const KIND: &'static str = "kind"; -const ACTOR_ID: &'static str = "actor_id"; +const ENTITY_ID: &'static str = "entity_id"; -/// A notification that can be stored, associated with a given user. +/// A notification that can be stored, associated with a given recipient. /// /// This struct is stored in the collab database as JSON, so it shouldn't be /// changed in a backward-incompatible way. For example, when renaming a /// variant, add a serde alias for the old name. /// -/// When a notification is initiated by a user, use the `actor_id` field -/// to store the user's id. This is value is stored in a dedicated column -/// in the database, so it can be queried more efficiently. +/// Most notification types have a special field which is aliased to +/// `entity_id`. This field is stored in its own database column, and can +/// be used to query the notification. #[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum Notification { ContactRequest { - actor_id: u64, + #[serde(rename = "entity_id")] + sender_id: u64, }, ContactRequestAccepted { - actor_id: u64, + #[serde(rename = "entity_id")] + responder_id: u64, }, ChannelInvitation { + #[serde(rename = "entity_id")] channel_id: u64, + channel_name: String, }, ChannelMessageMention { - actor_id: u64, + sender_id: u64, channel_id: u64, + #[serde(rename = "entity_id")] message_id: u64, }, } @@ -37,19 +42,19 @@ pub enum Notification { impl Notification { pub fn to_proto(&self) -> proto::Notification { let mut value = serde_json::to_value(self).unwrap(); - let mut actor_id = None; + let mut entity_id = None; let value = value.as_object_mut().unwrap(); let Some(Value::String(kind)) = value.remove(KIND) else { unreachable!("kind is the enum tag") }; - if let map::Entry::Occupied(e) = value.entry(ACTOR_ID) { + if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) { if e.get().is_u64() { - actor_id = e.remove().as_u64(); + entity_id = e.remove().as_u64(); } } proto::Notification { kind, - actor_id, + entity_id, content: serde_json::to_string(&value).unwrap(), ..Default::default() } @@ -59,8 +64,8 @@ impl Notification { let mut value = serde_json::from_str::(¬ification.content).ok()?; let object = value.as_object_mut()?; object.insert(KIND.into(), notification.kind.to_string().into()); - if let Some(actor_id) = notification.actor_id { - object.insert(ACTOR_ID.into(), actor_id.into()); + if let Some(entity_id) = notification.entity_id { + object.insert(ENTITY_ID.into(), entity_id.into()); } serde_json::from_value(value).ok() } @@ -74,11 +79,14 @@ impl Notification { fn test_notification() { // Notifications can be serialized and deserialized. for notification in [ - Notification::ContactRequest { actor_id: 1 }, - Notification::ContactRequestAccepted { actor_id: 2 }, - Notification::ChannelInvitation { channel_id: 100 }, + Notification::ContactRequest { sender_id: 1 }, + Notification::ContactRequestAccepted { responder_id: 2 }, + Notification::ChannelInvitation { + channel_id: 100, + channel_name: "the-channel".into(), + }, Notification::ChannelMessageMention { - actor_id: 200, + sender_id: 200, channel_id: 30, message_id: 1, }, @@ -90,6 +98,6 @@ fn test_notification() { // When notifications are serialized, the `kind` and `actor_id` fields are // stored separately, and do not appear redundantly in the JSON. - let notification = Notification::ContactRequest { actor_id: 1 }; + let notification = Notification::ContactRequest { sender_id: 1 }; assert_eq!(notification.to_proto().content, "{}"); } From 8db389313bf993d60ecf774122eea276ef0546d2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 17 Oct 2023 13:34:51 -0400 Subject: [PATCH 106/274] 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 52834dbf210845343ade57b084f3db2b1dc2e8ff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 17 Oct 2023 11:21:38 -0700 Subject: [PATCH 107/274] Add notifications integration test --- crates/collab/src/tests.rs | 1 + crates/collab/src/tests/notification_tests.rs | 115 ++++++++++++++++++ crates/collab/src/tests/test_server.rs | 7 ++ crates/collab_ui/src/notification_panel.rs | 25 +--- .../notifications/src/notification_store.rs | 36 +++++- 5 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 crates/collab/src/tests/notification_tests.rs diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e78bbe3466..139910e1f6 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -6,6 +6,7 @@ mod channel_message_tests; mod channel_tests; mod following_tests; mod integration_tests; +mod notification_tests; mod random_channel_buffer_tests; mod random_project_collaboration_tests; mod randomized_test_helpers; diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs new file mode 100644 index 0000000000..da94bd6fad --- /dev/null +++ b/crates/collab/src/tests/notification_tests.rs @@ -0,0 +1,115 @@ +use crate::tests::TestServer; +use gpui::{executor::Deterministic, TestAppContext}; +use rpc::Notification; +use std::sync::Arc; + +#[gpui::test] +async fn test_notifications( + 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; + + // Client A sends a contact request to client B. + client_a + .user_store() + .update(cx_a, |store, cx| store.request_contact(client_b.id(), cx)) + .await + .unwrap(); + + // Client B receives a contact request notification and responds to the + // request, accepting it. + deterministic.run_until_parked(); + client_b.notification_store().update(cx_b, |store, cx| { + assert_eq!(store.notification_count(), 1); + assert_eq!(store.unread_notification_count(), 1); + + let entry = store.notification_at(0).unwrap(); + assert_eq!( + entry.notification, + Notification::ContactRequest { + sender_id: client_a.id() + } + ); + assert!(!entry.is_read); + + store.respond_to_notification(entry.notification.clone(), true, cx); + }); + + // Client B sees the notification is now read, and that they responded. + deterministic.run_until_parked(); + client_b.notification_store().read_with(cx_b, |store, _| { + assert_eq!(store.notification_count(), 1); + assert_eq!(store.unread_notification_count(), 0); + + let entry = store.notification_at(0).unwrap(); + assert!(entry.is_read); + assert_eq!(entry.response, Some(true)); + }); + + // Client A receives a notification that client B accepted their request. + client_a.notification_store().read_with(cx_a, |store, _| { + assert_eq!(store.notification_count(), 1); + assert_eq!(store.unread_notification_count(), 1); + + let entry = store.notification_at(0).unwrap(); + assert_eq!( + entry.notification, + Notification::ContactRequestAccepted { + responder_id: client_b.id() + } + ); + assert!(!entry.is_read); + }); + + // Client A creates a channel and invites client B to be a member. + let channel_id = client_a + .channel_store() + .update(cx_a, |store, cx| { + store.create_channel("the-channel", None, cx) + }) + .await + .unwrap(); + client_a + .channel_store() + .update(cx_a, |store, cx| { + store.invite_member(channel_id, client_b.id(), false, cx) + }) + .await + .unwrap(); + + // Client B receives a channel invitation notification and responds to the + // invitation, accepting it. + deterministic.run_until_parked(); + client_b.notification_store().update(cx_b, |store, cx| { + assert_eq!(store.notification_count(), 2); + assert_eq!(store.unread_notification_count(), 1); + + let entry = store.notification_at(1).unwrap(); + assert_eq!( + entry.notification, + Notification::ChannelInvitation { + channel_id, + channel_name: "the-channel".to_string() + } + ); + assert!(!entry.is_read); + + store.respond_to_notification(entry.notification.clone(), true, cx); + }); + + // Client B sees the notification is now read, and that they responded. + deterministic.run_until_parked(); + client_b.notification_store().read_with(cx_b, |store, _| { + assert_eq!(store.notification_count(), 2); + assert_eq!(store.unread_notification_count(), 0); + + let entry = store.notification_at(1).unwrap(); + assert!(entry.is_read); + assert_eq!(entry.response, Some(true)); + }); +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 2dddd5961b..806b57bb59 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -16,6 +16,7 @@ use futures::{channel::oneshot, StreamExt as _}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; +use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; use rpc::RECEIVE_TIMEOUT; @@ -46,6 +47,7 @@ pub struct TestClient { pub username: String, pub app_state: Arc, channel_store: ModelHandle, + notification_store: ModelHandle, state: RefCell, } @@ -244,6 +246,7 @@ impl TestServer { app_state, username: name.to_string(), channel_store: cx.read(ChannelStore::global).clone(), + notification_store: cx.read(NotificationStore::global).clone(), state: Default::default(), }; client.wait_for_current_user(cx).await; @@ -449,6 +452,10 @@ impl TestClient { &self.channel_store } + pub fn notification_store(&self) -> &ModelHandle { + &self.notification_store + } + pub fn user_store(&self) -> &ModelHandle { &self.app_state.user_store } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 3f1bafb10e..30242d6360 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -386,7 +386,8 @@ impl NotificationPanel { ) { match event { NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx), - NotificationEvent::NotificationRemoved { entry } => self.remove_toast(entry, cx), + NotificationEvent::NotificationRemoved { entry } + | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry, cx), NotificationEvent::NotificationsUpdated { old_range, new_count, @@ -450,25 +451,9 @@ impl NotificationPanel { response: bool, cx: &mut ViewContext, ) { - match notification { - Notification::ContactRequest { - sender_id: actor_id, - } => { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(actor_id, response, cx) - }) - .detach(); - } - Notification::ChannelInvitation { channel_id, .. } => { - self.channel_store - .update(cx, |store, cx| { - store.respond_to_channel_invite(channel_id, response, cx) - }) - .detach(); - } - _ => {} - } + self.notification_store.update(cx, |store, cx| { + store.respond_to_notification(notification, response, cx); + }); } } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 43afb8181a..5a1ed2677e 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -36,6 +36,9 @@ pub enum NotificationEvent { NotificationRemoved { entry: NotificationEntry, }, + NotificationRead { + entry: NotificationEntry, + }, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -272,7 +275,13 @@ impl NotificationStore { if let Some(existing_notification) = cursor.item() { if existing_notification.id == id { - if new_notification.is_none() { + if let Some(new_notification) = &new_notification { + if new_notification.is_read { + cx.emit(NotificationEvent::NotificationRead { + entry: new_notification.clone(), + }); + } + } else { cx.emit(NotificationEvent::NotificationRemoved { entry: existing_notification.clone(), }); @@ -303,6 +312,31 @@ impl NotificationStore { new_count, }); } + + pub fn respond_to_notification( + &mut self, + notification: Notification, + response: bool, + cx: &mut ModelContext, + ) { + match notification { + Notification::ContactRequest { sender_id } => { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(sender_id, response, cx) + }) + .detach(); + } + Notification::ChannelInvitation { channel_id, .. } => { + self.channel_store + .update(cx, |store, cx| { + store.respond_to_channel_invite(channel_id, response, cx) + }) + .detach(); + } + _ => {} + } + } } impl Entity for NotificationStore { From 33296802fb0424863ad3a956b7b70b76afaa23f3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 17 Oct 2023 12:11:39 +0300 Subject: [PATCH 108/274] 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 109/274] 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 110/274] 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 111/274] 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 112/274] 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 113/274] 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 114/274] 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 115/274] 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 116/274] 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 a874a09b7e3b30696dad650bc997342fd8a53a61 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 17 Oct 2023 16:21:03 -0400 Subject: [PATCH 117/274] added openai language model tokenizer and LanguageModel trait --- crates/ai/src/ai.rs | 1 + crates/ai/src/models.rs | 49 ++++++++++++++++++++++++++ crates/ai/src/templates/base.rs | 54 ++++++++++++----------------- crates/ai/src/templates/preamble.rs | 42 +++++++++++++++------- 4 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 crates/ai/src/models.rs diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 04e9e14536..f168c15793 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,3 +1,4 @@ pub mod completion; pub mod embedding; +pub mod models; pub mod templates; diff --git a/crates/ai/src/models.rs b/crates/ai/src/models.rs new file mode 100644 index 0000000000..4fe96d44f3 --- /dev/null +++ b/crates/ai/src/models.rs @@ -0,0 +1,49 @@ +use anyhow::anyhow; +use tiktoken_rs::CoreBPE; +use util::ResultExt; + +pub trait LanguageModel { + fn name(&self) -> String; + fn count_tokens(&self, content: &str) -> anyhow::Result; + fn truncate(&self, content: &str, length: usize) -> anyhow::Result; + fn capacity(&self) -> anyhow::Result; +} + +struct OpenAILanguageModel { + name: String, + bpe: Option, +} + +impl OpenAILanguageModel { + pub fn load(model_name: String) -> Self { + let bpe = tiktoken_rs::get_bpe_from_model(&model_name).log_err(); + OpenAILanguageModel { + name: model_name, + bpe, + } + } +} + +impl LanguageModel for OpenAILanguageModel { + fn name(&self) -> String { + self.name.clone() + } + fn count_tokens(&self, content: &str) -> anyhow::Result { + if let Some(bpe) = &self.bpe { + anyhow::Ok(bpe.encode_with_special_tokens(content).len()) + } else { + Err(anyhow!("bpe for open ai model was not retrieved")) + } + } + fn truncate(&self, content: &str, length: usize) -> anyhow::Result { + if let Some(bpe) = &self.bpe { + let tokens = bpe.encode_with_special_tokens(content); + bpe.decode(tokens[..length].to_vec()) + } else { + Err(anyhow!("bpe for open ai model was not retrieved")) + } + } + fn capacity(&self) -> anyhow::Result { + anyhow::Ok(tiktoken_rs::model::get_context_size(&self.name)) + } +} diff --git a/crates/ai/src/templates/base.rs b/crates/ai/src/templates/base.rs index 74a4c424ae..b5f9da3586 100644 --- a/crates/ai/src/templates/base.rs +++ b/crates/ai/src/templates/base.rs @@ -1,17 +1,11 @@ -use std::fmt::Write; -use std::{cmp::Reverse, sync::Arc}; +use std::cmp::Reverse; +use std::sync::Arc; use util::ResultExt; +use crate::models::LanguageModel; use crate::templates::repository_context::PromptCodeSnippet; -pub trait LanguageModel { - fn name(&self) -> String; - fn count_tokens(&self, content: &str) -> usize; - fn truncate(&self, content: &str, length: usize) -> String; - fn capacity(&self) -> usize; -} - pub(crate) enum PromptFileType { Text, Code, @@ -73,7 +67,7 @@ impl PromptChain { pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> { // Argsort based on Prompt Priority let seperator = "\n"; - let seperator_tokens = self.args.model.count_tokens(seperator); + let seperator_tokens = self.args.model.count_tokens(seperator)?; let mut sorted_indices = (0..self.templates.len()).collect::>(); sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); @@ -81,7 +75,7 @@ impl PromptChain { // If Truncate let mut tokens_outstanding = if truncate { - Some(self.args.model.capacity() - self.args.reserved_tokens) + Some(self.args.model.capacity()? - self.args.reserved_tokens) } else { None }; @@ -111,7 +105,7 @@ impl PromptChain { } let full_prompt = prompts.join(seperator); - let total_token_count = self.args.model.count_tokens(&full_prompt); + let total_token_count = self.args.model.count_tokens(&full_prompt)?; anyhow::Ok((prompts.join(seperator), total_token_count)) } } @@ -131,10 +125,10 @@ pub(crate) mod tests { ) -> anyhow::Result<(String, usize)> { let mut content = "This is a test prompt template".to_string(); - let mut token_count = args.model.count_tokens(&content); + let mut token_count = args.model.count_tokens(&content)?; if let Some(max_token_length) = max_token_length { if token_count > max_token_length { - content = args.model.truncate(&content, max_token_length); + content = args.model.truncate(&content, max_token_length)?; token_count = max_token_length; } } @@ -152,10 +146,10 @@ pub(crate) mod tests { ) -> anyhow::Result<(String, usize)> { let mut content = "This is a low priority test prompt template".to_string(); - let mut token_count = args.model.count_tokens(&content); + let mut token_count = args.model.count_tokens(&content)?; if let Some(max_token_length) = max_token_length { if token_count > max_token_length { - content = args.model.truncate(&content, max_token_length); + content = args.model.truncate(&content, max_token_length)?; token_count = max_token_length; } } @@ -169,26 +163,22 @@ pub(crate) mod tests { capacity: usize, } - impl DummyLanguageModel { - fn set_capacity(&mut self, capacity: usize) { - self.capacity = capacity - } - } - impl LanguageModel for DummyLanguageModel { fn name(&self) -> String { "dummy".to_string() } - fn count_tokens(&self, content: &str) -> usize { - content.chars().collect::>().len() + fn count_tokens(&self, content: &str) -> anyhow::Result { + anyhow::Ok(content.chars().collect::>().len()) } - fn truncate(&self, content: &str, length: usize) -> String { - content.chars().collect::>()[..length] - .into_iter() - .collect::() + fn truncate(&self, content: &str, length: usize) -> anyhow::Result { + anyhow::Ok( + content.chars().collect::>()[..length] + .into_iter() + .collect::(), + ) } - fn capacity(&self) -> usize { - self.capacity + fn capacity(&self) -> anyhow::Result { + anyhow::Ok(self.capacity) } } @@ -215,7 +205,7 @@ pub(crate) mod tests { .to_string() ); - assert_eq!(model.count_tokens(&prompt), token_count); + assert_eq!(model.count_tokens(&prompt).unwrap(), token_count); // Testing with Truncation Off // Should ignore capacity and return all prompts @@ -242,7 +232,7 @@ pub(crate) mod tests { .to_string() ); - assert_eq!(model.count_tokens(&prompt), token_count); + assert_eq!(model.count_tokens(&prompt).unwrap(), token_count); // Testing with Truncation Off // Should ignore capacity and return all prompts diff --git a/crates/ai/src/templates/preamble.rs b/crates/ai/src/templates/preamble.rs index b1d33f885e..f395dbf8be 100644 --- a/crates/ai/src/templates/preamble.rs +++ b/crates/ai/src/templates/preamble.rs @@ -4,31 +4,49 @@ use std::fmt::Write; struct EngineerPreamble {} impl PromptTemplate for EngineerPreamble { - fn generate(&self, args: &PromptArguments, max_token_length: Option) -> String { - let mut prompt = String::new(); + fn generate( + &self, + args: &PromptArguments, + max_token_length: Option, + ) -> anyhow::Result<(String, usize)> { + let mut prompts = Vec::new(); match args.get_file_type() { PromptFileType::Code => { - writeln!( - prompt, + prompts.push(format!( "You are an expert {} engineer.", args.language_name.clone().unwrap_or("".to_string()) - ) - .unwrap(); + )); } PromptFileType::Text => { - writeln!(prompt, "You are an expert engineer.").unwrap(); + prompts.push("You are an expert engineer.".to_string()); } } if let Some(project_name) = args.project_name.clone() { - writeln!( - prompt, + prompts.push(format!( "You are currently working inside the '{project_name}' in Zed the code editor." - ) - .unwrap(); + )); } - prompt + if let Some(mut remaining_tokens) = max_token_length { + let mut prompt = String::new(); + let mut total_count = 0; + for prompt_piece in prompts { + let prompt_token_count = + args.model.count_tokens(&prompt_piece)? + args.model.count_tokens("\n")?; + if remaining_tokens > prompt_token_count { + writeln!(prompt, "{prompt_piece}").unwrap(); + remaining_tokens -= prompt_token_count; + total_count += prompt_token_count; + } + } + + anyhow::Ok((prompt, total_count)) + } else { + let prompt = prompts.join("\n"); + let token_count = args.model.count_tokens(&prompt)?; + anyhow::Ok((prompt, token_count)) + } } } From 02853bbd606dc87a638bd2ca01a5232203069499 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 17 Oct 2023 17:29:07 -0400 Subject: [PATCH 118/274] added prompt template for repository context --- crates/ai/src/models.rs | 8 +- crates/ai/src/prompts.rs | 149 ------------------ crates/ai/src/templates/preamble.rs | 6 +- crates/ai/src/templates/repository_context.rs | 47 +++++- crates/assistant/src/assistant_panel.rs | 22 ++- crates/assistant/src/prompts.rs | 87 ++++------ 6 files changed, 96 insertions(+), 223 deletions(-) delete mode 100644 crates/ai/src/prompts.rs diff --git a/crates/ai/src/models.rs b/crates/ai/src/models.rs index 4fe96d44f3..69e73e9b56 100644 --- a/crates/ai/src/models.rs +++ b/crates/ai/src/models.rs @@ -9,16 +9,16 @@ pub trait LanguageModel { fn capacity(&self) -> anyhow::Result; } -struct OpenAILanguageModel { +pub struct OpenAILanguageModel { name: String, bpe: Option, } impl OpenAILanguageModel { - pub fn load(model_name: String) -> Self { - let bpe = tiktoken_rs::get_bpe_from_model(&model_name).log_err(); + pub fn load(model_name: &str) -> Self { + let bpe = tiktoken_rs::get_bpe_from_model(model_name).log_err(); OpenAILanguageModel { - name: model_name, + name: model_name.to_string(), bpe, } } diff --git a/crates/ai/src/prompts.rs b/crates/ai/src/prompts.rs deleted file mode 100644 index 6d2c0629fa..0000000000 --- a/crates/ai/src/prompts.rs +++ /dev/null @@ -1,149 +0,0 @@ -use gpui::{AsyncAppContext, ModelHandle}; -use language::{Anchor, Buffer}; -use std::{fmt::Write, ops::Range, path::PathBuf}; - -pub struct PromptCodeSnippet { - path: Option, - language_name: Option, - content: String, -} - -impl PromptCodeSnippet { - pub fn new(buffer: ModelHandle, range: Range, cx: &AsyncAppContext) -> Self { - let (content, language_name, file_path) = buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let content = snapshot.text_for_range(range.clone()).collect::(); - - let language_name = buffer - .language() - .and_then(|language| Some(language.name().to_string())); - - let file_path = buffer - .file() - .and_then(|file| Some(file.path().to_path_buf())); - - (content, language_name, file_path) - }); - - PromptCodeSnippet { - path: file_path, - language_name, - content, - } - } -} - -impl ToString for PromptCodeSnippet { - fn to_string(&self) -> String { - let path = self - .path - .as_ref() - .and_then(|path| Some(path.to_string_lossy().to_string())) - .unwrap_or("".to_string()); - let language_name = self.language_name.clone().unwrap_or("".to_string()); - let content = self.content.clone(); - - format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") - } -} - -enum PromptFileType { - Text, - Code, -} - -#[derive(Default)] -struct PromptArguments { - pub language_name: Option, - pub project_name: Option, - pub snippets: Vec, - pub model_name: String, -} - -impl PromptArguments { - pub fn get_file_type(&self) -> PromptFileType { - if self - .language_name - .as_ref() - .and_then(|name| Some(!["Markdown", "Plain Text"].contains(&name.as_str()))) - .unwrap_or(true) - { - PromptFileType::Code - } else { - PromptFileType::Text - } - } -} - -trait PromptTemplate { - fn generate(args: PromptArguments, max_token_length: Option) -> String; -} - -struct EngineerPreamble {} - -impl PromptTemplate for EngineerPreamble { - fn generate(args: PromptArguments, max_token_length: Option) -> String { - let mut prompt = String::new(); - - match args.get_file_type() { - PromptFileType::Code => { - writeln!( - prompt, - "You are an expert {} engineer.", - args.language_name.unwrap_or("".to_string()) - ) - .unwrap(); - } - PromptFileType::Text => { - writeln!(prompt, "You are an expert engineer.").unwrap(); - } - } - - if let Some(project_name) = args.project_name { - writeln!( - prompt, - "You are currently working inside the '{project_name}' in Zed the code editor." - ) - .unwrap(); - } - - prompt - } -} - -struct RepositorySnippets {} - -impl PromptTemplate for RepositorySnippets { - fn generate(args: PromptArguments, max_token_length: Option) -> String { - const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; - let mut template = "You are working inside a large repository, here are a few code snippets that may be useful"; - let mut prompt = String::new(); - - if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(args.model_name.as_str()) { - let default_token_count = - tiktoken_rs::model::get_context_size(args.model_name.as_str()); - let mut remaining_token_count = max_token_length.unwrap_or(default_token_count); - - for snippet in args.snippets { - let mut snippet_prompt = template.to_string(); - let content = snippet.to_string(); - writeln!(snippet_prompt, "{content}").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 { - writeln!(prompt, "{snippet_prompt}").unwrap(); - remaining_token_count -= token_count; - template = ""; - } - } else { - break; - } - } - } - - prompt - } -} diff --git a/crates/ai/src/templates/preamble.rs b/crates/ai/src/templates/preamble.rs index f395dbf8be..5834fa1b21 100644 --- a/crates/ai/src/templates/preamble.rs +++ b/crates/ai/src/templates/preamble.rs @@ -1,7 +1,7 @@ use crate::templates::base::{PromptArguments, PromptFileType, PromptTemplate}; use std::fmt::Write; -struct EngineerPreamble {} +pub struct EngineerPreamble {} impl PromptTemplate for EngineerPreamble { fn generate( @@ -14,8 +14,8 @@ impl PromptTemplate for EngineerPreamble { match args.get_file_type() { PromptFileType::Code => { prompts.push(format!( - "You are an expert {} engineer.", - args.language_name.clone().unwrap_or("".to_string()) + "You are an expert {}engineer.", + args.language_name.clone().unwrap_or("".to_string()) + " " )); } PromptFileType::Text => { diff --git a/crates/ai/src/templates/repository_context.rs b/crates/ai/src/templates/repository_context.rs index f9c2253c65..7dd1647c44 100644 --- a/crates/ai/src/templates/repository_context.rs +++ b/crates/ai/src/templates/repository_context.rs @@ -1,8 +1,11 @@ +use crate::templates::base::{PromptArguments, PromptTemplate}; +use std::fmt::Write; use std::{ops::Range, path::PathBuf}; use gpui::{AsyncAppContext, ModelHandle}; use language::{Anchor, Buffer}; +#[derive(Clone)] pub struct PromptCodeSnippet { path: Option, language_name: Option, @@ -17,7 +20,7 @@ impl PromptCodeSnippet { let language_name = buffer .language() - .and_then(|language| Some(language.name().to_string())); + .and_then(|language| Some(language.name().to_string().to_lowercase())); let file_path = buffer .file() @@ -47,3 +50,45 @@ impl ToString for PromptCodeSnippet { format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") } } + +pub struct RepositoryContext {} + +impl PromptTemplate for RepositoryContext { + fn generate( + &self, + args: &PromptArguments, + max_token_length: Option, + ) -> anyhow::Result<(String, usize)> { + const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; + let mut template = "You are working inside a large repository, here are a few code snippets that may be useful."; + let mut prompt = String::new(); + + let mut remaining_tokens = max_token_length.clone(); + let seperator_token_length = args.model.count_tokens("\n")?; + for snippet in &args.snippets { + let mut snippet_prompt = template.to_string(); + let content = snippet.to_string(); + writeln!(snippet_prompt, "{content}").unwrap(); + + let token_count = args.model.count_tokens(&snippet_prompt)?; + if token_count <= MAXIMUM_SNIPPET_TOKEN_COUNT { + if let Some(tokens_left) = remaining_tokens { + if tokens_left >= token_count { + writeln!(prompt, "{snippet_prompt}").unwrap(); + remaining_tokens = if tokens_left >= (token_count + seperator_token_length) + { + Some(tokens_left - token_count - seperator_token_length) + } else { + Some(0) + }; + } + } else { + writeln!(prompt, "{snippet_prompt}").unwrap(); + } + } + } + + let total_token_count = args.model.count_tokens(&prompt)?; + anyhow::Ok((prompt, total_token_count)) + } +} diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e8edf70498..06de5c135f 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,12 +1,15 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, - prompts::{generate_content_prompt, PromptCodeSnippet}, + prompts::generate_content_prompt, MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; -use ai::completion::{ - stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL, +use ai::{ + completion::{ + stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL, + }, + templates::repository_context::PromptCodeSnippet, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; @@ -668,14 +671,7 @@ impl AssistantPanel { let snippets = cx.spawn(|_, cx| async move { let mut snippets = Vec::new(); for result in search_results.await { - snippets.push(PromptCodeSnippet::new(result, &cx)); - - // snippets.push(result.buffer.read_with(&cx, |buffer, _| { - // buffer - // .snapshot() - // .text_for_range(result.range) - // .collect::() - // })); + snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx)); } snippets }); @@ -717,7 +713,8 @@ impl AssistantPanel { } cx.spawn(|_, mut cx| async move { - let prompt = prompt.await; + // I Don't know if we want to return a ? here. + let prompt = prompt.await?; messages.push(RequestMessage { role: Role::User, @@ -729,6 +726,7 @@ impl AssistantPanel { stream: true, }; codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + anyhow::Ok(()) }) .detach(); } diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 7aafe75920..e33a6e4022 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,61 +1,15 @@ use crate::codegen::CodegenKind; -use gpui::AsyncAppContext; +use ai::models::{LanguageModel, OpenAILanguageModel}; +use ai::templates::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate}; +use ai::templates::preamble::EngineerPreamble; +use ai::templates::repository_context::{PromptCodeSnippet, RepositoryContext}; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; -use semantic_index::SearchResult; 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 { - path: Option, - language_name: Option, - content: String, -} - -impl PromptCodeSnippet { - pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self { - let (content, language_name, file_path) = - search_result.buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let content = snapshot - .text_for_range(search_result.range.clone()) - .collect::(); - - let language_name = buffer - .language() - .and_then(|language| Some(language.name().to_string())); - - let file_path = buffer - .file() - .and_then(|file| Some(file.path().to_path_buf())); - - (content, language_name, file_path) - }); - - PromptCodeSnippet { - path: file_path, - language_name, - content, - } - } -} - -impl ToString for PromptCodeSnippet { - fn to_string(&self) -> String { - let path = self - .path - .as_ref() - .and_then(|path| Some(path.to_string_lossy().to_string())) - .unwrap_or("".to_string()); - let language_name = self.language_name.clone().unwrap_or("".to_string()); - let content = self.content.clone(); - - format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```") - } -} - #[allow(dead_code)] fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] @@ -175,7 +129,32 @@ pub fn generate_content_prompt( kind: CodegenKind, search_results: Vec, model: &str, -) -> String { +) -> anyhow::Result { + // Using new Prompt Templates + let openai_model: Arc = Arc::new(OpenAILanguageModel::load(model)); + let lang_name = if let Some(language_name) = language_name { + Some(language_name.to_string()) + } else { + None + }; + + let args = PromptArguments { + model: openai_model, + language_name: lang_name.clone(), + project_name: None, + snippets: search_results.clone(), + reserved_tokens: 1000, + }; + + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::High, Box::new(EngineerPreamble {})), + (PromptPriority::Low, Box::new(RepositoryContext {})), + ]; + let chain = PromptChain::new(args, templates); + + let prompt = chain.generate(true)?; + println!("{:?}", prompt); + const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500; const RESERVED_TOKENS_FOR_GENERATION: usize = 1000; @@ -183,7 +162,7 @@ pub fn generate_content_prompt( let range = range.to_offset(buffer); // General Preamble - if let Some(language_name) = language_name { + if let Some(language_name) = language_name.clone() { prompts.push(format!("You're an expert {language_name} engineer.\n")); } else { prompts.push("You're an expert engineer.\n".to_string()); @@ -297,7 +276,7 @@ pub fn generate_content_prompt( } } - prompts.join("\n") + anyhow::Ok(prompts.join("\n")) } #[cfg(test)] From 783f05172b5361d96a1fff0c73dc0fa94b243ab2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Oct 2023 15:40:23 -0600 Subject: [PATCH 119/274] Make sure guests join as guests --- crates/collab/src/db/queries/channels.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index ee989b2ea0..d64d97f2ad 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -135,8 +135,7 @@ impl Database { .most_public_ancestor_for_channel(channel_id, &*tx) .await? .unwrap_or(channel_id); - // TODO: change this back to Guest. - role = Some(ChannelRole::Member); + role = Some(ChannelRole::Guest); joined_channel_id = Some(channel_id_to_join); channel_member::Entity::insert(channel_member::ActiveModel { @@ -144,8 +143,7 @@ impl Database { channel_id: ActiveValue::Set(channel_id_to_join), user_id: ActiveValue::Set(user_id), accepted: ActiveValue::Set(true), - // TODO: change this back to Guest. - role: ActiveValue::Set(ChannelRole::Member), + role: ActiveValue::Set(ChannelRole::Guest), }) .exec(&*tx) .await?; From 660021f5e5600b4808d3d11ae1ca985ae0ff57cb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 17 Oct 2023 15:43:06 -0700 Subject: [PATCH 120/274] Fix more issues with the channels panel * Put the newest notifications at the top * Have at most 1 notification toast, which is non-interactive, but focuses the notification panel on click, and auto-dismisses on a timer. --- crates/channel/src/channel_store.rs | 6 + crates/collab/src/db/queries/channels.rs | 16 +- crates/collab/src/rpc.rs | 16 +- crates/collab/src/tests/notification_tests.rs | 50 ++- crates/collab_ui/src/notification_panel.rs | 326 ++++++++++++------ crates/collab_ui/src/notifications.rs | 111 +----- .../src/notifications/contact_notification.rs | 106 ------ .../notifications/src/notification_store.rs | 33 +- crates/rpc/src/notification.rs | 6 +- 9 files changed, 328 insertions(+), 342 deletions(-) delete mode 100644 crates/collab_ui/src/notifications/contact_notification.rs diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index d8dc7896ea..ae8a797d06 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -213,6 +213,12 @@ impl ChannelStore { self.channel_index.by_id().values().nth(ix) } + pub fn has_channel_invitation(&self, channel_id: ChannelId) -> bool { + self.channel_invitations + .iter() + .any(|channel| channel.id == channel_id) + } + pub fn channel_invitations(&self) -> &[Arc] { &self.channel_invitations } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 745bd6e3ab..d2499ab3ce 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -187,6 +187,7 @@ impl Database { rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), channel_name: channel.name, + inviter_id: inviter_id.to_proto(), }, true, &*tx, @@ -276,6 +277,7 @@ impl Database { &rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), channel_name: Default::default(), + inviter_id: Default::default(), }, accept, &*tx, @@ -292,7 +294,7 @@ impl Database { channel_id: ChannelId, member_id: UserId, remover_id: UserId, - ) -> Result<()> { + ) -> Result> { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel_id, remover_id, &*tx) .await?; @@ -310,7 +312,17 @@ impl Database { Err(anyhow!("no such member"))?; } - Ok(()) + Ok(self + .remove_notification( + member_id, + rpc::Notification::ChannelInvitation { + channel_id: channel_id.to_proto(), + channel_name: Default::default(), + inviter_id: Default::default(), + }, + &*tx, + ) + .await?) }) .await } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9f3c22ce97..053058e06e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2331,7 +2331,8 @@ async fn remove_channel_member( let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - db.remove_channel_member(channel_id, member_id, session.user_id) + let removed_notification_id = db + .remove_channel_member(channel_id, member_id, session.user_id) .await?; let mut update = proto::UpdateChannels::default(); @@ -2342,7 +2343,18 @@ async fn remove_channel_member( .await .user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; + session.peer.send(connection_id, update.clone()).trace_err(); + if let Some(notification_id) = removed_notification_id { + session + .peer + .send( + connection_id, + proto::DeleteNotification { + notification_id: notification_id.to_proto(), + }, + ) + .trace_err(); + } } response.send(proto::Ack {})?; diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs index da94bd6fad..518208c0c7 100644 --- a/crates/collab/src/tests/notification_tests.rs +++ b/crates/collab/src/tests/notification_tests.rs @@ -1,5 +1,7 @@ use crate::tests::TestServer; use gpui::{executor::Deterministic, TestAppContext}; +use notifications::NotificationEvent; +use parking_lot::Mutex; use rpc::Notification; use std::sync::Arc; @@ -14,6 +16,23 @@ async fn test_notifications( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let notification_events_a = Arc::new(Mutex::new(Vec::new())); + let notification_events_b = Arc::new(Mutex::new(Vec::new())); + client_a.notification_store().update(cx_a, |_, cx| { + let events = notification_events_a.clone(); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + events.lock().push(event.clone()); + }) + .detach() + }); + client_b.notification_store().update(cx_b, |_, cx| { + let events = notification_events_b.clone(); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + events.lock().push(event.clone()); + }) + .detach() + }); + // Client A sends a contact request to client B. client_a .user_store() @@ -36,6 +55,18 @@ async fn test_notifications( } ); assert!(!entry.is_read); + assert_eq!( + ¬ification_events_b.lock()[0..], + &[ + NotificationEvent::NewNotification { + entry: entry.clone(), + }, + NotificationEvent::NotificationsUpdated { + old_range: 0..0, + new_count: 1 + } + ] + ); store.respond_to_notification(entry.notification.clone(), true, cx); }); @@ -49,6 +80,18 @@ async fn test_notifications( let entry = store.notification_at(0).unwrap(); assert!(entry.is_read); assert_eq!(entry.response, Some(true)); + assert_eq!( + ¬ification_events_b.lock()[2..], + &[ + NotificationEvent::NotificationRead { + entry: entry.clone(), + }, + NotificationEvent::NotificationsUpdated { + old_range: 0..1, + new_count: 1 + } + ] + ); }); // Client A receives a notification that client B accepted their request. @@ -89,12 +132,13 @@ async fn test_notifications( assert_eq!(store.notification_count(), 2); assert_eq!(store.unread_notification_count(), 1); - let entry = store.notification_at(1).unwrap(); + let entry = store.notification_at(0).unwrap(); assert_eq!( entry.notification, Notification::ChannelInvitation { channel_id, - channel_name: "the-channel".to_string() + channel_name: "the-channel".to_string(), + inviter_id: client_a.id() } ); assert!(!entry.is_read); @@ -108,7 +152,7 @@ async fn test_notifications( assert_eq!(store.notification_count(), 2); assert_eq!(store.unread_notification_count(), 0); - let entry = store.notification_at(1).unwrap(); + let entry = store.notification_at(0).unwrap(); assert!(entry.is_read); assert_eq!(entry.response, Some(true)); }); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 30242d6360..93ba05a671 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -1,11 +1,9 @@ use crate::{ - format_timestamp, is_channels_feature_enabled, - notifications::contact_notification::ContactNotification, render_avatar, - NotificationPanelSettings, + format_timestamp, is_channels_feature_enabled, render_avatar, NotificationPanelSettings, }; use anyhow::Result; use channel::ChannelStore; -use client::{Client, Notification, UserStore}; +use client::{Client, Notification, User, UserStore}; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ @@ -19,7 +17,7 @@ use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use theme::{IconButton, Theme}; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; @@ -28,6 +26,7 @@ use workspace::{ Workspace, }; +const TOAST_DURATION: Duration = Duration::from_secs(5); const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel"; pub struct NotificationPanel { @@ -42,6 +41,7 @@ pub struct NotificationPanel { pending_serialization: Task>, subscriptions: Vec, workspace: WeakViewHandle, + current_notification_toast: Option<(u64, Task<()>)>, local_timezone: UtcOffset, has_focus: bool, } @@ -58,7 +58,7 @@ pub enum Event { Dismissed, } -actions!(chat_panel, [ToggleFocus]); +actions!(notification_panel, [ToggleFocus]); pub fn init(_cx: &mut AppContext) {} @@ -69,14 +69,8 @@ impl NotificationPanel { let user_store = workspace.app_state().user_store.clone(); let workspace_handle = workspace.weak_handle(); - let notification_list = - ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { - this.render_notification(ix, cx) - }); - cx.add_view(|cx| { let mut status = client.status(); - cx.spawn(|this, mut cx| async move { while let Some(_) = status.next().await { if this @@ -91,6 +85,12 @@ impl NotificationPanel { }) .detach(); + let notification_list = + ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { + this.render_notification(ix, cx) + .unwrap_or_else(|| Empty::new().into_any()) + }); + let mut this = Self { fs, client, @@ -102,6 +102,7 @@ impl NotificationPanel { pending_serialization: Task::ready(None), workspace: workspace_handle, has_focus: false, + current_notification_toast: None, subscriptions: Vec::new(), active: false, width: None, @@ -169,73 +170,20 @@ impl NotificationPanel { ); } - fn render_notification(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - self.try_render_notification(ix, cx) - .unwrap_or_else(|| Empty::new().into_any()) - } - - fn try_render_notification( + fn render_notification( &mut self, ix: usize, cx: &mut ViewContext, ) -> Option> { - let notification_store = self.notification_store.read(cx); - let user_store = self.user_store.read(cx); - let channel_store = self.channel_store.read(cx); - let entry = notification_store.notification_at(ix)?; - let notification = entry.notification.clone(); + let entry = self.notification_store.read(cx).notification_at(ix)?; let now = OffsetDateTime::now_utc(); let timestamp = entry.timestamp; - - let icon; - let text; - let actor; - let needs_acceptance; - match notification { - Notification::ContactRequest { sender_id } => { - let requester = user_store.get_cached_user(sender_id)?; - icon = "icons/plus.svg"; - text = format!("{} wants to add you as a contact", requester.github_login); - needs_acceptance = true; - actor = Some(requester); - } - Notification::ContactRequestAccepted { responder_id } => { - let responder = user_store.get_cached_user(responder_id)?; - icon = "icons/plus.svg"; - text = format!("{} accepted your contact invite", responder.github_login); - needs_acceptance = false; - actor = Some(responder); - } - Notification::ChannelInvitation { - ref channel_name, .. - } => { - actor = None; - icon = "icons/hash.svg"; - text = format!("you were invited to join the #{channel_name} channel"); - needs_acceptance = true; - } - Notification::ChannelMessageMention { - sender_id, - channel_id, - message_id, - } => { - let sender = user_store.get_cached_user(sender_id)?; - let channel = channel_store.channel_for_id(channel_id)?; - let message = notification_store.channel_message_for_id(message_id)?; - - icon = "icons/conversations.svg"; - text = format!( - "{} mentioned you in the #{} channel:\n{}", - sender.github_login, channel.name, message.body, - ); - needs_acceptance = false; - actor = Some(sender); - } - } + let (actor, text, icon, needs_response) = self.present_notification(entry, cx)?; let theme = theme::current(cx); let style = &theme.notification_panel; let response = entry.response; + let notification = entry.notification.clone(); let message_style = if entry.is_read { style.read_text.clone() @@ -276,7 +224,7 @@ impl NotificationPanel { ) .into_any(), ) - } else if needs_acceptance { + } else if needs_response { Some( Flex::row() .with_children([ @@ -336,6 +284,69 @@ impl NotificationPanel { ) } + fn present_notification( + &self, + entry: &NotificationEntry, + cx: &AppContext, + ) -> Option<(Option>, String, &'static str, bool)> { + let user_store = self.user_store.read(cx); + let channel_store = self.channel_store.read(cx); + let icon; + let text; + let actor; + let needs_response; + match entry.notification { + Notification::ContactRequest { sender_id } => { + let requester = user_store.get_cached_user(sender_id)?; + icon = "icons/plus.svg"; + text = format!("{} wants to add you as a contact", requester.github_login); + needs_response = user_store.is_contact_request_pending(&requester); + actor = Some(requester); + } + Notification::ContactRequestAccepted { responder_id } => { + let responder = user_store.get_cached_user(responder_id)?; + icon = "icons/plus.svg"; + text = format!("{} accepted your contact invite", responder.github_login); + needs_response = false; + actor = Some(responder); + } + Notification::ChannelInvitation { + ref channel_name, + channel_id, + inviter_id, + } => { + let inviter = user_store.get_cached_user(inviter_id)?; + icon = "icons/hash.svg"; + text = format!( + "{} invited you to join the #{channel_name} channel", + inviter.github_login + ); + needs_response = channel_store.has_channel_invitation(channel_id); + actor = Some(inviter); + } + Notification::ChannelMessageMention { + sender_id, + channel_id, + message_id, + } => { + let sender = user_store.get_cached_user(sender_id)?; + let channel = channel_store.channel_for_id(channel_id)?; + let message = self + .notification_store + .read(cx) + .channel_message_for_id(message_id)?; + icon = "icons/conversations.svg"; + text = format!( + "{} mentioned you in the #{} channel:\n{}", + sender.github_login, channel.name, message.body, + ); + needs_response = false; + actor = Some(sender); + } + } + Some((actor, text, icon, needs_response)) + } + fn render_sign_in_prompt( &self, theme: &Arc, @@ -387,7 +398,7 @@ impl NotificationPanel { match event { NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx), NotificationEvent::NotificationRemoved { entry } - | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry, cx), + | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx), NotificationEvent::NotificationsUpdated { old_range, new_count, @@ -399,49 +410,44 @@ impl NotificationPanel { } fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { - let id = entry.id as usize; - match entry.notification { - Notification::ContactRequest { - sender_id: actor_id, - } - | Notification::ContactRequestAccepted { - responder_id: actor_id, - } => { - let user_store = self.user_store.clone(); - let Some(user) = user_store.read(cx).get_cached_user(actor_id) else { - return; - }; - self.workspace - .update(cx, |workspace, cx| { - workspace.show_notification(id, cx, |cx| { - cx.add_view(|_| { - ContactNotification::new( - user, - entry.notification.clone(), - user_store, - ) - }) - }) - }) + let Some((actor, text, _, _)) = self.present_notification(entry, cx) else { + return; + }; + + let id = entry.id; + self.current_notification_toast = Some(( + id, + cx.spawn(|this, mut cx| async move { + cx.background().timer(TOAST_DURATION).await; + this.update(&mut cx, |this, cx| this.remove_toast(id, cx)) .ok(); - } - Notification::ChannelInvitation { .. } => {} - Notification::ChannelMessageMention { .. } => {} - } + }), + )); + + self.workspace + .update(cx, |workspace, cx| { + workspace.show_notification(0, cx, |cx| { + let workspace = cx.weak_handle(); + cx.add_view(|_| NotificationToast { + actor, + text, + workspace, + }) + }) + }) + .ok(); } - fn remove_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { - let id = entry.id as usize; - match entry.notification { - Notification::ContactRequest { .. } | Notification::ContactRequestAccepted { .. } => { + fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext) { + if let Some((current_id, _)) = &self.current_notification_toast { + if *current_id == notification_id { + self.current_notification_toast.take(); self.workspace .update(cx, |workspace, cx| { - workspace.dismiss_notification::(id, cx) + workspace.dismiss_notification::(0, cx) }) .ok(); } - Notification::ChannelInvitation { .. } => {} - Notification::ChannelMessageMention { .. } => {} } } @@ -582,3 +588,111 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> im .contained() .with_style(style.container) } + +pub struct NotificationToast { + actor: Option>, + text: String, + workspace: WeakViewHandle, +} + +pub enum ToastEvent { + Dismiss, +} + +impl NotificationToast { + fn focus_notification_panel(&self, cx: &mut AppContext) { + let workspace = self.workspace.clone(); + cx.defer(move |cx| { + workspace + .update(cx, |workspace, cx| { + workspace.focus_panel::(cx); + }) + .ok(); + }) + } +} + +impl Entity for NotificationToast { + type Event = ToastEvent; +} + +impl View for NotificationToast { + fn ui_name() -> &'static str { + "ContactNotification" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let user = self.actor.clone(); + let theme = theme::current(cx).clone(); + let theme = &theme.contact_notification; + + MouseEventHandler::new::(0, cx, |_, cx| { + Flex::row() + .with_children(user.and_then(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(theme.header_avatar) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top(), + ) + })) + .with_child( + Text::new(self.text.clone(), theme.header_message.text.clone()) + .contained() + .with_style(theme.header_message.container) + .aligned() + .top() + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::new::(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state); + Svg::new("icons/x.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(5.)) + .on_click(MouseButton::Left, move |_, _, cx| { + cx.emit(ToastEvent::Dismiss) + }) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .flex_float(), + ) + .contained() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.focus_notification_panel(cx); + cx.emit(ToastEvent::Dismiss); + }) + .into_any() + } +} + +impl workspace::notifications::Notification for NotificationToast { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, ToastEvent::Dismiss) + } +} diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index e4456163c6..5c184ec5c8 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -1,120 +1,11 @@ -use client::User; -use gpui::{ - elements::*, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ViewContext, -}; +use gpui::AppContext; use std::sync::Arc; use workspace::AppState; -pub mod contact_notification; pub mod incoming_call_notification; pub mod project_shared_notification; -enum Dismiss {} -enum Button {} - pub fn init(app_state: &Arc, cx: &mut AppContext) { incoming_call_notification::init(app_state, cx); project_shared_notification::init(app_state, cx); } - -pub fn render_user_notification( - user: Arc, - title: &'static str, - body: Option<&'static str>, - on_dismiss: F, - buttons: Vec<(&'static str, Box)>)>, - cx: &mut ViewContext, -) -> AnyElement -where - F: 'static + Fn(&mut V, &mut ViewContext), -{ - let theme = theme::current(cx).clone(); - let theme = &theme.contact_notification; - - Flex::column() - .with_child( - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.header_avatar) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - })) - .with_child( - Text::new( - format!("{} {}", user.github_login, title), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx)) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .flex_float(), - ) - .into_any_named("contact notification header"), - ) - .with_children(body.map(|body| { - Label::new(body, theme.body_message.text.clone()) - .contained() - .with_style(theme.body_message.container) - })) - .with_children(if buttons.is_empty() { - None - } else { - Some( - Flex::row() - .with_children(buttons.into_iter().enumerate().map( - |(ix, (message, handler))| { - MouseEventHandler::new::(ix, cx, |state, _| { - let button = theme.button.style_for(state); - Label::new(message, button.text.clone()) - .contained() - .with_style(button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| handler(view, cx)) - }, - )) - .aligned() - .right(), - ) - }) - .contained() - .into_any() -} diff --git a/crates/collab_ui/src/notifications/contact_notification.rs b/crates/collab_ui/src/notifications/contact_notification.rs deleted file mode 100644 index 2e3c3ca58a..0000000000 --- a/crates/collab_ui/src/notifications/contact_notification.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::notifications::render_user_notification; -use client::{User, UserStore}; -use gpui::{elements::*, Entity, ModelHandle, View, ViewContext}; -use std::sync::Arc; -use workspace::notifications::Notification; - -pub struct ContactNotification { - user_store: ModelHandle, - user: Arc, - notification: rpc::Notification, -} - -#[derive(Clone, PartialEq)] -struct Dismiss(u64); - -#[derive(Clone, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub enum Event { - Dismiss, -} - -impl Entity for ContactNotification { - type Event = Event; -} - -impl View for ContactNotification { - fn ui_name() -> &'static str { - "ContactNotification" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - match self.notification { - rpc::Notification::ContactRequest { .. } => render_user_notification( - self.user.clone(), - "wants to add you as a contact", - Some("They won't be alerted if you decline."), - |notification, cx| notification.dismiss(cx), - vec![ - ( - "Decline", - Box::new(|notification, cx| { - notification.respond_to_contact_request(false, cx) - }), - ), - ( - "Accept", - Box::new(|notification, cx| { - notification.respond_to_contact_request(true, cx) - }), - ), - ], - cx, - ), - rpc::Notification::ContactRequestAccepted { .. } => render_user_notification( - self.user.clone(), - "accepted your contact request", - None, - |notification, cx| notification.dismiss(cx), - vec![], - cx, - ), - _ => unreachable!(), - } - } -} - -impl Notification for ContactNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) - } -} - -impl ContactNotification { - pub fn new( - user: Arc, - notification: rpc::Notification, - user_store: ModelHandle, - ) -> Self { - Self { - user, - notification, - user_store, - } - } - - fn dismiss(&mut self, cx: &mut ViewContext) { - self.user_store.update(cx, |store, cx| { - store - .dismiss_contact_request(self.user.id, cx) - .detach_and_log_err(cx); - }); - cx.emit(Event::Dismiss); - } - - fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(self.user.id, accept, cx) - }) - .detach(); - } -} diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 5a1ed2677e..0ee4ad35f1 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -25,6 +25,7 @@ pub struct NotificationStore { _subscriptions: Vec, } +#[derive(Clone, PartialEq, Eq, Debug)] pub enum NotificationEvent { NotificationsUpdated { old_range: Range, @@ -118,7 +119,13 @@ impl NotificationStore { self.channel_messages.get(&id) } + // Get the nth newest notification. pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { + let count = self.notifications.summary().count; + if ix >= count { + return None; + } + let ix = count - 1 - ix; let mut cursor = self.notifications.cursor::(); cursor.seek(&Count(ix), Bias::Right, &()); cursor.item() @@ -200,7 +207,9 @@ impl NotificationStore { for entry in ¬ifications { match entry.notification { - Notification::ChannelInvitation { .. } => {} + Notification::ChannelInvitation { inviter_id, .. } => { + user_ids.push(inviter_id); + } Notification::ContactRequest { sender_id: requester_id, } => { @@ -273,8 +282,11 @@ impl NotificationStore { old_range.start = cursor.start().1 .0; } - if let Some(existing_notification) = cursor.item() { - if existing_notification.id == id { + let old_notification = cursor.item(); + if let Some(old_notification) = old_notification { + if old_notification.id == id { + cursor.next(&()); + if let Some(new_notification) = &new_notification { if new_notification.is_read { cx.emit(NotificationEvent::NotificationRead { @@ -283,20 +295,19 @@ impl NotificationStore { } } else { cx.emit(NotificationEvent::NotificationRemoved { - entry: existing_notification.clone(), + entry: old_notification.clone(), }); } - cursor.next(&()); + } + } else if let Some(new_notification) = &new_notification { + if is_new { + cx.emit(NotificationEvent::NewNotification { + entry: new_notification.clone(), + }); } } if let Some(notification) = new_notification { - if is_new { - cx.emit(NotificationEvent::NewNotification { - entry: notification.clone(), - }); - } - new_notifications.push(notification, &()); } } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 06dff82b75..c5476469be 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -30,12 +30,13 @@ pub enum Notification { #[serde(rename = "entity_id")] channel_id: u64, channel_name: String, + inviter_id: u64, }, ChannelMessageMention { - sender_id: u64, - channel_id: u64, #[serde(rename = "entity_id")] message_id: u64, + sender_id: u64, + channel_id: u64, }, } @@ -84,6 +85,7 @@ fn test_notification() { Notification::ChannelInvitation { channel_id: 100, channel_name: "the-channel".into(), + inviter_id: 50, }, Notification::ChannelMessageMention { sender_id: 200, From ee87ac2f9b9f4ea2432b96ea63e8b3cd8b428d1a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 17 Oct 2023 17:59:42 -0700 Subject: [PATCH 121/274] Start work on chat mentions --- Cargo.lock | 1 + crates/collab/src/db/queries/channels.rs | 18 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/chat_panel.rs | 69 +++--- .../src/chat_panel/message_editor.rs | 218 ++++++++++++++++++ crates/rpc/proto/zed.proto | 7 +- crates/rpc/src/proto.rs | 2 + crates/theme/src/theme.rs | 1 + styles/src/style_tree/chat_panel.ts | 1 + 9 files changed, 271 insertions(+), 47 deletions(-) create mode 100644 crates/collab_ui/src/chat_panel/message_editor.rs diff --git a/Cargo.lock b/Cargo.lock index a2fc2bf2d8..ce517efd09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,6 +1558,7 @@ dependencies = [ "fuzzy", "gpui", "language", + "lazy_static", "log", "menu", "notifications", diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index d2499ab3ce..1ca38b2e3c 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -552,7 +552,8 @@ impl Database { user_id: UserId, ) -> Result> { self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) + let user_membership = self + .check_user_is_channel_member(channel_id, user_id, &*tx) .await?; #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] @@ -613,6 +614,14 @@ impl Database { }); } + // If the user is not an admin, don't give them all of the details + if !user_membership.admin { + rows.retain_mut(|row| { + row.admin = false; + row.kind != proto::channel_member::Kind::Invitee as i32 + }); + } + Ok(rows) }) .await @@ -644,9 +653,9 @@ impl Database { channel_id: ChannelId, user_id: UserId, tx: &DatabaseTransaction, - ) -> Result<()> { + ) -> Result { let channel_ids = self.get_channel_ancestors(channel_id, tx).await?; - channel_member::Entity::find() + Ok(channel_member::Entity::find() .filter( channel_member::Column::ChannelId .is_in(channel_ids) @@ -654,8 +663,7 @@ impl Database { ) .one(&*tx) .await? - .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?; - Ok(()) + .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?) } pub async fn check_user_is_channel_admin( diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 4a0f8c5e8b..697faace80 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -54,6 +54,7 @@ zed-actions = {path = "../zed-actions"} anyhow.workspace = true futures.workspace = true +lazy_static.workspace = true log.workspace = true schemars.workspace = true postage.workspace = true diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index d58a406d78..28bfe62109 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -18,8 +18,9 @@ use gpui::{ AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::{language_settings::SoftWrap, LanguageRegistry}; +use language::LanguageRegistry; use menu::Confirm; +use message_editor::MessageEditor; use project::Fs; use rich_text::RichText; use serde::{Deserialize, Serialize}; @@ -33,6 +34,8 @@ use workspace::{ Workspace, }; +mod message_editor; + const MESSAGE_LOADING_THRESHOLD: usize = 50; const CHAT_PANEL_KEY: &'static str = "ChatPanel"; @@ -42,7 +45,7 @@ pub struct ChatPanel { languages: Arc, active_chat: Option<(ModelHandle, Subscription)>, message_list: ListState, - input_editor: ViewHandle, + input_editor: ViewHandle, channel_select: ViewHandle) { /// Immediately invoked function expression. Good for using the ? operator /// in functions which do not return an Option or Result #[macro_export] -macro_rules! iife { +macro_rules! try { ($block:block) => { (|| $block)() }; @@ -361,7 +361,7 @@ macro_rules! iife { /// Async Immediately invoked function expression. Good for using the ? operator /// in functions which do not return an Option or Result. Async version of above #[macro_export] -macro_rules! async_iife { +macro_rules! async_try { ($block:block) => { (|| async move { $block })() }; From 6f173c64b3f1596fdf7c2f89ab7fffd149767dc0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 25 Oct 2023 09:22:06 +0200 Subject: [PATCH 217/274] Fix tests by re-instating paths in the new format --- crates/channel/src/channel_store_tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 43e0344b2c..ff8761ee91 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -53,14 +53,14 @@ fn test_update_channels(cx: &mut AppContext) { name: "x".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![1], }, proto::Channel { id: 4, name: "y".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Member.into(), - parent_path: Vec::new(), + parent_path: vec![2], }, ], ..Default::default() @@ -92,21 +92,21 @@ fn test_dangling_channel_paths(cx: &mut AppContext) { name: "a".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![], }, proto::Channel { id: 1, name: "b".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![0], }, proto::Channel { id: 2, name: "c".to_string(), visibility: proto::ChannelVisibility::Members as i32, role: proto::ChannelRole::Admin.into(), - parent_path: Vec::new(), + parent_path: vec![0, 1], }, ], ..Default::default() From 70eeefa1f899f6a0b866c6d2d2318f90cf60f79d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 25 Oct 2023 09:27:17 +0200 Subject: [PATCH 218/274] Fix channel collapsing --- crates/channel/src/channel_store.rs | 7 ------- crates/collab_ui/src/collab_panel.rs | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 14738e170b..9757bb8092 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -188,13 +188,6 @@ impl ChannelStore { self.client.clone() } - pub fn channel_has_children(&self) -> bool { - self.channel_index - .by_id() - .iter() - .any(|(_, channel)| channel.parent_path.contains(&channel.id)) - } - /// Returns the number of unique channels in the store pub fn channel_count(&self) -> usize { self.channel_index.by_id().len() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 51eab1eb3f..66962b0402 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -472,9 +472,20 @@ impl CollabPanel { cx, ) } - ListEntry::Channel { channel, depth, .. } => { - let channel_row = - this.render_channel(&*channel, *depth, &theme, is_selected, ix, cx); + ListEntry::Channel { + channel, + depth, + has_children, + } => { + let channel_row = this.render_channel( + &*channel, + *depth, + &theme, + is_selected, + *has_children, + ix, + cx, + ); if is_selected && this.context_menu_on_selected { Stack::new() @@ -1867,12 +1878,12 @@ impl CollabPanel { depth: usize, theme: &theme::Theme, is_selected: bool, + has_children: bool, ix: usize, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; let collab_theme = &theme.collab_panel; - let has_children = self.channel_store.read(cx).channel_has_children(); let is_public = self .channel_store .read(cx) From 42259a400742b510435649b26dd6a1d2333cc24a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 24 Oct 2023 17:48:18 +0200 Subject: [PATCH 219/274] Fix channel dragging Co-authored-by: Conrad Co-authored-by: Joseph --- crates/collab_ui/src/collab_panel.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 66962b0402..16f8fb5d02 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2143,7 +2143,7 @@ impl CollabPanel { .on_up(MouseButton::Left, move |_, this, cx| { if let Some((_, dragged_channel)) = cx .global::>() - .currently_dragged::>(cx.window()) + .currently_dragged::(cx.window()) { this.channel_store .update(cx, |channel_store, cx| { @@ -2155,20 +2155,18 @@ impl CollabPanel { .on_move({ let channel = channel.clone(); move |_, this, cx| { - if let Some((_, dragged_channel_id)) = cx + if let Some((_, dragged_channel)) = cx .global::>() - .currently_dragged::(cx.window()) + .currently_dragged::(cx.window()) { - if this.drag_target_channel != Some(*dragged_channel_id) { + if channel.id != dragged_channel.id { this.drag_target_channel = Some(channel.id); - } else { - this.drag_target_channel = None; } cx.notify() } } }) - .as_draggable( + .as_draggable::<_, Channel>( channel.clone(), move |_, channel, cx: &mut ViewContext| { let theme = &theme::current(cx).collab_panel; From 32367eba14f3b08464e2e39c7331d353b2e69a4b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 15:39:02 +0200 Subject: [PATCH 220/274] Set up UI to allow dragging a channel to the root --- crates/channel/src/channel_store.rs | 2 +- crates/collab/src/db/queries/channels.rs | 24 ++--- crates/collab/src/db/tests/channel_tests.rs | 2 +- crates/collab/src/rpc.rs | 2 +- crates/collab/src/tests/channel_tests.rs | 6 +- crates/collab_ui/src/collab_panel.rs | 111 ++++++++++++++------ crates/rpc/proto/zed.proto | 2 +- crates/theme/src/theme.rs | 1 + styles/src/style_tree/collab_panel.ts | 10 +- styles/src/style_tree/search.ts | 5 +- 10 files changed, 107 insertions(+), 58 deletions(-) diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 9757bb8092..efa05d51a9 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -501,7 +501,7 @@ impl ChannelStore { pub fn move_channel( &mut self, channel_id: ChannelId, - to: ChannelId, + to: Option, cx: &mut ModelContext, ) -> Task> { let client = self.client.clone(); diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index b65e677764..a5c6e4dcfc 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1205,37 +1205,37 @@ impl Database { pub async fn move_channel( &self, channel_id: ChannelId, - new_parent_id: ChannelId, + new_parent_id: Option, admin_id: UserId, ) -> Result> { - // check you're an admin of source and target (and maybe current channel) - // change parent_path on current channel - // change parent_path on all children - self.transaction(|tx| async move { + let Some(new_parent_id) = new_parent_id else { + return Err(anyhow!("not supported"))?; + }; + let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) + .await?; let channel = self.get_channel_internal(channel_id, &*tx).await?; self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; - self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) - .await?; let previous_participants = self .get_channel_participant_details_internal(&channel, &*tx) .await?; let old_path = format!("{}{}/", channel.parent_path, channel.id); - let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent_id); + let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent.id); let new_path = format!("{}{}/", new_parent_path, channel.id); if old_path == new_path { return Ok(None); } - let mut channel = channel.into_active_model(); - channel.parent_path = ActiveValue::Set(new_parent_path); - channel.save(&*tx).await?; + let mut model = channel.into_active_model(); + model.parent_path = ActiveValue::Set(new_parent_path); + model.update(&*tx).await?; let descendent_ids = ChannelId::find_by_statement::(Statement::from_sql_and_values( @@ -1250,7 +1250,7 @@ impl Database { .all(&*tx) .await?; - let participants_to_update: HashMap = self + let participants_to_update: HashMap<_, _> = self .participants_to_notify_for_channel_change(&new_parent, &*tx) .await? .into_iter() diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 936765b8c9..0d486003bc 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -424,7 +424,7 @@ async fn test_db_channel_moving_bugs(db: &Arc) { // Move to same parent should be a no-op assert!(db - .move_channel(projects_id, zed_id, user_id) + .move_channel(projects_id, Some(zed_id), user_id) .await .unwrap() .is_none()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 01e8530e67..a0ec7da392 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2476,7 +2476,7 @@ async fn move_channel( session: Session, ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); - let to = ChannelId::from_proto(request.to); + let to = request.to.map(ChannelId::from_proto); let result = session .db() diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index d2c5e1cec3..a33ded6492 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1016,7 +1016,7 @@ async fn test_channel_link_notifications( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(vim_channel, active_channel, cx) + channel_store.move_channel(vim_channel, Some(active_channel), cx) }) .await .unwrap(); @@ -1051,7 +1051,7 @@ async fn test_channel_link_notifications( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(helix_channel, vim_channel, cx) + channel_store.move_channel(helix_channel, Some(vim_channel), cx) }) .await .unwrap(); @@ -1424,7 +1424,7 @@ async fn test_channel_moving( client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_d_id, channel_b_id, cx) + channel_store.move_channel(channel_d_id, Some(channel_b_id), cx) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16f8fb5d02..8d68ee12c0 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -226,7 +226,7 @@ pub fn init(cx: &mut AppContext) { panel .channel_store .update(cx, |channel_store, cx| { - channel_store.move_channel(clipboard.channel_id, selected_channel.id, cx) + channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx) }) .detach_and_log_err(cx) }, @@ -237,7 +237,7 @@ pub fn init(cx: &mut AppContext) { if let Some(clipboard) = panel.channel_clipboard.take() { panel.channel_store.update(cx, |channel_store, cx| { channel_store - .move_channel(clipboard.channel_id, action.to, cx) + .move_channel(clipboard.channel_id, Some(action.to), cx) .detach_and_log_err(cx) }) } @@ -287,11 +287,18 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, - drag_target_channel: Option, + drag_target_channel: ChannelDragTarget, workspace: WeakViewHandle, context_menu_on_selected: bool, } +#[derive(PartialEq, Eq)] +enum ChannelDragTarget { + None, + Root, + Channel(ChannelId), +} + #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { width: Option, @@ -577,7 +584,7 @@ impl CollabPanel { workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, - drag_target_channel: None, + drag_target_channel: ChannelDragTarget::None, list_state, }; @@ -1450,6 +1457,7 @@ impl CollabPanel { let mut channel_link = None; let mut channel_tooltip_text = None; let mut channel_icon = None; + let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { @@ -1533,26 +1541,37 @@ impl CollabPanel { cx, ), ), - Section::Channels => Some( - MouseEventHandler::new::(0, cx, |state, _| { - render_icon_button( - theme - .collab_panel - .add_contact_button - .style_for(is_selected, state), - "icons/plus.svg", - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) - .with_tooltip::( - 0, - "Create a channel", - None, - tooltip_style.clone(), - cx, - ), - ), + Section::Channels => { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + && self.drag_target_channel == ChannelDragTarget::Root + { + is_dragged_over = true; + } + + Some( + MouseEventHandler::new::(0, cx, |state, _| { + render_icon_button( + theme + .collab_panel + .add_contact_button + .style_for(is_selected, state), + "icons/plus.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) + .with_tooltip::( + 0, + "Create a channel", + None, + tooltip_style.clone(), + cx, + ), + ) + } _ => None, }; @@ -1623,9 +1642,37 @@ impl CollabPanel { .constrained() .with_height(theme.collab_panel.row_height) .contained() - .with_style(header_style.container) + .with_style(if is_dragged_over { + theme.collab_panel.dragged_over_header + } else { + header_style.container + }) }); + result = result + .on_move(move |_, this, cx| { + if cx + .global::>() + .currently_dragged::(cx.window()) + .is_some() + { + this.drag_target_channel = ChannelDragTarget::Root; + cx.notify() + } + }) + .on_up(MouseButton::Left, move |_, this, cx| { + if let Some((_, dragged_channel)) = cx + .global::>() + .currently_dragged::(cx.window()) + { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(dragged_channel.id, None, cx) + }) + .detach_and_log_err(cx) + } + }); + if can_collapse { result = result .with_cursor_style(CursorStyle::PointingHand) @@ -1917,13 +1964,7 @@ impl CollabPanel { .global::>() .currently_dragged::(cx.window()) .is_some() - && self - .drag_target_channel - .as_ref() - .filter(|channel_id| { - channel.parent_path.contains(channel_id) || channel.id == **channel_id - }) - .is_some() + && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) { is_dragged_over = true; } @@ -2126,7 +2167,7 @@ impl CollabPanel { ) }) .on_click(MouseButton::Left, move |_, this, cx| { - if this.drag_target_channel.take().is_none() { + if this.drag_target_channel == ChannelDragTarget::None { if is_active { this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) } else { @@ -2147,7 +2188,7 @@ impl CollabPanel { { this.channel_store .update(cx, |channel_store, cx| { - channel_store.move_channel(dragged_channel.id, channel_id, cx) + channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) }) .detach_and_log_err(cx) } @@ -2160,7 +2201,7 @@ impl CollabPanel { .currently_dragged::(cx.window()) { if channel.id != dragged_channel.id { - this.drag_target_channel = Some(channel.id); + this.drag_target_channel = ChannelDragTarget::Channel(channel.id); } cx.notify() } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fddbf1e50d..206777879b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1130,7 +1130,7 @@ message GetChannelMessagesById { message MoveChannel { uint64 channel_id = 1; - uint64 to = 2; + optional uint64 to = 2; } message JoinChannelBuffer { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3f4264886f..e4b8c02eca 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -250,6 +250,7 @@ pub struct CollabPanel { pub add_contact_button: Toggleable>, pub add_channel_button: Toggleable>, pub header_row: ContainedText, + pub dragged_over_header: ContainerStyle, pub subheader_row: Toggleable>, pub leave_call: Interactive, pub contact_row: Toggleable>, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 2a7702842a..272b6055ed 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -210,6 +210,14 @@ export default function contacts_panel(): any { right: SPACING, }, }, + dragged_over_header: { + margin: { top: SPACING }, + padding: { + left: SPACING, + right: SPACING, + }, + background: background(layer, "hovered"), + }, subheader_row, leave_call: interactive({ base: { @@ -279,7 +287,7 @@ export default function contacts_panel(): any { margin: { left: CHANNEL_SPACING, }, - } + }, }, list_empty_label_container: { margin: { diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index b0ac023c09..2317108bde 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,7 +2,6 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" -import { text_button } from "../component/text_button" const search_results = () => { const theme = useTheme() @@ -36,7 +35,7 @@ export default function search(): any { left: 10, right: 4, }, - margin: { right: SEARCH_ROW_SPACING } + margin: { right: SEARCH_ROW_SPACING }, } const include_exclude_editor = { @@ -378,7 +377,7 @@ export default function search(): any { modes_container: { padding: { right: SEARCH_ROW_SPACING, - } + }, }, replace_icon: { icon: { From b5cbfb8f1ddddd40a6e9a78194dd51a97d5eda50 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 15:50:37 +0200 Subject: [PATCH 221/274] Allow moving channels to the root --- crates/collab/src/db/queries/channels.rs | 42 +++++++++++++++------ crates/collab/src/db/tests/channel_tests.rs | 12 ++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index a5c6e4dcfc..68b06e435d 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1209,24 +1209,29 @@ impl Database { admin_id: UserId, ) -> Result> { self.transaction(|tx| async move { - let Some(new_parent_id) = new_parent_id else { - return Err(anyhow!("not supported"))?; - }; - - let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; - self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) - .await?; let channel = self.get_channel_internal(channel_id, &*tx).await?; - self.check_user_is_channel_admin(&channel, admin_id, &*tx) .await?; + let new_parent_path; + let new_parent_channel; + if let Some(new_parent_id) = new_parent_id { + let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?; + self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) + .await?; + + new_parent_path = new_parent.path(); + new_parent_channel = Some(new_parent); + } else { + new_parent_path = String::new(); + new_parent_channel = None; + }; + let previous_participants = self .get_channel_participant_details_internal(&channel, &*tx) .await?; let old_path = format!("{}{}/", channel.parent_path, channel.id); - let new_parent_path = format!("{}{}/", new_parent.parent_path, new_parent.id); let new_path = format!("{}{}/", new_parent_path, channel.id); if old_path == new_path { @@ -1235,7 +1240,19 @@ impl Database { let mut model = channel.into_active_model(); model.parent_path = ActiveValue::Set(new_parent_path); - model.update(&*tx).await?; + let channel = model.update(&*tx).await?; + + if new_parent_channel.is_none() { + channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(admin_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Admin), + } + .insert(&*tx) + .await?; + } let descendent_ids = ChannelId::find_by_statement::(Statement::from_sql_and_values( @@ -1251,7 +1268,10 @@ impl Database { .await?; let participants_to_update: HashMap<_, _> = self - .participants_to_notify_for_channel_change(&new_parent, &*tx) + .participants_to_notify_for_channel_change( + new_parent_channel.as_ref().unwrap_or(&channel), + &*tx, + ) .await? .into_iter() .collect(); diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 0d486003bc..43526c7f24 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -438,6 +438,18 @@ async fn test_db_channel_moving_bugs(db: &Arc) { (livestreaming_id, &[zed_id, projects_id]), ], ); + + // Move the project channel to the root + db.move_channel(projects_id, None, user_id).await.unwrap(); + let result = db.get_channels_for_user(user_id).await.unwrap(); + assert_channel_tree( + result.channels, + &[ + (zed_id, &[]), + (projects_id, &[]), + (livestreaming_id, &[projects_id]), + ], + ); } test_both_dbs!( From 71c72d8e08055521368a192f071ea6b3150ed395 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 25 Oct 2023 16:07:54 +0200 Subject: [PATCH 222/274] v0.111.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 c7b2d51b5d..609c1cb3de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10094,7 +10094,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.110.0" +version = "0.111.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 730dbc6be1..250a1814aa 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.110.0" +version = "0.111.0" publish = false [lib] From 26a3d41dc75d1bb30fa0d4471a809d6fd81a0781 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 25 Oct 2023 07:10:21 -0700 Subject: [PATCH 223/274] Change from try (reserved keyword) to maybe --- crates/collab/src/rpc.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 8 ++++---- crates/db/src/db.rs | 4 ++-- crates/project_panel/src/file_associations.rs | 10 +++++----- crates/util/src/util.rs | 12 ++++++------ crates/zed/src/languages/elixir.rs | 4 ++-- crates/zed/src/languages/lua.rs | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index dda638e107..cfd61c1747 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -944,7 +944,7 @@ async fn create_room( let live_kit_room = live_kit_room.clone(); let live_kit = session.live_kit_client.as_ref(); - util::async_iife!({ + util::async_maybe!({ let live_kit = live_kit?; let token = live_kit diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8c33727cfb..69ba985968 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -46,7 +46,7 @@ use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use std::{borrow::Cow, hash::Hash, mem, sync::Arc}; use theme::{components::ComponentExt, IconButton, Interactive}; -use util::{iife, ResultExt, TryFutureExt}; +use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::ItemHandle, @@ -1487,7 +1487,7 @@ impl CollabPanel { let text = match section { Section::ActiveCall => { - let channel_name = iife!({ + let channel_name = maybe!({ let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; @@ -1929,7 +1929,7 @@ impl CollabPanel { self.selected_channel().map(|channel| channel.0.id) == Some(channel.id); let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok()); - let is_active = iife!({ + let is_active = maybe!({ let call_channel = ActiveCall::global(cx) .read(cx) .room()? @@ -2817,7 +2817,7 @@ impl CollabPanel { } } ListEntry::Channel { channel, .. } => { - let is_active = iife!({ + let is_active = maybe!({ let call_channel = ActiveCall::global(cx) .read(cx) .room()? diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index a28db249d3..9247c6e36d 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -20,7 +20,7 @@ use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use util::channel::ReleaseChannel; -use util::{async_iife, ResultExt}; +use util::{async_maybe, ResultExt}; const CONNECTION_INITIALIZE_QUERY: &'static str = sql!( PRAGMA foreign_keys=TRUE; @@ -57,7 +57,7 @@ pub async fn open_db( let release_channel_name = release_channel.dev_name(); let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name))); - let connection = async_iife!({ + let connection = async_maybe!({ smol::fs::create_dir_all(&main_db_dir) .await .context("Could not create db directory") diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index f2692b96db..9e9a865f3e 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{AppContext, AssetSource}; use serde_derive::Deserialize; -use util::{iife, paths::PathExt}; +use util::{maybe, paths::PathExt}; #[derive(Deserialize, Debug)] struct TypeConfig { @@ -42,12 +42,12 @@ impl FileAssociations { } pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { - iife!({ + maybe!({ let this = cx.has_global::().then(|| cx.global::())?; // FIXME: Associate a type with the languages and have the file's langauge // override these associations - iife!({ + maybe!({ let suffix = path.icon_suffix()?; this.suffixes @@ -61,7 +61,7 @@ impl FileAssociations { } pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { - iife!({ + maybe!({ let this = cx.has_global::().then(|| cx.global::())?; let key = if expanded { @@ -78,7 +78,7 @@ impl FileAssociations { } pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { - iife!({ + maybe!({ let this = cx.has_global::().then(|| cx.global::())?; let key = if expanded { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 8bc0607f9d..19a2cd9077 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -349,19 +349,19 @@ pub fn unzip_option(option: Option<(T, U)>) -> (Option, Option) { } } -/// Immediately invoked function expression. Good for using the ? operator +/// Evaluates to an immediately invoked function expression. Good for using the ? operator /// in functions which do not return an Option or Result #[macro_export] -macro_rules! try { +macro_rules! maybe { ($block:block) => { (|| $block)() }; } -/// Async Immediately invoked function expression. Good for using the ? operator -/// in functions which do not return an Option or Result. Async version of above +/// Evaluates to an immediately invoked function expression. Good for using the ? operator +/// in functions which do not return an Option or Result, but async. #[macro_export] -macro_rules! async_try { +macro_rules! async_maybe { ($block:block) => { (|| async move { $block })() }; @@ -434,7 +434,7 @@ mod tests { None } - let foo = iife!({ + let foo = maybe!({ option_returning_function()?; Some(()) }); diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index 9d2ebb7f47..5c0ff273ae 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -19,7 +19,7 @@ use std::{ }, }; use util::{ - async_iife, + async_maybe, fs::remove_matching, github::{latest_github_release, GitHubLspBinaryVersion}, ResultExt, @@ -421,7 +421,7 @@ impl LspAdapter for NextLspAdapter { } async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option { - async_iife!({ + async_maybe!({ let mut last_binary_path = None; let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 8187847c9a..5fffb37e81 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -8,7 +8,7 @@ use lsp::LanguageServerBinary; use smol::fs; use std::{any::Any, env::consts, path::PathBuf}; use util::{ - async_iife, + async_maybe, github::{latest_github_release, GitHubLspBinaryVersion}, ResultExt, }; @@ -106,7 +106,7 @@ impl super::LspAdapter for LuaLspAdapter { } async fn get_cached_server_binary(container_dir: PathBuf) -> Option { - async_iife!({ + async_maybe!({ let mut last_binary_path = None; let mut entries = fs::read_dir(&container_dir).await?; while let Some(entry) = entries.next().await { From c44d1cda9a732838f98162fb88b6aeeedbe551dd Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 25 Oct 2023 16:24:53 +0200 Subject: [PATCH 224/274] collab 0.26.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 609c1cb3de..7413faedd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1468,7 +1468,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.25.0" +version = "0.26.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 64bc191b21..903275406d 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.25.0" +version = "0.26.0" publish = false [[bin]] From eb8d37627431e511684e04fc51529754c54b1cb1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 17:15:33 +0200 Subject: [PATCH 225/274] Avoid unused import in release builds --- crates/zed/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5add524414..0cdedd6745 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -34,7 +34,7 @@ use std::{ Arc, Weak, }, thread, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::{SystemTime, UNIX_EPOCH}, }; use util::{ channel::{parse_zed_link, ReleaseChannel}, @@ -684,7 +684,7 @@ fn load_embedded_fonts(app: &App) { #[cfg(debug_assertions)] async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { let mut events = fs - .watch("styles/src".as_ref(), Duration::from_millis(100)) + .watch("styles/src".as_ref(), std::time::Duration::from_millis(100)) .await; while (events.next().await).is_some() { let output = Command::new("npm") @@ -710,7 +710,7 @@ async fn watch_languages(fs: Arc, languages: Arc) -> O let mut events = fs .watch( "crates/zed/src/languages".as_ref(), - Duration::from_millis(100), + std::time::Duration::from_millis(100), ) .await; while (events.next().await).is_some() { @@ -725,7 +725,7 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { let mut events = fs .watch( "assets/icons/file_icons/file_types.json".as_ref(), - Duration::from_millis(100), + std::time::Duration::from_millis(100), ) .await; while (events.next().await).is_some() { From 2c5caf91bcb3382e4f69b2d030152d62084de62a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 17:37:14 +0200 Subject: [PATCH 226/274] Bump RPC version for channels + notifications changes --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 682ba6ac73..6f35bf64bc 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 65; +pub const PROTOCOL_VERSION: u32 = 66; From 841a5ef7b86c3516e0d4629645a0db9c5c6d69e6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 17:38:09 +0200 Subject: [PATCH 227/274] collab 0.27.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 7413faedd4..08ffcac7cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1468,7 +1468,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.26.0" +version = "0.27.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 903275406d..987c295407 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.26.0" +version = "0.27.0" publish = false [[bin]] From 3a369bc20777865db85ce6ad4fed27ed241f79f9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2023 18:02:27 +0200 Subject: [PATCH 228/274] Name embedded.provisionprofile the same on stable as other channels --- ...e.provisionprofile => embedded.provisionprofile} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/zed/contents/stable/{Zed_Stable_Provisioning_Profile.provisionprofile => embedded.provisionprofile} (100%) diff --git a/crates/zed/contents/stable/Zed_Stable_Provisioning_Profile.provisionprofile b/crates/zed/contents/stable/embedded.provisionprofile similarity index 100% rename from crates/zed/contents/stable/Zed_Stable_Provisioning_Profile.provisionprofile rename to crates/zed/contents/stable/embedded.provisionprofile From 39480364bd22ae55acf58a7f1b9cab242d261f07 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:34:14 +0200 Subject: [PATCH 229/274] vcs_menu: Fix a circular view handle in modal picker. Co-authored-by: Julia Risley --- crates/vcs_menu/src/lib.rs | 49 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index dce3724ccd..7e89e489ff 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -16,7 +16,7 @@ actions!(branches, [OpenRecent]); pub fn init(cx: &mut AppContext) { Picker::::init(cx); - cx.add_async_action(toggle); + cx.add_action(toggle); } pub type BranchList = Picker; @@ -24,30 +24,29 @@ pub fn build_branch_list( workspace: ViewHandle, cx: &mut ViewContext, ) -> Result { - Ok(Picker::new(BranchListDelegate::new(workspace, 29, cx)?, cx) - .with_theme(|theme| theme.picker.clone())) + let delegate = workspace.read_with(cx, |workspace, cx| { + BranchListDelegate::new(workspace, cx.handle(), 29, cx) + })?; + + Ok(Picker::new(delegate, cx).with_theme(|theme| theme.picker.clone())) } fn toggle( - _: &mut Workspace, + workspace: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext, -) -> Option>> { - Some(cx.spawn(|workspace, mut cx| async move { - workspace.update(&mut cx, |workspace, cx| { - // Modal branch picker has a longer trailoff than a popover one. - let delegate = BranchListDelegate::new(cx.handle(), 70, cx)?; - workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| { - Picker::new(delegate, cx) - .with_theme(|theme| theme.picker.clone()) - .with_max_size(800., 1200.) - }) - }); - Ok::<_, anyhow::Error>(()) - })??; - Ok(()) - })) +) -> Result<()> { + // Modal branch picker has a longer trailoff than a popover one. + let delegate = BranchListDelegate::new(workspace, cx.handle(), 70, cx)?; + workspace.toggle_modal(cx, |_, cx| { + cx.add_view(|cx| { + Picker::new(delegate, cx) + .with_theme(|theme| theme.picker.clone()) + .with_max_size(800., 1200.) + }) + }); + + Ok(()) } pub struct BranchListDelegate { @@ -62,15 +61,16 @@ pub struct BranchListDelegate { impl BranchListDelegate { fn new( - workspace: ViewHandle, + workspace: &Workspace, + handle: ViewHandle, branch_name_trailoff_after: usize, cx: &AppContext, ) -> Result { - let project = workspace.read(cx).project().read(&cx); - + let project = workspace.project().read(&cx); let Some(worktree) = project.visible_worktrees(cx).next() else { bail!("Cannot update branch list as there are no visible worktrees") }; + let mut cwd = worktree.read(cx).abs_path().to_path_buf(); cwd.push(".git"); let Some(repo) = project.fs().open_repo(&cwd) else { @@ -79,13 +79,14 @@ impl BranchListDelegate { let all_branches = repo.lock().branches()?; Ok(Self { matches: vec![], - workspace, + workspace: handle, all_branches, selected_index: 0, last_query: Default::default(), branch_name_trailoff_after, }) } + fn display_error_toast(&self, message: String, cx: &mut ViewContext) { const GIT_CHECKOUT_FAILURE_ID: usize = 2048; self.workspace.update(cx, |model, ctx| { From 7f6bb3d1eb5dc0ddbd7de0687796610f116e47ce Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:31:47 +0200 Subject: [PATCH 230/274] Extract multi_buffer module out of editor (#3170) Release Notes: - N/A --- Cargo.lock | 51 +++++ Cargo.toml | 1 + crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant_panel.rs | 2 +- crates/assistant/src/codegen.rs | 3 +- crates/editor/Cargo.toml | 2 + crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/display_map/fold_map.rs | 2 +- crates/editor/src/display_map/inlay_map.rs | 6 +- crates/editor/src/editor.rs | 5 +- crates/editor/src/git.rs | 193 +++++++++++++++++ .../src/test/editor_lsp_test_context.rs | 4 +- crates/multi_buffer/Cargo.toml | 80 +++++++ .../src}/anchor.rs | 10 +- .../src/multi_buffer.rs | 198 +----------------- 15 files changed, 347 insertions(+), 213 deletions(-) create mode 100644 crates/multi_buffer/Cargo.toml rename crates/{editor/src/multi_buffer => multi_buffer/src}/anchor.rs (95%) rename crates/{editor => multi_buffer}/src/multi_buffer.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 08ffcac7cf..8ab616fbd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,6 +310,7 @@ dependencies = [ "language", "log", "menu", + "multi_buffer", "ordered-float 2.10.0", "parking_lot 0.11.2", "project", @@ -2410,6 +2411,7 @@ dependencies = [ "lazy_static", "log", "lsp", + "multi_buffer", "ordered-float 2.10.0", "parking_lot 0.11.2", "postage", @@ -4600,6 +4602,55 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" +[[package]] +name = "multi_buffer" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "anyhow", + "client", + "clock", + "collections", + "context_menu", + "convert_case 0.6.0", + "copilot", + "ctor", + "env_logger 0.9.3", + "futures 0.3.28", + "git", + "gpui", + "indoc", + "itertools 0.10.5", + "language", + "lazy_static", + "log", + "lsp", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "postage", + "project", + "pulldown-cmark", + "rand 0.8.5", + "rich_text", + "schemars", + "serde", + "serde_derive", + "settings", + "smallvec", + "smol", + "snippet", + "sum_tree", + "text", + "theme", + "tree-sitter", + "tree-sitter-html", + "tree-sitter-rust", + "tree-sitter-typescript", + "unindent", + "util", + "workspace", +] + [[package]] name = "multimap" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 836a0bd6b2..1d9da19605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "crates/lsp", "crates/media", "crates/menu", + "crates/multi_buffer", "crates/node_runtime", "crates/notifications", "crates/outline", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 9cfdd3301a..256f4d8416 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -17,6 +17,7 @@ fs = { path = "../fs" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } +multi_buffer = { path = "../multi_buffer" } search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index ca8c54a285..0dee8be510 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -296,7 +296,7 @@ impl AssistantPanel { }; let selection = editor.read(cx).selections.newest_anchor().clone(); - if selection.start.excerpt_id() != selection.end.excerpt_id() { + if selection.start.excerpt_id != selection.end.excerpt_id { return; } let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index b6ef6b5cfa..6b79daba42 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -1,10 +1,11 @@ use crate::streaming_diff::{Hunk, StreamingDiff}; use ai::completion::{CompletionProvider, OpenAIRequest}; use anyhow::Result; -use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{Entity, ModelContext, ModelHandle, Task}; use language::{Rope, TransactionId}; +use multi_buffer; use std::{cmp, future, ops::Range, sync::Arc}; pub enum Event { diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index d03e1c1106..95d7820063 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -14,6 +14,7 @@ test-support = [ "text/test-support", "language/test-support", "gpui/test-support", + "multi_buffer/test-support", "project/test-support", "util/test-support", "workspace/test-support", @@ -34,6 +35,7 @@ git = { path = "../git" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } +multi_buffer = { path = "../multi_buffer" } project = { path = "../project" } rpc = { path = "../rpc" } rich_text = { path = "../rich_text" } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index e54ac04d89..c07625bf9c 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -993,8 +993,8 @@ mod tests { use super::*; use crate::display_map::inlay_map::InlayMap; use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; - use crate::multi_buffer::MultiBuffer; use gpui::{elements::Empty, Element}; + use multi_buffer::MultiBuffer; use rand::prelude::*; use settings::SettingsStore; use std::env; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 8faa0c3ec2..4636d9a17f 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -91,7 +91,7 @@ impl<'a> FoldMapWriter<'a> { // For now, ignore any ranges that span an excerpt boundary. let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); - if fold.0.start.excerpt_id() != fold.0.end.excerpt_id() { + if fold.0.start.excerpt_id != fold.0.end.excerpt_id { continue; } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 124b32c234..c0c352453b 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,10 +1,8 @@ -use crate::{ - multi_buffer::{MultiBufferChunks, MultiBufferRows}, - Anchor, InlayId, MultiBufferSnapshot, ToOffset, -}; +use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset}; use collections::{BTreeMap, BTreeSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; +use multi_buffer::{MultiBufferChunks, MultiBufferRows}; use std::{ any::TypeId, cmp, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 140d79fd33..bfb87afff2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11,7 +11,6 @@ pub mod items; mod link_go_to_definition; mod mouse_context_menu; pub mod movement; -pub mod multi_buffer; mod persistence; pub mod scroll; pub mod selections_collection; @@ -7716,8 +7715,8 @@ impl Editor { let mut buffer_highlights = this .document_highlights_for_position(selection.head(), &buffer) .filter(|highlight| { - highlight.start.excerpt_id() == selection.head().excerpt_id() - && highlight.end.excerpt_id() == selection.head().excerpt_id() + highlight.start.excerpt_id == selection.head().excerpt_id + && highlight.end.excerpt_id == selection.head().excerpt_id }); buffer_highlights .next() diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 0ec7358df7..f8c6ef9a1f 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -87,3 +87,196 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> } } } + +#[cfg(any(test, feature = "test_support"))] +mod tests { + use crate::editor_tests::init_test; + use crate::Point; + use gpui::TestAppContext; + use multi_buffer::{ExcerptRange, MultiBuffer}; + use project::{FakeFs, Project}; + use unindent::Unindent; + #[gpui::test] + async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { + use git::diff::DiffHunkStatus; + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, [], cx).await; + + // buffer has two modified hunks with two rows each + let buffer_1 = project + .update(cx, |project, cx| { + project.create_buffer( + " + 1.zero + 1.ONE + 1.TWO + 1.three + 1.FOUR + 1.FIVE + 1.six + " + .unindent() + .as_str(), + None, + cx, + ) + }) + .unwrap(); + buffer_1.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 1.zero + 1.one + 1.two + 1.three + 1.four + 1.five + 1.six + " + .unindent(), + ), + cx, + ); + }); + + // buffer has a deletion hunk and an insertion hunk + let buffer_2 = project + .update(cx, |project, cx| { + project.create_buffer( + " + 2.zero + 2.one + 2.two + 2.three + 2.four + 2.five + 2.six + " + .unindent() + .as_str(), + None, + cx, + ) + }) + .unwrap(); + buffer_2.update(cx, |buffer, cx| { + buffer.set_diff_base( + Some( + " + 2.zero + 2.one + 2.one-and-a-half + 2.two + 2.three + 2.four + 2.six + " + .unindent(), + ), + cx, + ); + }); + + cx.foreground().run_until_parked(); + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // excerpt ends in the middle of a modified hunk + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt begins in the middle of a modified hunk + ExcerptRange { + context: Point::new(5, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + // excerpt ends at a deletion + ExcerptRange { + context: Point::new(0, 0)..Point::new(1, 5), + primary: Default::default(), + }, + // excerpt starts at a deletion + ExcerptRange { + context: Point::new(2, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains a deletion hunk + ExcerptRange { + context: Point::new(1, 0)..Point::new(2, 5), + primary: Default::default(), + }, + // excerpt fully contains an insertion hunk + ExcerptRange { + context: Point::new(4, 0)..Point::new(6, 5), + primary: Default::default(), + }, + ], + cx, + ); + multibuffer + }); + + let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); + + assert_eq!( + snapshot.text(), + " + 1.zero + 1.ONE + 1.FIVE + 1.six + 2.zero + 2.one + 2.two + 2.one + 2.two + 2.four + 2.five + 2.six" + .unindent() + ); + + let expected = [ + (DiffHunkStatus::Modified, 1..2), + (DiffHunkStatus::Modified, 2..3), + //TODO: Define better when and where removed hunks show up at range extremities + (DiffHunkStatus::Removed, 6..6), + (DiffHunkStatus::Removed, 8..8), + (DiffHunkStatus::Added, 10..11), + ]; + + assert_eq!( + snapshot + .git_diff_hunks_in_range(0..12) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + &expected, + ); + + assert_eq!( + snapshot + .git_diff_hunks_in_range_rev(0..12) + .map(|hunk| (hunk.status(), hunk.buffer_range)) + .collect::>(), + expected + .iter() + .rev() + .cloned() + .collect::>() + .as_slice(), + ); + } +} diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 085ce96382..3e2f38a0b6 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -6,18 +6,18 @@ use std::{ use anyhow::Result; +use crate::{Editor, ToPoint}; use collections::HashSet; use futures::Future; use gpui::{json, ViewContext, ViewHandle}; use indoc::indoc; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use lsp::{notification, request}; +use multi_buffer::ToPointUtf16; use project::Project; use smol::stream::StreamExt; use workspace::{AppState, Workspace, WorkspaceHandle}; -use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint}; - use super::editor_test_context::EditorTestContext; pub struct EditorLspTestContext<'a> { diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml new file mode 100644 index 0000000000..02c71c734a --- /dev/null +++ b/crates/multi_buffer/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "multi_buffer" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/multi_buffer.rs" +doctest = false + +[features] +test-support = [ + "copilot/test-support", + "text/test-support", + "language/test-support", + "gpui/test-support", + "util/test-support", + "tree-sitter-rust", + "tree-sitter-typescript" +] + +[dependencies] +client = { path = "../client" } +clock = { path = "../clock" } +collections = { path = "../collections" } +context_menu = { path = "../context_menu" } +git = { path = "../git" } +gpui = { path = "../gpui" } +language = { path = "../language" } +lsp = { path = "../lsp" } +rich_text = { path = "../rich_text" } +settings = { path = "../settings" } +snippet = { path = "../snippet" } +sum_tree = { path = "../sum_tree" } +text = { path = "../text" } +theme = { path = "../theme" } +util = { path = "../util" } + +aho-corasick = "1.1" +anyhow.workspace = true +convert_case = "0.6.0" +futures.workspace = true +indoc = "1.0.4" +itertools = "0.10" +lazy_static.workspace = true +log.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +postage.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } +rand.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +smallvec.workspace = true +smol.workspace = true + +tree-sitter-rust = { workspace = true, optional = true } +tree-sitter-html = { workspace = true, optional = true } +tree-sitter-typescript = { workspace = true, optional = true } + +[dev-dependencies] +copilot = { path = "../copilot", features = ["test-support"] } +text = { path = "../text", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +rand.workspace = true +unindent.workspace = true +tree-sitter.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-html.workspace = true +tree-sitter-typescript.workspace = true diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/multi_buffer/src/anchor.rs similarity index 95% rename from crates/editor/src/multi_buffer/anchor.rs rename to crates/multi_buffer/src/anchor.rs index 1be4dc2dfb..39a8182da1 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -8,9 +8,9 @@ use sum_tree::Bias; #[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] pub struct Anchor { - pub(crate) buffer_id: Option, - pub(crate) excerpt_id: ExcerptId, - pub(crate) text_anchor: text::Anchor, + pub buffer_id: Option, + pub excerpt_id: ExcerptId, + pub text_anchor: text::Anchor, } impl Anchor { @@ -30,10 +30,6 @@ impl Anchor { } } - pub fn excerpt_id(&self) -> ExcerptId { - self.excerpt_id - } - pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot); if excerpt_id_cmp.is_eq() { diff --git a/crates/editor/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs similarity index 97% rename from crates/editor/src/multi_buffer.rs rename to crates/multi_buffer/src/multi_buffer.rs index 23a117405c..fc629c653f 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -303,7 +303,7 @@ impl MultiBuffer { self.snapshot.borrow().clone() } - pub(crate) fn read(&self, cx: &AppContext) -> Ref { + pub fn read(&self, cx: &AppContext) -> Ref { self.sync(cx); self.snapshot.borrow() } @@ -589,7 +589,7 @@ impl MultiBuffer { self.start_transaction_at(Instant::now(), cx) } - pub(crate) fn start_transaction_at( + pub fn start_transaction_at( &mut self, now: Instant, cx: &mut ModelContext, @@ -608,7 +608,7 @@ impl MultiBuffer { self.end_transaction_at(Instant::now(), cx) } - pub(crate) fn end_transaction_at( + pub fn end_transaction_at( &mut self, now: Instant, cx: &mut ModelContext, @@ -1508,7 +1508,7 @@ impl MultiBuffer { "untitled".into() } - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] pub fn is_parsing(&self, cx: &AppContext) -> bool { self.as_singleton().unwrap().read(cx).is_parsing() } @@ -3198,7 +3198,7 @@ impl MultiBufferSnapshot { theme: Option<&SyntaxTheme>, ) -> Option<(u64, Vec>)> { let anchor = self.anchor_before(offset); - let excerpt_id = anchor.excerpt_id(); + let excerpt_id = anchor.excerpt_id; let excerpt = self.excerpt(excerpt_id)?; Some(( excerpt.buffer_id, @@ -4129,17 +4129,13 @@ where #[cfg(test)] mod tests { - use crate::editor_tests::init_test; - use super::*; use futures::StreamExt; use gpui::{AppContext, TestAppContext}; use language::{Buffer, Rope}; - use project::{FakeFs, Project}; use rand::prelude::*; use settings::SettingsStore; use std::{env, rc::Rc}; - use unindent::Unindent; use util::test::sample_text; #[gpui::test] @@ -4838,190 +4834,6 @@ mod tests { ); } - #[gpui::test] - async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { - use git::diff::DiffHunkStatus; - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.background()); - let project = Project::test(fs, [], cx).await; - - // buffer has two modified hunks with two rows each - let buffer_1 = project - .update(cx, |project, cx| { - project.create_buffer( - " - 1.zero - 1.ONE - 1.TWO - 1.three - 1.FOUR - 1.FIVE - 1.six - " - .unindent() - .as_str(), - None, - cx, - ) - }) - .unwrap(); - buffer_1.update(cx, |buffer, cx| { - buffer.set_diff_base( - Some( - " - 1.zero - 1.one - 1.two - 1.three - 1.four - 1.five - 1.six - " - .unindent(), - ), - cx, - ); - }); - - // buffer has a deletion hunk and an insertion hunk - let buffer_2 = project - .update(cx, |project, cx| { - project.create_buffer( - " - 2.zero - 2.one - 2.two - 2.three - 2.four - 2.five - 2.six - " - .unindent() - .as_str(), - None, - cx, - ) - }) - .unwrap(); - buffer_2.update(cx, |buffer, cx| { - buffer.set_diff_base( - Some( - " - 2.zero - 2.one - 2.one-and-a-half - 2.two - 2.three - 2.four - 2.six - " - .unindent(), - ), - cx, - ); - }); - - cx.foreground().run_until_parked(); - - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer_1.clone(), - [ - // excerpt ends in the middle of a modified hunk - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 5), - primary: Default::default(), - }, - // excerpt begins in the middle of a modified hunk - ExcerptRange { - context: Point::new(5, 0)..Point::new(6, 5), - primary: Default::default(), - }, - ], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ - // excerpt ends at a deletion - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 5), - primary: Default::default(), - }, - // excerpt starts at a deletion - ExcerptRange { - context: Point::new(2, 0)..Point::new(2, 5), - primary: Default::default(), - }, - // excerpt fully contains a deletion hunk - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 5), - primary: Default::default(), - }, - // excerpt fully contains an insertion hunk - ExcerptRange { - context: Point::new(4, 0)..Point::new(6, 5), - primary: Default::default(), - }, - ], - cx, - ); - multibuffer - }); - - let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); - - assert_eq!( - snapshot.text(), - " - 1.zero - 1.ONE - 1.FIVE - 1.six - 2.zero - 2.one - 2.two - 2.one - 2.two - 2.four - 2.five - 2.six" - .unindent() - ); - - let expected = [ - (DiffHunkStatus::Modified, 1..2), - (DiffHunkStatus::Modified, 2..3), - //TODO: Define better when and where removed hunks show up at range extremities - (DiffHunkStatus::Removed, 6..6), - (DiffHunkStatus::Removed, 8..8), - (DiffHunkStatus::Added, 10..11), - ]; - - assert_eq!( - snapshot - .git_diff_hunks_in_range(0..12) - .map(|hunk| (hunk.status(), hunk.buffer_range)) - .collect::>(), - &expected, - ); - - assert_eq!( - snapshot - .git_diff_hunks_in_range_rev(0..12) - .map(|hunk| (hunk.status(), hunk.buffer_range)) - .collect::>(), - expected - .iter() - .rev() - .cloned() - .collect::>() - .as_slice(), - ); - } - #[gpui::test(iterations = 100)] fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") From ffcec011f85ac9e9fb8ef3a597ac2279d897d054 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 25 Oct 2023 23:24:17 +0200 Subject: [PATCH 231/274] Don't use function_name in vim tests --- .../src/test/neovim_backed_test_context.rs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 7944e9297c..d6c00c8534 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,7 +1,10 @@ use editor::scroll::VERTICAL_SCROLL_MARGIN; use indoc::indoc; use settings::SettingsStore; -use std::ops::{Deref, DerefMut}; +use std::{ + ops::{Deref, DerefMut}, + panic, thread, +}; use collections::{HashMap, HashSet}; use gpui::{geometry::vector::vec2f, ContextHandle}; @@ -59,12 +62,22 @@ pub struct NeovimBackedTestContext<'a> { impl<'a> NeovimBackedTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> { - let function_name = cx.function_name.clone(); - let cx = VimTestContext::new(cx, true).await; + // rust stores the name of the test on the current thread. + // We use this to automatically name a file that will store + // the neovim connection's requests/responses so that we can + // run without neovim on CI. + let thread = thread::current(); + let test_name = thread + .name() + .expect("thread is not named") + .split(":") + .last() + .unwrap() + .to_string(); Self { - cx, + cx: VimTestContext::new(cx, true).await, exemptions: Default::default(), - neovim: NeovimConnection::new(function_name).await, + neovim: NeovimConnection::new(test_name).await, last_set_state: None, recent_keystrokes: Default::default(), From 1ec6638c7f5d812085e9e5e5f5da6c06c0800cea Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:16:21 +0200 Subject: [PATCH 232/274] vue: use anyhow::ensure instead of asserting on filesystem state (#3173) Release Notes: - Fixed a crash on failed assertion in Vue.js language support. --- crates/zed/src/languages/vue.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/languages/vue.rs b/crates/zed/src/languages/vue.rs index f0374452df..16afd2e299 100644 --- a/crates/zed/src/languages/vue.rs +++ b/crates/zed/src/languages/vue.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, ensure, Result}; use async_trait::async_trait; use futures::StreamExt; pub use language::*; @@ -98,7 +98,10 @@ impl super::LspAdapter for VueLspAdapter { ) .await?; } - assert!(fs::metadata(&server_path).await.is_ok()); + ensure!( + fs::metadata(&server_path).await.is_ok(), + "@vue/language-server package installation failed" + ); if fs::metadata(&ts_path).await.is_err() { self.node .npm_install_packages( @@ -108,7 +111,10 @@ impl super::LspAdapter for VueLspAdapter { .await?; } - assert!(fs::metadata(&ts_path).await.is_ok()); + ensure!( + fs::metadata(&ts_path).await.is_ok(), + "typescript for Vue package installation failed" + ); *self.typescript_install_path.lock() = Some(ts_path); Ok(LanguageServerBinary { path: self.node.binary_path().await?, From bc3572f80e88105619edbfede449f799478f2592 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:39:45 +0200 Subject: [PATCH 233/274] util: Improve error message for failing requests to GH. (#3159) Release notes: - N/A Co-authored-by: Julia Risley --- crates/util/src/github.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index a3df4c996b..e4b316e12d 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -1,5 +1,5 @@ use crate::http::HttpClient; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use futures::AsyncReadExt; use serde::Deserialize; use std::sync::Arc; @@ -46,6 +46,14 @@ pub async fn latest_github_release( .await .context("error reading latest release")?; + if response.status().is_client_error() { + let text = String::from_utf8_lossy(body.as_slice()); + bail!( + "status error {}, response: {text:?}", + response.status().as_u16() + ); + } + let releases = match serde_json::from_slice::>(body.as_slice()) { Ok(releases) => releases, From 170ebd822118543e7c7eb6c5361cb1e6371dbce4 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 26 Oct 2023 12:29:22 +0200 Subject: [PATCH 234/274] Capture language server stderr during startup/init and log if failure --- Cargo.lock | 2 ++ crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 12 +++++++++-- crates/language/src/language.rs | 20 +++++++++--------- crates/lsp/src/lsp.rs | 36 +++++++++++++++++++++++---------- crates/prettier/Cargo.toml | 1 + crates/prettier/src/prettier.rs | 1 + crates/project/src/project.rs | 33 ++++++++++++++++-------------- 8 files changed, 68 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08ffcac7cf..83aa2b9a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1713,6 +1713,7 @@ dependencies = [ "log", "lsp", "node_runtime", + "parking_lot 0.11.2", "rpc", "serde", "serde_derive", @@ -5562,6 +5563,7 @@ dependencies = [ "log", "lsp", "node_runtime", + "parking_lot 0.11.2", "serde", "serde_derive", "serde_json", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index bac335f7b7..2558974753 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -36,6 +36,7 @@ serde.workspace = true serde_derive.workspace = true smol.workspace = true futures.workspace = true +parking_lot.workspace = true [dev-dependencies] clock = { path = "../clock" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4b81bebc02..3b383c2ac9 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -16,6 +16,7 @@ use language::{ }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId}; use node_runtime::NodeRuntime; +use parking_lot::Mutex; use request::StatusNotification; use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; @@ -387,8 +388,15 @@ impl Copilot { path: node_path, arguments, }; - let server = - LanguageServer::new(new_server_id, binary, Path::new("/"), None, cx.clone())?; + + let server = LanguageServer::new( + Arc::new(Mutex::new(None)), + new_server_id, + binary, + Path::new("/"), + None, + cx.clone(), + )?; server .on_notification::( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 59d1d12cb9..6a40c7974c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -645,7 +645,7 @@ struct LanguageRegistryState { pub struct PendingLanguageServer { pub server_id: LanguageServerId, - pub task: Task>>, + pub task: Task>, pub container_dir: Option>, } @@ -884,6 +884,7 @@ impl LanguageRegistry { pub fn create_pending_language_server( self: &Arc, + stderr_capture: Arc>>, language: Arc, adapter: Arc, root_path: Arc, @@ -923,7 +924,7 @@ impl LanguageRegistry { }) .detach(); - Ok(Some(server)) + Ok(server) }); return Some(PendingLanguageServer { @@ -971,24 +972,23 @@ impl LanguageRegistry { .clone(); drop(lock); - let binary = match entry.clone().await.log_err() { - Some(binary) => binary, - None => return Ok(None), + let binary = match entry.clone().await { + Ok(binary) => binary, + Err(err) => anyhow::bail!("{err}"), }; if let Some(task) = adapter.will_start_server(&delegate, &mut cx) { - if task.await.log_err().is_none() { - return Ok(None); - } + task.await?; } - Ok(Some(lsp::LanguageServer::new( + lsp::LanguageServer::new( + stderr_capture, server_id, binary, &root_path, adapter.code_action_kinds(), cx, - )?)) + ) }) }; diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index b4099e2f6e..98fd81f012 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -136,6 +136,7 @@ struct Error { impl LanguageServer { pub fn new( + stderr_capture: Arc>>, server_id: LanguageServerId, binary: LanguageServerBinary, root_path: &Path, @@ -165,6 +166,7 @@ impl LanguageServer { stdin, stdout, Some(stderr), + stderr_capture, Some(server), root_path, code_action_kinds, @@ -197,6 +199,7 @@ impl LanguageServer { stdin: Stdin, stdout: Stdout, stderr: Option, + stderr_capture: Arc>>, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -218,20 +221,23 @@ impl LanguageServer { let io_handlers = Arc::new(Mutex::new(HashMap::default())); let stdout_input_task = cx.spawn(|cx| { - { - Self::handle_input( - stdout, - on_unhandled_notification.clone(), - notification_handlers.clone(), - response_handlers.clone(), - io_handlers.clone(), - cx, - ) - } + Self::handle_input( + stdout, + on_unhandled_notification.clone(), + notification_handlers.clone(), + response_handlers.clone(), + io_handlers.clone(), + cx, + ) .log_err() }); let stderr_input_task = stderr - .map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err())) + .map(|stderr| { + cx.spawn(|_| { + Self::handle_stderr(stderr, io_handlers.clone(), stderr_capture.clone()) + .log_err() + }) + }) .unwrap_or_else(|| Task::Ready(Some(None))); let input_task = cx.spawn(|_| async move { let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); @@ -353,12 +359,14 @@ impl LanguageServer { async fn handle_stderr( stderr: Stderr, io_handlers: Arc>>, + stderr_capture: Arc>>, ) -> anyhow::Result<()> where Stderr: AsyncRead + Unpin + Send + 'static, { let mut stderr = BufReader::new(stderr); let mut buffer = Vec::new(); + loop { buffer.clear(); stderr.read_until(b'\n', &mut buffer).await?; @@ -367,6 +375,10 @@ impl LanguageServer { for handler in io_handlers.lock().values_mut() { handler(IoKind::StdErr, message); } + + if let Some(stderr) = stderr_capture.lock().as_mut() { + stderr.push_str(message); + } } // Don't starve the main thread when receiving lots of messages at once. @@ -938,6 +950,7 @@ impl LanguageServer { stdin_writer, stdout_reader, None::, + Arc::new(Mutex::new(None)), None, Path::new("/"), None, @@ -950,6 +963,7 @@ impl LanguageServer { stdout_writer, stdin_reader, None::, + Arc::new(Mutex::new(None)), None, Path::new("/"), None, diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml index 997fa87126..4419112baf 100644 --- a/crates/prettier/Cargo.toml +++ b/crates/prettier/Cargo.toml @@ -27,6 +27,7 @@ serde_derive.workspace = true serde_json.workspace = true anyhow.workspace = true futures.workspace = true +parking_lot.workspace = true [dev-dependencies] language = { path = "../language", features = ["test-support"] } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index bddfcb3a8f..79fef40908 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -210,6 +210,7 @@ impl Prettier { .spawn(async move { node.binary_path().await }) .await?; let server = LanguageServer::new( + Arc::new(parking_lot::Mutex::new(None)), server_id, LanguageServerBinary { path: node_path, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fd21c64945..924c3d0095 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -52,6 +52,7 @@ use lsp::{ }; use lsp_command::*; use node_runtime::NodeRuntime; +use parking_lot::Mutex; use postage::watch; use prettier::{LocateStart, Prettier}; use project_settings::{LspSettings, ProjectSettings}; @@ -2726,7 +2727,9 @@ impl Project { return; } + let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); let pending_server = match self.languages.create_pending_language_server( + stderr_capture.clone(), language.clone(), adapter.clone(), worktree_path, @@ -2763,10 +2766,14 @@ impl Project { .await; match result { - Ok(server) => server, + Ok(server) => { + stderr_capture.lock().take(); + Some(server) + } Err(err) => { log::error!("failed to start language server {:?}: {}", server_name, err); + log::error!("server stderr: {:?}", stderr_capture.lock().take()); if let Some(this) = this.upgrade(&cx) { if let Some(container_dir) = container_dir { @@ -2862,20 +2869,17 @@ impl Project { server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), cx: &mut AsyncAppContext, - ) -> Result>> { - let setup = Self::setup_pending_language_server( + ) -> Result> { + let language_server = Self::setup_pending_language_server( this, override_initialization_options, pending_server, adapter.clone(), server_id, cx, - ); + ) + .await?; - let language_server = match setup.await? { - Some(language_server) => language_server, - None => return Ok(None), - }; let this = match this.upgrade(cx) { Some(this) => this, None => return Err(anyhow!("failed to upgrade project handle")), @@ -2892,7 +2896,7 @@ impl Project { ) })?; - Ok(Some(language_server)) + Ok(language_server) } async fn setup_pending_language_server( @@ -2902,12 +2906,9 @@ impl Project { adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, - ) -> Result>> { + ) -> Result> { let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; - let language_server = match pending_server.task.await? { - Some(server) => server, - None => return Ok(None), - }; + let language_server = pending_server.task.await?; language_server .on_notification::({ @@ -2978,6 +2979,7 @@ impl Project { }, ) .detach(); + language_server .on_request::({ move |params, mut cx| async move { @@ -3043,6 +3045,7 @@ impl Project { } }) .detach(); + let mut initialization_options = adapter.adapter.initialization_options().await; match (&mut initialization_options, override_options) { (Some(initialization_options), Some(override_options)) => { @@ -3062,7 +3065,7 @@ impl Project { ) .ok(); - Ok(Some(language_server)) + Ok(language_server) } fn insert_newly_running_language_server( From 7b4a895ab9d16ba7ab51f5568238efae32fe4c29 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 26 Oct 2023 15:41:29 +0200 Subject: [PATCH 235/274] ui2: Clean up `drain`s --- crates/ui2/src/components/chat_panel.rs | 4 ++-- crates/ui2/src/components/context_menu.rs | 5 +++-- crates/ui2/src/components/list.rs | 15 +++++---------- crates/ui2/src/components/modal.rs | 2 +- crates/ui2/src/components/palette.rs | 4 ++-- crates/ui2/src/components/panel.rs | 4 ++-- crates/ui2/src/components/panes.rs | 6 +++--- crates/ui2/src/components/toolbar.rs | 6 +++--- 8 files changed, 21 insertions(+), 25 deletions(-) diff --git a/crates/ui2/src/components/chat_panel.rs b/crates/ui2/src/components/chat_panel.rs index e4494f4614..5155d70010 100644 --- a/crates/ui2/src/components/chat_panel.rs +++ b/crates/ui2/src/components/chat_panel.rs @@ -22,7 +22,7 @@ impl ChatPanel { self } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { div() .id(self.element_id.clone()) .flex() @@ -60,7 +60,7 @@ impl ChatPanel { .flex_col() .gap_3() .overflow_y_scroll() - .children(self.messages.drain(..)), + .children(self.messages), ) // Composer .child(div().flex().my_2().child(Input::new("Message #design"))), diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 3e0323d942..206f5a2f31 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -42,7 +42,8 @@ impl ContextMenu { items: items.into_iter().collect(), } } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); v_stack() @@ -53,7 +54,7 @@ impl ContextMenu { .child( List::new( self.items - .drain(..) + .into_iter() .map(ContextMenuItem::to_list_item) .collect(), ) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 0bf413e217..c629f6b89c 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -471,7 +471,7 @@ impl ListDetailsEntry { self } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); let settings = user_settings(cx); @@ -504,14 +504,13 @@ impl ListDetailsEntry { .child(Label::new(self.label.clone()).color(label_color)) .children( self.meta - .take() .map(|meta| Label::new(meta).color(LabelColor::Muted)), ) .child( h_stack() .gap_1() .justify_end() - .children(self.actions.take().unwrap_or_default().into_iter()), + .children(self.actions.unwrap_or_default()), ) } } @@ -564,13 +563,13 @@ impl List { self } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let is_toggleable = self.toggleable != Toggleable::NotToggleable; let is_toggled = Toggleable::is_toggled(&self.toggleable); let list_content = match (self.items.is_empty(), is_toggled) { (_, false) => div(), - (false, _) => div().children(self.items.drain(..)), + (false, _) => div().children(self.items), (true, _) => { div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted)) } @@ -578,11 +577,7 @@ impl List { v_stack() .py_1() - .children( - self.header - .take() - .map(|header| header.toggleable(self.toggleable)), - ) + .children(self.header.map(|header| header.toggleable(self.toggleable))) .child(list_content) } } diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index 2036c5df11..a83d114688 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -58,7 +58,7 @@ impl Modal { .child(div().children(self.title.clone().map(|t| Label::new(t)))) .child(IconButton::new("close", Icon::Close)), ) - .child(v_stack().p_1().children(self.children.drain(..))) + .child(v_stack().p_1().children(self.children)) .when( self.primary_action.is_some() || self.secondary_action.is_some(), |this| { diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 9f2da78a53..254afd94c4 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -42,7 +42,7 @@ impl Palette { self } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); v_stack() @@ -81,7 +81,7 @@ impl Palette { .into_iter() .flatten(), ) - .children(self.items.drain(..).enumerate().map(|(index, item)| { + .children(self.items.into_iter().enumerate().map(|(index, item)| { h_stack() .id(index) .justify_between() diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index a1a075a4b2..861babf27c 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -92,7 +92,7 @@ impl Panel { self } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); let current_size = self.width.unwrap_or(self.initial_width); @@ -113,7 +113,7 @@ impl Panel { }) .bg(theme.surface) .border_color(theme.border) - .children(self.children.drain(..)) + .children(self.children) } } diff --git a/crates/ui2/src/components/panes.rs b/crates/ui2/src/components/panes.rs index a45de158bb..b692872741 100644 --- a/crates/ui2/src/components/panes.rs +++ b/crates/ui2/src/components/panes.rs @@ -96,7 +96,7 @@ impl PaneGroup { } } - fn render(mut self, view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); if !self.panes.is_empty() { @@ -106,7 +106,7 @@ impl PaneGroup { .gap_px() .w_full() .h_full() - .children(self.panes.drain(..).map(|pane| pane.render(view, cx))); + .children(self.panes.into_iter().map(|pane| pane.render(view, cx))); if self.split_direction == SplitDirection::Horizontal { return el; @@ -123,7 +123,7 @@ impl PaneGroup { .w_full() .h_full() .bg(theme.editor) - .children(self.groups.drain(..).map(|group| group.render(view, cx))); + .children(self.groups.into_iter().map(|group| group.render(view, cx))); if self.split_direction == SplitDirection::Horizontal { return el; diff --git a/crates/ui2/src/components/toolbar.rs b/crates/ui2/src/components/toolbar.rs index e32697388e..83bcde255e 100644 --- a/crates/ui2/src/components/toolbar.rs +++ b/crates/ui2/src/components/toolbar.rs @@ -54,7 +54,7 @@ impl Toolbar { self } - fn render(mut self, _view: &mut S, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut S, cx: &mut ViewContext) -> impl Component { let theme = theme(cx); div() @@ -62,8 +62,8 @@ impl Toolbar { .p_2() .flex() .justify_between() - .child(div().flex().children(self.left_items.drain(..))) - .child(div().flex().children(self.right_items.drain(..))) + .child(div().flex().children(self.left_items)) + .child(div().flex().children(self.right_items)) } } From eb19071d84993a56acbb29ef81f64d3d137fd865 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 26 Oct 2023 15:44:39 +0200 Subject: [PATCH 236/274] ui2: Clean up `take`s --- crates/ui2/src/components/list.rs | 6 +++--- crates/ui2/src/components/modal.rs | 6 +++--- crates/ui2/src/components/palette.rs | 4 ++-- crates/ui2/src/elements/details.rs | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index c629f6b89c..bfd650b390 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -250,7 +250,7 @@ impl ListItem { pub struct ListEntry { disclosure_control_style: DisclosureControlVisibility, indent_level: u32, - label: Option