From 91c32ed30728017f73dc3f7b19c690f44ed383ab Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 27 Jul 2023 11:43:32 +0200 Subject: [PATCH 001/326] WIP: project search redesign --- crates/search/src/project_search.rs | 84 +++++++++++++---------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 52ee12c26d..56c1d6f1d0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -505,6 +505,7 @@ impl ProjectSearchView { Some(Arc::new(|theme| theme.search.editor.input.clone())), cx, ); + editor.set_placeholder_text("Text search all files", cx); editor.set_text(query_text, cx); editor }); @@ -1230,31 +1231,37 @@ impl View for ProjectSearchBar { .flex(1.0, true); let row_spacing = theme.workspace.toolbar.container.padding.bottom; + let query = ChildView::new(&search.query_editor, cx) + .aligned() + .left() + .flex(1., true); + let matches = search.active_match_index.map(|match_ix| { + Label::new( + format!( + "{}/{}", + match_ix + 1, + search.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), + ) + .contained() + .with_style(theme.search.match_index.container) + .aligned() + .left() + }); + let case_button = self.render_option_button("Case", SearchOptions::CASE_SENSITIVE, cx); + let word_button = self.render_option_button("Word", SearchOptions::WHOLE_WORD, cx); + let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); + let semantic_index = + SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); Flex::column() .with_child( Flex::row() + .with_children(matches) .with_child( Flex::row() - .with_child( - ChildView::new(&search.query_editor, cx) - .aligned() - .left() - .flex(1., true), - ) - .with_children(search.active_match_index.map(|match_ix| { - Label::new( - format!( - "{}/{}", - match_ix + 1, - search.model.read(cx).match_ranges.len() - ), - theme.search.match_index.text.clone(), - ) - .contained() - .with_style(theme.search.match_index.container) - .aligned() - })) + .with_child(query) .contained() .with_style(query_container_style) .aligned() @@ -1269,35 +1276,17 @@ impl View for ProjectSearchBar { .with_child(self.render_nav_button(">", Direction::Next, cx)) .aligned(), ) - .with_child({ - let row = if SemanticIndex::enabled(cx) { - Flex::row().with_child(self.render_semantic_search_button(cx)) - } else { - Flex::row() - }; - - let row = row - .with_child(self.render_option_button( - "Case", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_child(self.render_option_button( - "Word", - SearchOptions::WHOLE_WORD, - cx, - )) - .with_child(self.render_option_button( - "Regex", - SearchOptions::REGEX, - cx, - )) + .with_child( + Flex::row() + .with_children(semantic_index) + .with_child(case_button) + .with_child(word_button) + .with_child(regex_button) .contained() .with_style(theme.search.option_button_group) - .aligned(); - - row - }) + .aligned() + .right(), + ) .contained() .with_margin_bottom(row_spacing), ) @@ -1328,8 +1317,7 @@ impl View for ProjectSearchBar { ) .contained() .with_style(theme.search.container) - .aligned() - .left() + .flex_float() .into_any_named("project search") } else { Empty::new().into_any() From f2a35a7e1dd75dd0f12a90130ce76c52b7a870a5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:18:19 +0200 Subject: [PATCH 002/326] Use a three-way layout --- crates/search/src/project_search.rs | 129 +++++++++++++++++----------- 1 file changed, 80 insertions(+), 49 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 56c1d6f1d0..1485a9359f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1254,69 +1254,100 @@ impl View for ProjectSearchBar { let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); let semantic_index = SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); - - Flex::column() + Flex::row() + .with_child(Flex::row().flex(1., true)) .with_child( - Flex::row() - .with_children(matches) + Flex::column() .with_child( Flex::row() - .with_child(query) + .with_children(matches) + .with_child( + Flex::row() + .with_child(query) + .contained() + .with_style(query_container_style) + .aligned() + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false), + ) + .with_child( + Flex::row() + .with_child(self.render_nav_button( + "<", + Direction::Prev, + cx, + )) + .with_child(self.render_nav_button( + ">", + Direction::Next, + cx, + )) + .aligned(), + ) + .with_child( + Flex::row() + .with_children(semantic_index) + .with_child(case_button) + .with_child(word_button) + .with_child(regex_button) + .contained() + .with_style(theme.search.option_button_group) + .aligned() + .right(), + ) .contained() - .with_style(query_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .flex(1., false), + .with_margin_bottom(row_spacing), ) .with_child( Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .aligned(), - ) - .with_child( - Flex::row() - .with_children(semantic_index) - .with_child(case_button) - .with_child(word_button) - .with_child(regex_button) - .contained() - .with_style(theme.search.option_button_group) - .aligned() - .right(), + .with_child( + Flex::row() + .with_child(included_files_view) + .contained() + .with_style(include_container_style) + .aligned() + .constrained() + .with_min_width( + theme.search.include_exclude_editor.min_width, + ) + .with_max_width( + theme.search.include_exclude_editor.max_width, + ) + .flex(1., false), + ) + .with_child( + Flex::row() + .with_child(excluded_files_view) + .contained() + .with_style(exclude_container_style) + .aligned() + .constrained() + .with_min_width( + theme.search.include_exclude_editor.min_width, + ) + .with_max_width( + theme.search.include_exclude_editor.max_width, + ) + .flex(1., false), + ), ) .contained() - .with_margin_bottom(row_spacing), + .with_style(theme.search.container) + .flex(2., true), ) .with_child( Flex::row() - .with_child( - Flex::row() - .with_child(included_files_view) - .contained() - .with_style(include_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(excluded_files_view) - .contained() - .with_style(exclude_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width) - .flex(1., false), - ), + .with_child(Label::new( + "Here be dragons", + theme.search.match_index.text.clone(), + )) + .flex(1., true) + .aligned() + .right(), ) .contained() - .with_style(theme.search.container) .flex_float() .into_any_named("project search") } else { From 4761197d621d0d0d92574b74dc1eacf71645cdd8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:08:31 +0200 Subject: [PATCH 003/326] Add filter button. Move semantic & regex buttons to the right hand side. Add default tab name for the new project search. --- crates/search/src/project_search.rs | 135 ++++++++++++++++++---------- 1 file changed, 86 insertions(+), 49 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1485a9359f..435b5542a9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -102,6 +102,7 @@ pub struct ProjectSearchView { query_editor_was_focused: bool, included_files_editor: ViewHandle, excluded_files_editor: ViewHandle, + filters_enabled: bool, } struct SemanticSearchState { @@ -365,11 +366,19 @@ impl Item for ProjectSearchView { .contained() .with_margin_right(tab_theme.spacing), ) - .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - let query_text = util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN); - - Label::new(query_text, tab_theme.label.clone()).aligned() - })) + .with_child({ + let tab_name: Option> = + self.model.read(cx).active_query.as_ref().map(|query| { + let query_text = + util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN); + query_text.into() + }); + Label::new( + tab_name.unwrap_or("Project search".into()), + tab_theme.label.clone(), + ) + .aligned() + }) .into_any() } @@ -565,7 +574,7 @@ impl ProjectSearchView { cx.emit(ViewEvent::EditorEvent(event.clone())) }) .detach(); - + let filters_enabled = false; let mut this = ProjectSearchView { search_id: model.read(cx).search_id, model, @@ -578,6 +587,7 @@ impl ProjectSearchView { query_editor_was_focused: false, included_files_editor, excluded_files_editor, + filters_enabled, }; this.model_changed(cx); this @@ -1012,6 +1022,19 @@ impl ProjectSearchBar { false } } + fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.filters_enabled = !search_view.filters_enabled; + search_view.semantic = None; + search_view.search(cx); + }); + cx.notify(); + true + } else { + false + } + } fn toggle_semantic_search(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { @@ -1249,9 +1272,56 @@ impl View for ProjectSearchBar { .aligned() .left() }); + let filters = search.filters_enabled.then(|| { + Flex::row() + .with_child( + Flex::row() + .with_child(included_files_view) + .contained() + .with_style(include_container_style) + .aligned() + .constrained() + .with_min_width(theme.search.include_exclude_editor.min_width) + .with_max_width(theme.search.include_exclude_editor.max_width) + .flex(1., false), + ) + .with_child( + Flex::row() + .with_child(excluded_files_view) + .contained() + .with_style(exclude_container_style) + .aligned() + .constrained() + .with_min_width(theme.search.include_exclude_editor.min_width) + .with_max_width(theme.search.include_exclude_editor.max_width) + .flex(1., false), + ) + }); + let filter_button = { + let tooltip_style = theme::current(cx).tooltip.clone(); + let is_active = search.filters_enabled; + MouseEventHandler::::new(0, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); + Label::new("Filter", style.text.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_filters(cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::(0, "Toggle filters".into(), None, tooltip_style, cx) + .into_any() + }; let case_button = self.render_option_button("Case", SearchOptions::CASE_SENSITIVE, cx); let word_button = self.render_option_button("Word", SearchOptions::WHOLE_WORD, cx); let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); + let semantic_index = SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); Flex::row() @@ -1288,10 +1358,9 @@ impl View for ProjectSearchBar { ) .with_child( Flex::row() - .with_children(semantic_index) .with_child(case_button) .with_child(word_button) - .with_child(regex_button) + .with_child(filter_button) .contained() .with_style(theme.search.option_button_group) .aligned() @@ -1300,52 +1369,20 @@ impl View for ProjectSearchBar { .contained() .with_margin_bottom(row_spacing), ) - .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child(included_files_view) - .contained() - .with_style(include_container_style) - .aligned() - .constrained() - .with_min_width( - theme.search.include_exclude_editor.min_width, - ) - .with_max_width( - theme.search.include_exclude_editor.max_width, - ) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(excluded_files_view) - .contained() - .with_style(exclude_container_style) - .aligned() - .constrained() - .with_min_width( - theme.search.include_exclude_editor.min_width, - ) - .with_max_width( - theme.search.include_exclude_editor.max_width, - ) - .flex(1., false), - ), - ) + .with_children(filters) .contained() .with_style(theme.search.container) .flex(2., true), ) .with_child( - Flex::row() - .with_child(Label::new( - "Here be dragons", - theme.search.match_index.text.clone(), - )) - .flex(1., true) - .aligned() - .right(), + Flex::column().with_child( + Flex::row() + .with_children(semantic_index) + .with_child(regex_button) + .flex(1., true) + .aligned() + .right(), + ), ) .contained() .flex_float() From 8ca1e0b15b16217940627a10e9c5d43dd49aae97 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:09:19 +0200 Subject: [PATCH 004/326] Add dummy filter icon --- assets/icons/filter_12.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 assets/icons/filter_12.svg diff --git a/assets/icons/filter_12.svg b/assets/icons/filter_12.svg new file mode 100644 index 0000000000..9c1ad5ba5c --- /dev/null +++ b/assets/icons/filter_12.svg @@ -0,0 +1,3 @@ + + + From dff9bf7d7eb4ccea9504b73f7bf14235b0391a16 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:03:26 +0200 Subject: [PATCH 005/326] Make row_count of toolbaritem dynamic (WIP). Move result count to the left hand side. --- crates/search/src/project_search.rs | 23 ++++++++++++++++++----- crates/workspace/src/toolbar.rs | 4 ++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 435b5542a9..9b6b8da320 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -374,7 +374,9 @@ impl Item for ProjectSearchView { query_text.into() }); Label::new( - tab_name.unwrap_or("Project search".into()), + tab_name + .filter(|name| !name.is_empty()) + .unwrap_or("Project search".into()), tab_theme.label.clone(), ) .aligned() @@ -425,6 +427,7 @@ impl Item for ProjectSearchView { project: ModelHandle, cx: &mut ViewContext, ) -> Task> { + self.results_editor .update(cx, |editor, cx| editor.reload(project, cx)) } @@ -825,6 +828,7 @@ impl ProjectSearchView { } fn model_changed(&mut self, cx: &mut ViewContext) { + let match_ranges = self.model.read(cx).match_ranges.clone(); if match_ranges.is_empty() { self.active_match_index = None; @@ -1026,8 +1030,11 @@ impl ProjectSearchBar { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { search_view.filters_enabled = !search_view.filters_enabled; + search_view.included_files_editor.update(cx, |_, cx| {cx.notify()}); + search_view.excluded_files_editor.update(cx, |_, cx| {cx.notify()}); search_view.semantic = None; search_view.search(cx); + cx.notify(); }); cx.notify(); true @@ -1325,12 +1332,12 @@ impl View for ProjectSearchBar { let semantic_index = SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); Flex::row() - .with_child(Flex::row().flex(1., true)) + .with_child(Flex::column().with_child(Flex::row().with_children(matches).aligned() + .left()).flex(1., true)) .with_child( Flex::column() .with_child( Flex::row() - .with_children(matches) .with_child( Flex::row() .with_child(query) @@ -1413,8 +1420,14 @@ impl ToolbarItemView for ProjectSearchBar { } } - fn row_count(&self) -> usize { - 2 + fn row_count(&self, cx: &ViewContext) -> usize { + self.active_project_search + .as_ref() + .map(|search| { + let offset = search.read(cx).filters_enabled as usize; + 1 + offset + }) + .unwrap_or_else(|| 1) } } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 69394b8421..3fa37f3666 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -25,7 +25,7 @@ pub trait ToolbarItemView: View { /// Number of times toolbar's height will be repeated to get the effective height. /// Useful when multiple rows one under each other are needed. /// The rows have the same width and act as a whole when reacting to resizes and similar events. - fn row_count(&self) -> usize { + fn row_count(&self, _cx: &ViewContext) -> usize { 1 } } @@ -362,7 +362,7 @@ impl ToolbarItemViewHandle for ViewHandle { } fn row_count(&self, cx: &WindowContext) -> usize { - self.read(cx).row_count() + self.read_with(cx, |this, cx| this.row_count(cx)) } } From b9cdb851d2f448f72bf57b322daf1b0797141a30 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:31:24 +0200 Subject: [PATCH 006/326] Update results text --- crates/search/src/project_search.rs | 31 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 9b6b8da320..1321aadb4f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -278,17 +278,23 @@ impl View for ProjectSearchView { Cow::Borrowed("Indexing complete") } } else if self.query_editor.read(cx).text(cx).is_empty() { - Cow::Borrowed("") + Cow::Borrowed("Text search all files and folders") } else { Cow::Borrowed("No results") }; MouseEventHandler::::new(0, cx, |_, _| { - Label::new(text, theme.search.results_status.clone()) - .aligned() + Flex::column() + .with_child(Flex::column().contained().flex(1., true)) + .with_child( + Label::new(text, theme.search.results_status.clone()) + .aligned() + .top() + .contained() + .flex(7., true), + ) .contained() .with_background_color(theme.editor.background) - .flex(1., true) }) .on_down(MouseButton::Left, |_, _, cx| { cx.focus_parent(); @@ -427,7 +433,6 @@ impl Item for ProjectSearchView { project: ModelHandle, cx: &mut ViewContext, ) -> Task> { - self.results_editor .update(cx, |editor, cx| editor.reload(project, cx)) } @@ -828,7 +833,6 @@ impl ProjectSearchView { } fn model_changed(&mut self, cx: &mut ViewContext) { - let match_ranges = self.model.read(cx).match_ranges.clone(); if match_ranges.is_empty() { self.active_match_index = None; @@ -1030,8 +1034,12 @@ impl ProjectSearchBar { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { search_view.filters_enabled = !search_view.filters_enabled; - search_view.included_files_editor.update(cx, |_, cx| {cx.notify()}); - search_view.excluded_files_editor.update(cx, |_, cx| {cx.notify()}); + search_view + .included_files_editor + .update(cx, |_, cx| cx.notify()); + search_view + .excluded_files_editor + .update(cx, |_, cx| cx.notify()); search_view.semantic = None; search_view.search(cx); cx.notify(); @@ -1332,8 +1340,11 @@ impl View for ProjectSearchBar { let semantic_index = SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); Flex::row() - .with_child(Flex::column().with_child(Flex::row().with_children(matches).aligned() - .left()).flex(1., true)) + .with_child( + Flex::column() + .with_child(Flex::row().with_children(matches).aligned().left()) + .flex(1., true), + ) .with_child( Flex::column() .with_child( From ac35dae66ec56b18e20de5665c8b48c747ec190d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 18 Jul 2023 18:55:54 -0700 Subject: [PATCH 007/326] Add channels panel with stubbed out information co-authored-by: nate --- Cargo.lock | 26 ++ Cargo.toml | 1 + assets/settings/default.json | 6 + crates/channels/Cargo.toml | 38 ++ crates/channels/src/channels.rs | 103 +++++ crates/channels/src/channels_panel.rs | 369 ++++++++++++++++++ .../channels/src/channels_panel_settings.rs | 37 ++ crates/gpui/src/elements/flex.rs | 7 + crates/project_panel/src/project_panel.rs | 24 -- crates/theme/src/theme.rs | 80 ++++ crates/theme/src/ui.rs | 10 + crates/workspace/src/dock.rs | 28 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 17 +- styles/src/style_tree/app.ts | 2 + styles/src/style_tree/channels_panel.ts | 68 ++++ 17 files changed, 784 insertions(+), 34 deletions(-) create mode 100644 crates/channels/Cargo.toml create mode 100644 crates/channels/src/channels.rs create mode 100644 crates/channels/src/channels_panel.rs create mode 100644 crates/channels/src/channels_panel_settings.rs create mode 100644 styles/src/style_tree/channels_panel.ts diff --git a/Cargo.lock b/Cargo.lock index 535c20bcb9..e0a4b6a7bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1254,6 +1254,31 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "channels" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "context_menu", + "db", + "editor", + "futures 0.3.28", + "gpui", + "log", + "menu", + "project", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "chrono" version = "0.4.26" @@ -9857,6 +9882,7 @@ dependencies = [ "backtrace", "breadcrumbs", "call", + "channels", "chrono", "cli", "client", diff --git a/Cargo.toml b/Cargo.toml index 6e79c6b657..8803d1c34b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/auto_update", "crates/breadcrumbs", "crates/call", + "crates/channels", "crates/cli", "crates/client", "crates/clock", diff --git a/assets/settings/default.json b/assets/settings/default.json index 397dac0961..c40ed4e8da 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -122,6 +122,12 @@ // Amount of indentation for nested items. "indent_size": 20 }, + "channels_panel": { + // Where to dock channels panel. Can be 'left' or 'right'. + "dock": "left", + // Default width of the channels panel. + "default_width": 240 + }, "assistant": { // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. "dock": "right", diff --git a/crates/channels/Cargo.toml b/crates/channels/Cargo.toml new file mode 100644 index 0000000000..7507072130 --- /dev/null +++ b/crates/channels/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "channels" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/channels.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +context_menu = { path = "../context_menu" } +client = { path = "../client" } +db = { path = "../db" } +editor = { path = "../editor" } +gpui = { path = "../gpui" } +project = { path = "../project" } +theme = { path = "../theme" } +settings = { path = "../settings" } +workspace = { path = "../workspace" } +menu = { path = "../menu" } +util = { path = "../util" } + +log.workspace = true +anyhow.workspace = true +schemars.workspace = true +serde_json.workspace = true +serde.workspace = true +serde_derive.workspace = true +futures.workspace = true + +[dev-dependencies] +client = { path = "../client", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } +serde_json.workspace = true diff --git a/crates/channels/src/channels.rs b/crates/channels/src/channels.rs new file mode 100644 index 0000000000..8e55441b29 --- /dev/null +++ b/crates/channels/src/channels.rs @@ -0,0 +1,103 @@ +mod channels_panel; +mod channels_panel_settings; + +pub use channels_panel::*; +use gpui::{AppContext, Entity}; + +use std::sync::Arc; + +use client::Client; + +pub fn init(client: Arc, cx: &mut AppContext) { + let channels = cx.add_model(|cx| Channels::new(client, cx)); + cx.set_global(channels); + channels_panel::init(cx); +} + +#[derive(Debug, Clone)] +struct Channel { + id: u64, + name: String, + sub_channels: Vec, + _room: Option<()>, +} + +impl Channel { + fn new(id: u64, name: impl AsRef, members: Vec) -> Channel { + Channel { + name: name.as_ref().to_string(), + id, + sub_channels: members, + _room: None, + } + } + + fn members(&self) -> &[Channel] { + &self.sub_channels + } + + fn name(&self) -> &str { + &self.name + } +} + +struct Channels { + channels: Vec, +} + +impl Channels { + fn channels(&self) -> Vec { + self.channels.clone() + } +} + +enum ChannelEvents {} + +impl Entity for Channels { + type Event = ChannelEvents; +} + +impl Channels { + fn new(_client: Arc, _cx: &mut AppContext) -> Self { + //TODO: Subscribe to channel updates from the server + Channels { + channels: vec![Channel::new( + 0, + "Zed Industries", + vec![ + Channel::new(1, "#general", Vec::new()), + Channel::new(2, "#admiral", Vec::new()), + Channel::new(3, "#livestreaming", vec![]), + Channel::new(4, "#crdb", Vec::new()), + Channel::new(5, "#crdb-1", Vec::new()), + Channel::new(6, "#crdb-2", Vec::new()), + Channel::new(7, "#crdb-3", vec![]), + Channel::new(8, "#crdb-4", Vec::new()), + Channel::new(9, "#crdb-1", Vec::new()), + Channel::new(10, "#crdb-1", Vec::new()), + Channel::new(11, "#crdb-1", Vec::new()), + Channel::new(12, "#crdb-1", vec![]), + Channel::new(13, "#crdb-1", Vec::new()), + Channel::new(14, "#crdb-1", Vec::new()), + Channel::new(15, "#crdb-1", Vec::new()), + Channel::new(16, "#crdb-1", Vec::new()), + Channel::new(17, "#crdb", vec![]), + ], + ), + Channel::new( + 18, + "CRDB Consulting", + vec![ + Channel::new(19, "#crdb 😭", Vec::new()), + Channel::new(20, "#crdb 😌", Vec::new()), + Channel::new(21, "#crdb 🦀", vec![]), + Channel::new(22, "#crdb 😤", Vec::new()), + Channel::new(23, "#crdb 😤", Vec::new()), + Channel::new(24, "#crdb 😤", Vec::new()), + Channel::new(25, "#crdb 😤", vec![]), + Channel::new(26, "#crdb 😤", Vec::new()), + ], + )], + } + } +} diff --git a/crates/channels/src/channels_panel.rs b/crates/channels/src/channels_panel.rs new file mode 100644 index 0000000000..73697b3b72 --- /dev/null +++ b/crates/channels/src/channels_panel.rs @@ -0,0 +1,369 @@ +use std::sync::Arc; + +use crate::{ + channels_panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}, + Channel, Channels, +}; +use anyhow::Result; +use collections::HashMap; +use context_menu::ContextMenu; +use db::kvp::KEY_VALUE_STORE; +use gpui::{ + actions, + elements::{ChildView, Empty, Flex, Label, MouseEventHandler, ParentElement, Stack}, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Task, View, + ViewContext, ViewHandle, WeakViewHandle, +}; +use project::Fs; +use serde_derive::{Deserialize, Serialize}; +use settings::SettingsStore; +use theme::ChannelTreeStyle; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; + +actions!(channels, [ToggleFocus]); + +const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; + +pub fn init(cx: &mut AppContext) { + settings::register::(cx); +} + +pub struct ChannelsPanel { + width: Option, + fs: Arc, + has_focus: bool, + pending_serialization: Task>, + channels: ModelHandle, + context_menu: ViewHandle, + collapsed_channels: HashMap, +} + +#[derive(Serialize, Deserialize)] +struct SerializedChannelsPanel { + width: Option, + collapsed_channels: Option>, +} + +#[derive(Debug)] +pub enum Event { + DockPositionChanged, + Focus, +} + +impl Entity for ChannelsPanel { + type Event = Event; +} + +impl ChannelsPanel { + pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { + cx.add_view(|cx| { + let view_id = cx.view_id(); + let this = Self { + width: None, + has_focus: false, + fs: workspace.app_state().fs.clone(), + pending_serialization: Task::ready(None), + channels: cx.global::>().clone(), + context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + collapsed_channels: HashMap::default(), + }; + + // Update the dock position when the setting changes. + let mut old_dock_position = this.position(cx); + cx.observe_global::(move |this: &mut ChannelsPanel, 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); + } + }) + .detach(); + + 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(CHANNELS_PANEL_KEY) }) + .await + .log_err() + .flatten() + { + Some(serde_json::from_str::(&panel)?) + } else { + None + }; + + workspace.update(&mut cx, |workspace, cx| { + let panel = ChannelsPanel::new(workspace, cx); + if let Some(serialized_panel) = serialized_panel { + panel.update(cx, |panel, cx| { + panel.width = serialized_panel.width; + panel.collapsed_channels = + serialized_panel.collapsed_channels.unwrap_or_default(); + cx.notify(); + }); + } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + let collapsed_channels = self.collapsed_channels.clone(); + self.pending_serialization = cx.background().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + CHANNELS_PANEL_KEY.into(), + serde_json::to_string(&SerializedChannelsPanel { + width, + collapsed_channels: Some(collapsed_channels), + })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + + fn render_channel( + &mut self, + depth: usize, + channel: &Channel, + style: &ChannelTreeStyle, + root: bool, + cx: &mut ViewContext, + ) -> AnyElement { + let has_chilren = !channel.members().is_empty(); + + let sub_channel_details = has_chilren.then(|| { + let mut sub_channels = Flex::column(); + let collapsed = self + .collapsed_channels + .get(&channel.id) + .copied() + .unwrap_or_default(); + if !collapsed { + for sub_channel in channel.members() { + sub_channels = sub_channels.with_child(self.render_channel( + depth + 1, + sub_channel, + style, + false, + cx, + )); + } + } + (sub_channels, collapsed) + }); + + let channel_id = channel.id; + + enum ChannelCollapser {} + Flex::row() + .with_child( + Empty::new() + .constrained() + .with_width(depth as f32 * style.channel_indent), + ) + .with_child( + Flex::column() + .with_child( + Flex::row() + .with_child( + sub_channel_details + .as_ref() + .map(|(_, expanded)| { + MouseEventHandler::::new( + channel.id as usize, + cx, + |state, _cx| { + let icon = + style.channel_icon.style_for(!*expanded, state); + theme::ui::icon(icon) + }, + ) + .on_click( + gpui::platform::MouseButton::Left, + move |_, v, cx| { + let entry = v + .collapsed_channels + .entry(channel_id) + .or_default(); + *entry = !*entry; + v.serialize(cx); + cx.notify(); + }, + ) + .into_any() + }) + .unwrap_or_else(|| { + Empty::new() + .constrained() + .with_width(style.channel_icon.default_style().width()) + .into_any() + }), + ) + .with_child( + Label::new( + channel.name().to_string(), + if root { + style.root_name.clone() + } else { + style.channel_name.clone() + }, + ) + .into_any(), + ), + ) + .with_children(sub_channel_details.map(|(elements, _)| elements)), + ) + .into_any() + } +} + +impl View for ChannelsPanel { + fn ui_name() -> &'static str { + "ChannelsPanel" + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } + + fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { + let theme = theme::current(cx).clone(); + + let mut channels_column = Flex::column(); + for channel in self.channels.read(cx).channels() { + channels_column = channels_column.with_child(self.render_channel( + 0, + &channel, + &theme.channels_panel.channel_tree, + true, + cx, + )); + } + + let spacing = theme.channels_panel.spacing; + + enum ChannelsPanelScrollTag {} + Stack::new() + .with_child( + // Full panel column + Flex::column() + .with_spacing(spacing) + .with_child( + // Channels section column + Flex::column() + .with_child( + Flex::row().with_child( + Label::new( + "Active Channels", + theme.editor.invalid_information_diagnostic.message.clone(), + ) + .into_any(), + ), + ) + // Channels list column + .with_child(channels_column), + ) + // TODO: Replace with spacing implementation + .with_child(Empty::new().constrained().with_height(spacing)) + .with_child( + Flex::column().with_child( + Flex::row().with_child( + Label::new( + "Contacts", + theme.editor.invalid_information_diagnostic.message.clone(), + ) + .into_any(), + ), + ), + ) + .scrollable::(0, None, cx) + .expanded(), + ) + .with_child(ChildView::new(&self.context_menu, cx)) + .into_any_named("channels panel") + .into_any() + } +} + +impl Panel for ChannelsPanel { + fn position(&self, cx: &gpui::WindowContext) -> DockPosition { + match settings::get::(cx).dock { + ChannelsPanelDockPosition::Left => DockPosition::Left, + ChannelsPanelDockPosition::Right => DockPosition::Right, + } + } + + 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| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left, + DockPosition::Right => ChannelsPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); + } + + fn size(&self, cx: &gpui::WindowContext) -> f32 { + self.width + .unwrap_or_else(|| settings::get::(cx).default_width) + } + + fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + self.width = Some(size); + self.serialize(cx); + cx.notify(); + } + + fn icon_path(&self) -> &'static str { + "icons/bolt_16.svg" + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Channels Panel".to_string(), Some(Box::new(ToggleFocus))) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, Event::DockPositionChanged) + } + + fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, Event::Focus) + } +} diff --git a/crates/channels/src/channels_panel_settings.rs b/crates/channels/src/channels_panel_settings.rs new file mode 100644 index 0000000000..fe3484b782 --- /dev/null +++ b/crates/channels/src/channels_panel_settings.rs @@ -0,0 +1,37 @@ +use anyhow; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ChannelsPanelDockPosition { + Left, + Right, +} + +#[derive(Deserialize, Debug)] +pub struct ChannelsPanelSettings { + pub dock: ChannelsPanelDockPosition, + pub default_width: f32, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct ChannelsPanelSettingsContent { + pub dock: Option, + pub default_width: Option, +} + +impl Setting for ChannelsPanelSettings { + const KEY: Option<&'static str> = Some("channels_panel"); + + type FileContent = ChannelsPanelSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 857f3f56fc..40959c8f5c 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,6 +22,7 @@ pub struct Flex { children: Vec>, scroll_state: Option<(ElementStateHandle>, usize)>, child_alignment: f32, + spacing: f32, } impl Flex { @@ -31,6 +32,7 @@ impl Flex { children: Default::default(), scroll_state: None, child_alignment: -1., + spacing: 0., } } @@ -42,6 +44,11 @@ impl Flex { Self::new(Axis::Vertical) } + pub fn with_spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + /// Render children centered relative to the cross-axis of the parent flex. /// /// If this is a flex row, children will be centered vertically. If this is a diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e6e1cff598..67a23f8d77 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1649,22 +1649,6 @@ impl workspace::dock::Panel for ProjectPanel { cx.notify(); } - fn should_zoom_in_on_event(_: &Self::Event) -> bool { - false - } - - fn should_zoom_out_on_event(_: &Self::Event) -> bool { - false - } - - fn is_zoomed(&self, _: &WindowContext) -> bool { - false - } - - fn set_zoomed(&mut self, _: bool, _: &mut ViewContext) {} - - fn set_active(&mut self, _: bool, _: &mut ViewContext) {} - fn icon_path(&self) -> &'static str { "icons/folder_tree_16.svg" } @@ -1677,14 +1661,6 @@ impl workspace::dock::Panel for ProjectPanel { matches!(event, Event::DockPositionChanged) } - fn should_activate_on_event(_: &Self::Event) -> bool { - false - } - - fn should_close_on_event(_: &Self::Event) -> bool { - false - } - fn has_focus(&self, _: &WindowContext) -> bool { self.has_focus } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4766f636f3..844b093a5e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -49,6 +49,7 @@ pub struct Theme { pub copilot: Copilot, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, + pub channels_panel: ChanelsPanelStyle, pub command_palette: CommandPalette, pub picker: Picker, pub editor: Editor, @@ -880,6 +881,16 @@ impl Interactive { } } +impl Toggleable> { + pub fn style_for(&self, active: bool, state: &mut MouseState) -> &T { + self.in_state(active).style_for(state) + } + + pub fn default_style(&self) -> &T { + &self.inactive.default + } +} + impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { fn deserialize(deserializer: D) -> Result where @@ -1045,6 +1056,75 @@ pub struct AssistantStyle { pub saved_conversation: SavedConversation, } +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct Contained { + container: ContainerStyle, + contained: T, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct FlexStyle { + // Between item spacing + item_spacing: f32, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ChannelProjectStyle { + // TODO: Implement Contained Flex + // ContainerStyle + Spacing between elements + // Negative spacing overlaps elements instead of spacing them out + pub container: Contained, + pub host: ImageStyle, + pub title: ContainedText, + pub members: Contained, + pub member: ImageStyle +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ChanneltemStyle { + pub icon: IconStyle, + pub title: TextStyle, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ChannelListStyle { + pub section_title: ContainedText, + pub channel: Toggleable>, + pub project: ChannelProjectStyle +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ContactItemStyle { + pub container: Contained, + pub avatar: IconStyle, + pub name: TextStyle, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ContactsListStyle { + pub section_title: ContainedText, + pub contact: ContactItemStyle, +} + + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ChannelTreeStyle { + pub channel_indent: f32, + pub channel_name: TextStyle, + pub root_name: TextStyle, + pub channel_icon: Toggleable>, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct ChanelsPanelStyle { + pub channel_tree: ChannelTreeStyle, + pub spacing: f32, + // TODO: Uncomment: + // pub container: ContainerStyle, + // pub channel_list: ChannelListStyle, + // pub contacts_list: ContactsListStyle +} + #[derive(Clone, Deserialize, Default, JsonSchema)] pub struct SavedConversation { pub container: Interactive, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 308ea6f2d7..76f6883f0e 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -107,6 +107,16 @@ pub struct IconStyle { pub container: ContainerStyle, } +impl IconStyle { + pub fn width(&self) -> f32 { + self.icon.dimensions.width + + self.container.padding.left + + self.container.padding.right + + self.container.margin.left + + self.container.margin.right + } +} + pub fn icon(style: &IconStyle) -> Container { svg(&style.icon).contained().with_style(style.container) } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ebaf399e22..3b0dc81920 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -20,13 +20,27 @@ pub trait Panel: View { None } fn should_change_position_on_event(_: &Self::Event) -> bool; - fn should_zoom_in_on_event(_: &Self::Event) -> bool; - fn should_zoom_out_on_event(_: &Self::Event) -> bool; - fn is_zoomed(&self, cx: &WindowContext) -> bool; - fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext); - fn set_active(&mut self, active: bool, cx: &mut ViewContext); - fn should_activate_on_event(_: &Self::Event) -> bool; - fn should_close_on_event(_: &Self::Event) -> bool; + fn should_zoom_in_on_event(_: &Self::Event) -> bool { + false + } + fn should_zoom_out_on_event(_: &Self::Event) -> bool { + false + } + fn is_zoomed(&self, _cx: &WindowContext) -> bool { + false + } + fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) { + + } + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) { + + } + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + fn should_close_on_event(_: &Self::Event) -> bool { + false + } fn has_focus(&self, cx: &WindowContext) -> bool; fn is_focus_event(_: &Self::Event) -> bool; } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a5877aaccb..71d8461b01 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,6 +21,7 @@ activity_indicator = { path = "../activity_indicator" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } +channels = { path = "../channels" } cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e44ab3e33a..5739052b67 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -155,6 +155,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); + channels::init(client.clone(), cx); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4b0bf1cd4c..c1046c0995 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -9,6 +9,7 @@ use ai::AssistantPanel; use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; +use channels::ChannelsPanel; pub use client; use collab_ui::{CollabTitlebarItem, ToggleContactsMenu}; use collections::VecDeque; @@ -221,6 +222,11 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action( + |workspace: &mut Workspace, _: &channels::ToggleFocus, cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ); cx.add_action( |workspace: &mut Workspace, _: &terminal_panel::ToggleFocus, @@ -339,9 +345,13 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - let (project_panel, terminal_panel, assistant_panel) = - futures::try_join!(project_panel, terminal_panel, assistant_panel)?; - + let channels_panel = ChannelsPanel::load(workspace_handle.clone(), cx.clone()); + let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!( + project_panel, + terminal_panel, + assistant_panel, + channels_panel + )?; workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); workspace.add_panel_with_extra_event_handler( @@ -359,6 +369,7 @@ pub fn initialize_workspace( ); workspace.add_panel(terminal_panel, cx); workspace.add_panel(assistant_panel, cx); + workspace.add_panel(channels_panel, cx); if !was_deserialized && workspace diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index ee0aa133a0..d504f8e623 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -24,6 +24,7 @@ import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" import { useTheme } from "../common" +import channels_panel from "./channels_panel" export default function app(): any { const theme = useTheme() @@ -46,6 +47,7 @@ export default function app(): any { editor: editor(), project_diagnostics: project_diagnostics(), project_panel: project_panel(), + channels_panel: channels_panel(), contacts_popover: contacts_popover(), contact_finder: contact_finder(), contact_list: contact_list(), diff --git a/styles/src/style_tree/channels_panel.ts b/styles/src/style_tree/channels_panel.ts new file mode 100644 index 0000000000..b46db5dc38 --- /dev/null +++ b/styles/src/style_tree/channels_panel.ts @@ -0,0 +1,68 @@ +// import { with_opacity } from "../theme/color" +import { + // Border, + // TextStyle, + // background, + // border, + foreground, + text, +} from "./components" +import { interactive, toggleable } from "../element" +// import merge from "ts-deepmerge" +import { useTheme } from "../theme" +export default function channels_panel(): any { + const theme = useTheme() + + // const { is_light } = theme + + return { + spacing: 10, + channel_tree: { + channel_indent: 10, + channel_name: text(theme.middle, "sans", "variant", { size: "md" }), + root_name: text(theme.middle, "sans", "variant", { size: "lg", weight: "bold" }), + channel_icon: (() => { + const base_icon = (asset: any, color: any) => { + return { + icon: { + color, + asset, + dimensions: { + width: 12, + height: 12, + } + }, + container: { + corner_radius: 4, + padding: { + top: 4, bottom: 4, left: 4, right: 4 + }, + margin: { + right: 4, + }, + } + } + } + + return toggleable({ + state: { + inactive: interactive({ + state: { + default: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "variant")), + hovered: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "hovered")), + clicked: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "active")), + }, + }), + active: interactive({ + state: { + default: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "variant")), + hovered: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "hovered")), + clicked: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "active")), + }, + }), + }, + }) + })(), + } + } +} From fe5db3035f354ac3898661fac3789e489d5c9b22 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 24 Jul 2023 12:14:15 -0700 Subject: [PATCH 008/326] move channels UI code to channels-rpc --- crates/channels/src/channels.rs | 92 +--------------- crates/channels/src/channels_panel.rs | 140 +----------------------- crates/gpui/src/elements/flex.rs | 7 -- crates/theme/src/theme.rs | 54 +-------- styles/src/style_tree/channels_panel.ts | 58 +--------- 5 files changed, 7 insertions(+), 344 deletions(-) diff --git a/crates/channels/src/channels.rs b/crates/channels/src/channels.rs index 8e55441b29..7560a36015 100644 --- a/crates/channels/src/channels.rs +++ b/crates/channels/src/channels.rs @@ -2,102 +2,12 @@ mod channels_panel; mod channels_panel_settings; pub use channels_panel::*; -use gpui::{AppContext, Entity}; +use gpui::{AppContext}; use std::sync::Arc; use client::Client; pub fn init(client: Arc, cx: &mut AppContext) { - let channels = cx.add_model(|cx| Channels::new(client, cx)); - cx.set_global(channels); channels_panel::init(cx); } - -#[derive(Debug, Clone)] -struct Channel { - id: u64, - name: String, - sub_channels: Vec, - _room: Option<()>, -} - -impl Channel { - fn new(id: u64, name: impl AsRef, members: Vec) -> Channel { - Channel { - name: name.as_ref().to_string(), - id, - sub_channels: members, - _room: None, - } - } - - fn members(&self) -> &[Channel] { - &self.sub_channels - } - - fn name(&self) -> &str { - &self.name - } -} - -struct Channels { - channels: Vec, -} - -impl Channels { - fn channels(&self) -> Vec { - self.channels.clone() - } -} - -enum ChannelEvents {} - -impl Entity for Channels { - type Event = ChannelEvents; -} - -impl Channels { - fn new(_client: Arc, _cx: &mut AppContext) -> Self { - //TODO: Subscribe to channel updates from the server - Channels { - channels: vec![Channel::new( - 0, - "Zed Industries", - vec![ - Channel::new(1, "#general", Vec::new()), - Channel::new(2, "#admiral", Vec::new()), - Channel::new(3, "#livestreaming", vec![]), - Channel::new(4, "#crdb", Vec::new()), - Channel::new(5, "#crdb-1", Vec::new()), - Channel::new(6, "#crdb-2", Vec::new()), - Channel::new(7, "#crdb-3", vec![]), - Channel::new(8, "#crdb-4", Vec::new()), - Channel::new(9, "#crdb-1", Vec::new()), - Channel::new(10, "#crdb-1", Vec::new()), - Channel::new(11, "#crdb-1", Vec::new()), - Channel::new(12, "#crdb-1", vec![]), - Channel::new(13, "#crdb-1", Vec::new()), - Channel::new(14, "#crdb-1", Vec::new()), - Channel::new(15, "#crdb-1", Vec::new()), - Channel::new(16, "#crdb-1", Vec::new()), - Channel::new(17, "#crdb", vec![]), - ], - ), - Channel::new( - 18, - "CRDB Consulting", - vec![ - Channel::new(19, "#crdb 😭", Vec::new()), - Channel::new(20, "#crdb 😌", Vec::new()), - Channel::new(21, "#crdb 🦀", vec![]), - Channel::new(22, "#crdb 😤", Vec::new()), - Channel::new(23, "#crdb 😤", Vec::new()), - Channel::new(24, "#crdb 😤", Vec::new()), - Channel::new(25, "#crdb 😤", vec![]), - Channel::new(26, "#crdb 😤", Vec::new()), - ], - )], - } - } -} diff --git a/crates/channels/src/channels_panel.rs b/crates/channels/src/channels_panel.rs index 73697b3b72..063f652191 100644 --- a/crates/channels/src/channels_panel.rs +++ b/crates/channels/src/channels_panel.rs @@ -1,23 +1,19 @@ use std::sync::Arc; -use crate::{ - channels_panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}, - Channel, Channels, -}; +use crate::channels_panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}; use anyhow::Result; use collections::HashMap; use context_menu::ContextMenu; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, - elements::{ChildView, Empty, Flex, Label, MouseEventHandler, ParentElement, Stack}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + elements::{ChildView, Flex, Label, ParentElement, Stack}, + serde_json, AppContext, AsyncAppContext, Element, Entity, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use theme::ChannelTreeStyle; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -37,7 +33,6 @@ pub struct ChannelsPanel { fs: Arc, has_focus: bool, pending_serialization: Task>, - channels: ModelHandle, context_menu: ViewHandle, collapsed_channels: HashMap, } @@ -67,7 +62,6 @@ impl ChannelsPanel { has_focus: false, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), - channels: cx.global::>().clone(), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), collapsed_channels: HashMap::default(), }; @@ -138,101 +132,6 @@ impl ChannelsPanel { .log_err(), ); } - - fn render_channel( - &mut self, - depth: usize, - channel: &Channel, - style: &ChannelTreeStyle, - root: bool, - cx: &mut ViewContext, - ) -> AnyElement { - let has_chilren = !channel.members().is_empty(); - - let sub_channel_details = has_chilren.then(|| { - let mut sub_channels = Flex::column(); - let collapsed = self - .collapsed_channels - .get(&channel.id) - .copied() - .unwrap_or_default(); - if !collapsed { - for sub_channel in channel.members() { - sub_channels = sub_channels.with_child(self.render_channel( - depth + 1, - sub_channel, - style, - false, - cx, - )); - } - } - (sub_channels, collapsed) - }); - - let channel_id = channel.id; - - enum ChannelCollapser {} - Flex::row() - .with_child( - Empty::new() - .constrained() - .with_width(depth as f32 * style.channel_indent), - ) - .with_child( - Flex::column() - .with_child( - Flex::row() - .with_child( - sub_channel_details - .as_ref() - .map(|(_, expanded)| { - MouseEventHandler::::new( - channel.id as usize, - cx, - |state, _cx| { - let icon = - style.channel_icon.style_for(!*expanded, state); - theme::ui::icon(icon) - }, - ) - .on_click( - gpui::platform::MouseButton::Left, - move |_, v, cx| { - let entry = v - .collapsed_channels - .entry(channel_id) - .or_default(); - *entry = !*entry; - v.serialize(cx); - cx.notify(); - }, - ) - .into_any() - }) - .unwrap_or_else(|| { - Empty::new() - .constrained() - .with_width(style.channel_icon.default_style().width()) - .into_any() - }), - ) - .with_child( - Label::new( - channel.name().to_string(), - if root { - style.root_name.clone() - } else { - style.channel_name.clone() - }, - ) - .into_any(), - ), - ) - .with_children(sub_channel_details.map(|(elements, _)| elements)), - ) - .into_any() - } } impl View for ChannelsPanel { @@ -254,42 +153,11 @@ impl View for ChannelsPanel { fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let theme = theme::current(cx).clone(); - let mut channels_column = Flex::column(); - for channel in self.channels.read(cx).channels() { - channels_column = channels_column.with_child(self.render_channel( - 0, - &channel, - &theme.channels_panel.channel_tree, - true, - cx, - )); - } - - let spacing = theme.channels_panel.spacing; - enum ChannelsPanelScrollTag {} Stack::new() .with_child( // Full panel column Flex::column() - .with_spacing(spacing) - .with_child( - // Channels section column - Flex::column() - .with_child( - Flex::row().with_child( - Label::new( - "Active Channels", - theme.editor.invalid_information_diagnostic.message.clone(), - ) - .into_any(), - ), - ) - // Channels list column - .with_child(channels_column), - ) - // TODO: Replace with spacing implementation - .with_child(Empty::new().constrained().with_height(spacing)) .with_child( Flex::column().with_child( Flex::row().with_child( diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 40959c8f5c..857f3f56fc 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,7 +22,6 @@ pub struct Flex { children: Vec>, scroll_state: Option<(ElementStateHandle>, usize)>, child_alignment: f32, - spacing: f32, } impl Flex { @@ -32,7 +31,6 @@ impl Flex { children: Default::default(), scroll_state: None, child_alignment: -1., - spacing: 0., } } @@ -44,11 +42,6 @@ impl Flex { Self::new(Axis::Vertical) } - pub fn with_spacing(mut self, spacing: f32) -> Self { - self.spacing = spacing; - self - } - /// Render children centered relative to the cross-axis of the parent flex. /// /// If this is a flex row, children will be centered vertically. If this is a diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 844b093a5e..56b3b2d156 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1068,61 +1068,9 @@ pub struct FlexStyle { item_spacing: f32, } -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ChannelProjectStyle { - // TODO: Implement Contained Flex - // ContainerStyle + Spacing between elements - // Negative spacing overlaps elements instead of spacing them out - pub container: Contained, - pub host: ImageStyle, - pub title: ContainedText, - pub members: Contained, - pub member: ImageStyle -} - -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ChanneltemStyle { - pub icon: IconStyle, - pub title: TextStyle, -} - -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ChannelListStyle { - pub section_title: ContainedText, - pub channel: Toggleable>, - pub project: ChannelProjectStyle -} - -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ContactItemStyle { - pub container: Contained, - pub avatar: IconStyle, - pub name: TextStyle, -} - -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ContactsListStyle { - pub section_title: ContainedText, - pub contact: ContactItemStyle, -} - - -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ChannelTreeStyle { - pub channel_indent: f32, - pub channel_name: TextStyle, - pub root_name: TextStyle, - pub channel_icon: Toggleable>, -} - #[derive(Clone, Deserialize, Default, JsonSchema)] pub struct ChanelsPanelStyle { - pub channel_tree: ChannelTreeStyle, - pub spacing: f32, - // TODO: Uncomment: - // pub container: ContainerStyle, - // pub channel_list: ChannelListStyle, - // pub contacts_list: ContactsListStyle + pub contacts_header: TextStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/channels_panel.ts b/styles/src/style_tree/channels_panel.ts index b46db5dc38..126bbbe18c 100644 --- a/styles/src/style_tree/channels_panel.ts +++ b/styles/src/style_tree/channels_panel.ts @@ -1,68 +1,12 @@ -// import { with_opacity } from "../theme/color" import { - // Border, - // TextStyle, - // background, - // border, - foreground, text, } from "./components" -import { interactive, toggleable } from "../element" -// import merge from "ts-deepmerge" import { useTheme } from "../theme" export default function channels_panel(): any { const theme = useTheme() - // const { is_light } = theme return { - spacing: 10, - channel_tree: { - channel_indent: 10, - channel_name: text(theme.middle, "sans", "variant", { size: "md" }), - root_name: text(theme.middle, "sans", "variant", { size: "lg", weight: "bold" }), - channel_icon: (() => { - const base_icon = (asset: any, color: any) => { - return { - icon: { - color, - asset, - dimensions: { - width: 12, - height: 12, - } - }, - container: { - corner_radius: 4, - padding: { - top: 4, bottom: 4, left: 4, right: 4 - }, - margin: { - right: 4, - }, - } - } - } - - return toggleable({ - state: { - inactive: interactive({ - state: { - default: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "variant")), - hovered: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "hovered")), - clicked: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "active")), - }, - }), - active: interactive({ - state: { - default: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "variant")), - hovered: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "hovered")), - clicked: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "active")), - }, - }), - }, - }) - })(), - } + contacts_header: text(theme.middle, "sans", "variant", { size: "lg" }), } } From 7f9df6dd2425a5746fe5a330b40422ef11ffa771 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 24 Jul 2023 14:39:16 -0700 Subject: [PATCH 009/326] Move channels panel into collab and rename to collab panel remove contacts popover and add to collab panel --- Cargo.lock | 28 +--- Cargo.toml | 1 - assets/keymaps/default.json | 3 +- crates/channels/Cargo.toml | 38 ------ crates/channels/src/channels.rs | 13 -- crates/collab_ui/Cargo.toml | 2 + crates/collab_ui/src/collab_titlebar_item.rs | 126 +----------------- crates/collab_ui/src/collab_ui.rs | 10 +- .../src/panel.rs} | 82 +++++++----- .../contacts.rs} | 33 ++--- .../{ => panel/contacts}/contact_finder.rs | 0 .../contacts/contacts_list.rs} | 13 +- .../src/panel/panel_settings.rs} | 0 crates/gpui/src/elements.rs | 8 +- crates/rpc/src/proto.rs | 2 + crates/zed/Cargo.toml | 1 - crates/zed/src/main.rs | 1 - crates/zed/src/zed.rs | 26 +--- 18 files changed, 95 insertions(+), 292 deletions(-) delete mode 100644 crates/channels/Cargo.toml delete mode 100644 crates/channels/src/channels.rs rename crates/{channels/src/channels_panel.rs => collab_ui/src/panel.rs} (79%) rename crates/collab_ui/src/{contacts_popover.rs => panel/contacts.rs} (85%) rename crates/collab_ui/src/{ => panel/contacts}/contact_finder.rs (100%) rename crates/collab_ui/src/{contact_list.rs => panel/contacts/contacts_list.rs} (99%) rename crates/{channels/src/channels_panel_settings.rs => collab_ui/src/panel/panel_settings.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index e0a4b6a7bf..617d2c9a81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1254,31 +1254,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" -[[package]] -name = "channels" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "context_menu", - "db", - "editor", - "futures 0.3.28", - "gpui", - "log", - "menu", - "project", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings", - "theme", - "util", - "workspace", -] - [[package]] name = "chrono" version = "0.4.26" @@ -1577,6 +1552,7 @@ dependencies = [ "clock", "collections", "context_menu", + "db", "editor", "feedback", "futures 0.3.28", @@ -1588,6 +1564,7 @@ dependencies = [ "postage", "project", "recent_projects", + "schemars", "serde", "serde_derive", "settings", @@ -9882,7 +9859,6 @@ dependencies = [ "backtrace", "breadcrumbs", "call", - "channels", "chrono", "cli", "client", diff --git a/Cargo.toml b/Cargo.toml index 8803d1c34b..6e79c6b657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "crates/auto_update", "crates/breadcrumbs", "crates/call", - "crates/channels", "crates/cli", "crates/client", "crates/clock", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index adc55f8c91..5c14d818a7 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -499,7 +499,8 @@ { "bindings": { "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", - "cmd-shift-c": "collab::ToggleContactsMenu", + // TODO: Move this to a dock open action + "cmd-shift-c": "collab_panel::ToggleFocus", "cmd-alt-i": "zed::DebugElements" } }, diff --git a/crates/channels/Cargo.toml b/crates/channels/Cargo.toml deleted file mode 100644 index 7507072130..0000000000 --- a/crates/channels/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "channels" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/channels.rs" -doctest = false - -[dependencies] -collections = { path = "../collections" } -context_menu = { path = "../context_menu" } -client = { path = "../client" } -db = { path = "../db" } -editor = { path = "../editor" } -gpui = { path = "../gpui" } -project = { path = "../project" } -theme = { path = "../theme" } -settings = { path = "../settings" } -workspace = { path = "../workspace" } -menu = { path = "../menu" } -util = { path = "../util" } - -log.workspace = true -anyhow.workspace = true -schemars.workspace = true -serde_json.workspace = true -serde.workspace = true -serde_derive.workspace = true -futures.workspace = true - -[dev-dependencies] -client = { path = "../client", features = ["test-support"] } -editor = { path = "../editor", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } -serde_json.workspace = true diff --git a/crates/channels/src/channels.rs b/crates/channels/src/channels.rs deleted file mode 100644 index 7560a36015..0000000000 --- a/crates/channels/src/channels.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod channels_panel; -mod channels_panel_settings; - -pub use channels_panel::*; -use gpui::{AppContext}; - -use std::sync::Arc; - -use client::Client; - -pub fn init(client: Arc, cx: &mut AppContext) { - channels_panel::init(cx); -} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 4a38c2691c..2ceac649ec 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -23,6 +23,7 @@ test-support = [ [dependencies] auto_update = { path = "../auto_update" } +db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } clock = { path = "../clock" } @@ -48,6 +49,7 @@ zed-actions = {path = "../zed-actions"} anyhow.workspace = true futures.workspace = true log.workspace = true +schemars.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a54c0e9e79..0d273fd1b8 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,12 +1,11 @@ use crate::{ - contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, + contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; use clock::ReplicaId; -use contacts_popover::ContactsPopover; use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ actions, @@ -33,7 +32,6 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40; actions!( collab, [ - ToggleContactsMenu, ToggleUserMenu, ToggleProjectMenu, SwitchBranch, @@ -43,7 +41,6 @@ actions!( ); pub fn init(cx: &mut AppContext) { - cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::unshare_project); cx.add_action(CollabTitlebarItem::toggle_user_menu); @@ -56,7 +53,6 @@ pub struct CollabTitlebarItem { user_store: ModelHandle, client: Arc, workspace: WeakViewHandle, - contacts_popover: Option>, branch_popover: Option>, project_popover: Option>, user_menu: ViewHandle, @@ -109,7 +105,6 @@ impl View for CollabTitlebarItem { let status = workspace.read(cx).client().status(); let status = &*status.borrow(); if matches!(status, client::Status::Connected { .. }) { - right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); let avatar = user.as_ref().and_then(|user| user.avatar.clone()); right_container.add_child(self.render_user_menu_button(&theme, avatar, cx)); } else { @@ -184,7 +179,6 @@ impl CollabTitlebarItem { project, user_store, client, - contacts_popover: None, user_menu: cx.add_view(|cx| { let view_id = cx.view_id(); let mut menu = ContextMenu::new(view_id, cx); @@ -315,9 +309,6 @@ impl CollabTitlebarItem { } fn active_call_changed(&mut self, cx: &mut ViewContext) { - if ActiveCall::global(cx).read(cx).room().is_none() { - self.contacts_popover = None; - } cx.notify(); } @@ -337,32 +328,6 @@ impl CollabTitlebarItem { .log_err(); } - pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext) { - if self.contacts_popover.take().is_none() { - let view = cx.add_view(|cx| { - ContactsPopover::new( - self.project.clone(), - self.user_store.clone(), - self.workspace.clone(), - cx, - ) - }); - cx.subscribe(&view, |this, _, event, cx| { - match event { - contacts_popover::Event::Dismissed => { - this.contacts_popover = None; - } - } - - cx.notify(); - }) - .detach(); - self.contacts_popover = Some(view); - } - - cx.notify(); - } - pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { self.user_menu.update(cx, |user_menu, cx| { let items = if let Some(_) = self.user_store.read(cx).current_user() { @@ -519,79 +484,7 @@ impl CollabTitlebarItem { } cx.notify(); } - fn render_toggle_contacts_button( - &self, - theme: &Theme, - cx: &mut ViewContext, - ) -> AnyElement { - let titlebar = &theme.titlebar; - let badge = if self - .user_store - .read(cx) - .incoming_contact_requests() - .is_empty() - { - None - } else { - Some( - Empty::new() - .collapsed() - .contained() - .with_style(titlebar.toggle_contacts_badge) - .contained() - .with_margin_left( - titlebar - .toggle_contacts_button - .inactive_state() - .default - .icon_width, - ) - .with_margin_top( - titlebar - .toggle_contacts_button - .inactive_state() - .default - .icon_width, - ) - .aligned(), - ) - }; - - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar - .toggle_contacts_button - .in_state(self.contacts_popover.is_some()) - .style_for(state); - Svg::new("icons/radix/person.svg") - .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) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_contacts_popover(&Default::default(), cx) - }) - .with_tooltip::( - 0, - "Show contacts menu".into(), - Some(Box::new(ToggleContactsMenu)), - theme.tooltip.clone(), - cx, - ), - ) - .with_children(badge) - .with_children(self.render_contacts_popover_host(titlebar, cx)) - .into_any() - } fn render_toggle_screen_sharing_button( &self, theme: &Theme, @@ -923,23 +816,6 @@ impl CollabTitlebarItem { .into_any() } - fn render_contacts_popover_host<'a>( - &'a self, - _theme: &'a theme::Titlebar, - cx: &'a ViewContext, - ) -> Option> { - self.contacts_popover.as_ref().map(|popover| { - Overlay::new(ChildView::new(popover, cx)) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopLeft) - .with_z_index(999) - .aligned() - .bottom() - .right() - .into_any() - }) - } - fn render_collaborators( &self, workspace: &ViewHandle, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index df4b502391..edbb89e339 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,16 +1,14 @@ mod collab_titlebar_item; -mod contact_finder; -mod contact_list; mod contact_notification; -mod contacts_popover; mod face_pile; mod incoming_call_notification; mod notifications; mod project_shared_notification; mod sharing_status_indicator; +pub mod panel; use call::{ActiveCall, Room}; -pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; +pub use collab_titlebar_item::CollabTitlebarItem; use gpui::{actions, AppContext, Task}; use std::sync::Arc; use util::ResultExt; @@ -24,9 +22,7 @@ actions!( pub fn init(app_state: &Arc, cx: &mut AppContext) { vcs_menu::init(cx); collab_titlebar_item::init(cx); - contact_list::init(cx); - contact_finder::init(cx); - contacts_popover::init(cx); + panel::init(app_state.client.clone(), cx); incoming_call_notification::init(&app_state, cx); project_shared_notification::init(&app_state, cx); sharing_status_indicator::init(cx); diff --git a/crates/channels/src/channels_panel.rs b/crates/collab_ui/src/panel.rs similarity index 79% rename from crates/channels/src/channels_panel.rs rename to crates/collab_ui/src/panel.rs index 063f652191..8fec29133f 100644 --- a/crates/channels/src/channels_panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -1,15 +1,17 @@ +mod contacts; +mod panel_settings; + use std::sync::Arc; -use crate::channels_panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}; use anyhow::Result; -use collections::HashMap; +use client::Client; use context_menu::ContextMenu; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, elements::{ChildView, Flex, Label, ParentElement, Stack}, - serde_json, AppContext, AsyncAppContext, Element, Entity, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + serde_json, AppContext, AsyncAppContext, Element, Entity, Task, View, ViewContext, ViewHandle, + WeakViewHandle, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; @@ -20,27 +22,32 @@ use workspace::{ Workspace, }; -actions!(channels, [ToggleFocus]); +use self::{ + contacts::Contacts, + panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}, +}; + +actions!(collab_panel, [ToggleFocus]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; -pub fn init(cx: &mut AppContext) { - settings::register::(cx); +pub fn init(_client: Arc, cx: &mut AppContext) { + settings::register::(cx); + contacts::init(cx); } -pub struct ChannelsPanel { +pub struct CollabPanel { width: Option, fs: Arc, has_focus: bool, pending_serialization: Task>, context_menu: ViewHandle, - collapsed_channels: HashMap, + contacts: ViewHandle, } #[derive(Serialize, Deserialize)] struct SerializedChannelsPanel { width: Option, - collapsed_channels: Option>, } #[derive(Debug)] @@ -49,26 +56,34 @@ pub enum Event { Focus, } -impl Entity for ChannelsPanel { +impl Entity for CollabPanel { type Event = Event; } -impl ChannelsPanel { +impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { cx.add_view(|cx| { let view_id = cx.view_id(); + let this = Self { width: None, has_focus: false, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - collapsed_channels: HashMap::default(), + contacts: cx.add_view(|cx| { + Contacts::new( + workspace.project().clone(), + workspace.user_store().clone(), + workspace.weak_handle(), + cx, + ) + }), }; // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this: &mut ChannelsPanel, cx| { + cx.observe_global::(move |this: &mut CollabPanel, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; @@ -99,12 +114,10 @@ impl ChannelsPanel { }; workspace.update(&mut cx, |workspace, cx| { - let panel = ChannelsPanel::new(workspace, cx); + let panel = CollabPanel::new(workspace, cx); if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; - panel.collapsed_channels = - serialized_panel.collapsed_channels.unwrap_or_default(); cx.notify(); }); } @@ -115,16 +128,12 @@ impl ChannelsPanel { fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; - let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background().spawn( async move { KEY_VALUE_STORE .write_kvp( CHANNELS_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { - width, - collapsed_channels: Some(collapsed_channels), - })?, + serde_json::to_string(&SerializedChannelsPanel { width })?, ) .await?; anyhow::Ok(()) @@ -134,7 +143,7 @@ impl ChannelsPanel { } } -impl View for ChannelsPanel { +impl View for CollabPanel { fn ui_name() -> &'static str { "ChannelsPanel" } @@ -159,18 +168,19 @@ impl View for ChannelsPanel { // Full panel column Flex::column() .with_child( - Flex::column().with_child( - Flex::row().with_child( - Label::new( - "Contacts", - theme.editor.invalid_information_diagnostic.message.clone(), - ) - .into_any(), - ), - ), + Flex::column() + .with_child( + Flex::row().with_child( + Label::new( + "Contacts", + theme.editor.invalid_information_diagnostic.message.clone(), + ) + .into_any(), + ), + ) + .with_child(ChildView::new(&self.contacts, cx)), ) - .scrollable::(0, None, cx) - .expanded(), + .scrollable::(0, None, cx), ) .with_child(ChildView::new(&self.context_menu, cx)) .into_any_named("channels panel") @@ -178,7 +188,7 @@ impl View for ChannelsPanel { } } -impl Panel for ChannelsPanel { +impl Panel for CollabPanel { fn position(&self, cx: &gpui::WindowContext) -> DockPosition { match settings::get::(cx).dock { ChannelsPanelDockPosition::Left => DockPosition::Left, @@ -216,7 +226,7 @@ impl Panel for ChannelsPanel { } fn icon_path(&self) -> &'static str { - "icons/bolt_16.svg" + "icons/radix/person.svg" } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/panel/contacts.rs similarity index 85% rename from crates/collab_ui/src/contacts_popover.rs rename to crates/collab_ui/src/panel/contacts.rs index 1d6d1c84c7..a1c1061f5e 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/panel/contacts.rs @@ -1,7 +1,6 @@ -use crate::{ - contact_finder::{build_contact_finder, ContactFinder}, - contact_list::ContactList, -}; +mod contact_finder; +mod contacts_list; + use client::UserStore; use gpui::{ actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View, @@ -11,10 +10,14 @@ use picker::PickerEvent; use project::Project; use workspace::Workspace; +use self::{contacts_list::ContactList, contact_finder::{ContactFinder, build_contact_finder}}; + actions!(contacts_popover, [ToggleContactFinder]); pub fn init(cx: &mut AppContext) { - cx.add_action(ContactsPopover::toggle_contact_finder); + cx.add_action(Contacts::toggle_contact_finder); + contact_finder::init(cx); + contacts_list::init(cx); } pub enum Event { @@ -26,7 +29,7 @@ enum Child { ContactFinder(ViewHandle), } -pub struct ContactsPopover { +pub struct Contacts { child: Child, project: ModelHandle, user_store: ModelHandle, @@ -34,7 +37,7 @@ pub struct ContactsPopover { _subscription: Option, } -impl ContactsPopover { +impl Contacts { pub fn new( project: ModelHandle, user_store: ModelHandle, @@ -61,7 +64,7 @@ impl ContactsPopover { } } - fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext) { + fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext) { let child = cx.add_view(|cx| { let finder = build_contact_finder(self.user_store.clone(), cx); finder.set_query(editor_text, cx); @@ -75,7 +78,7 @@ impl ContactsPopover { cx.notify(); } - fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext) { + fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext) { let child = cx.add_view(|cx| { ContactList::new( self.project.clone(), @@ -87,8 +90,8 @@ impl ContactsPopover { }); cx.focus(&child); self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event { - crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed), - crate::contact_list::Event::ToggleContactFinder => { + contacts_list::Event::Dismissed => cx.emit(Event::Dismissed), + contacts_list::Event::ToggleContactFinder => { this.toggle_contact_finder(&Default::default(), cx) } })); @@ -97,11 +100,11 @@ impl ContactsPopover { } } -impl Entity for ContactsPopover { +impl Entity for Contacts { type Event = Event; } -impl View for ContactsPopover { +impl View for Contacts { fn ui_name() -> &'static str { "ContactsPopover" } @@ -113,9 +116,9 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child, cx), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::::new(0, cx, |_, _| { Flex::column() - .with_child(child.flex(1., true)) + .with_child(child) .contained() .with_style(theme.contacts_popover.container) .constrained() diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/panel/contacts/contact_finder.rs similarity index 100% rename from crates/collab_ui/src/contact_finder.rs rename to crates/collab_ui/src/panel/contacts/contact_finder.rs diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/panel/contacts/contacts_list.rs similarity index 99% rename from crates/collab_ui/src/contact_list.rs rename to crates/collab_ui/src/panel/contacts/contacts_list.rs index 428f2156d1..f37d64cd05 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/panel/contacts/contacts_list.rs @@ -1326,12 +1326,11 @@ impl View for ContactList { Flex::column() .with_child( Flex::row() - .with_child( - ChildView::new(&self.filter_editor, cx) - .contained() - .with_style(theme.contact_list.user_query_editor.container) - .flex(1., true), - ) + // .with_child( + // ChildView::new(&self.filter_editor, cx) + // .contained() + // .with_style(theme.contact_list.user_query_editor.container) + // ) .with_child( MouseEventHandler::::new(0, cx, |_, _| { render_icon_button( @@ -1354,7 +1353,7 @@ impl View for ContactList { .constrained() .with_height(theme.contact_list.user_query_editor_height), ) - .with_child(List::new(self.list_state.clone()).flex(1., false)) + // .with_child(List::new(self.list_state.clone())) .into_any() } diff --git a/crates/channels/src/channels_panel_settings.rs b/crates/collab_ui/src/panel/panel_settings.rs similarity index 100% rename from crates/channels/src/channels_panel_settings.rs rename to crates/collab_ui/src/panel/panel_settings.rs diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 78403444ff..746238aaa9 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -47,6 +47,10 @@ pub trait Element: 'static { type LayoutState; type PaintState; + fn view_name(&self) -> &'static str { + V::ui_name() + } + fn layout( &mut self, constraint: SizeConstraint, @@ -267,8 +271,8 @@ impl> AnyElementState for ElementState { | ElementState::PostLayout { mut element, .. } | ElementState::PostPaint { mut element, .. } => { let (size, layout) = element.layout(constraint, view, cx); - debug_assert!(size.x().is_finite()); - debug_assert!(size.y().is_finite()); + debug_assert!(size.x().is_finite(), "Element for {:?} had infinite x size after layout", element.view_name()); + debug_assert!(size.y().is_finite(), "Element for {:?} had infinite x size after layout", element.view_name()); result = size; ElementState::PostLayout { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 605b05a562..e24d6cb4b7 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope}; use anyhow::{anyhow, Result}; use async_tungstenite::tungstenite::Message as WebSocketMessage; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 71d8461b01..a5877aaccb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,7 +21,6 @@ activity_indicator = { path = "../activity_indicator" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } -channels = { path = "../channels" } cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5739052b67..e44ab3e33a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -155,7 +155,6 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); - channels::init(client.clone(), cx); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c1046c0995..b2d1c2a7a2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -9,9 +9,8 @@ use ai::AssistantPanel; use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; -use channels::ChannelsPanel; pub use client; -use collab_ui::{CollabTitlebarItem, ToggleContactsMenu}; +use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut use collections::VecDeque; pub use editor; use editor::{Editor, MultiBuffer}; @@ -86,20 +85,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx.toggle_full_screen(); }, ); - cx.add_action( - |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext| { - if let Some(item) = workspace - .titlebar_item() - .and_then(|item| item.downcast::()) - { - cx.defer(move |_, cx| { - item.update(cx, |item, cx| { - item.toggle_contacts_popover(&Default::default(), cx); - }); - }); - } - }, - ); cx.add_global_action(quit); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { @@ -223,8 +208,10 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { }, ); cx.add_action( - |workspace: &mut Workspace, _: &channels::ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); + |workspace: &mut Workspace, + _: &collab_ui::panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); }, ); cx.add_action( @@ -345,7 +332,8 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - let channels_panel = ChannelsPanel::load(workspace_handle.clone(), cx.clone()); + let channels_panel = + collab_ui::panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!( project_panel, terminal_panel, From 969ecfcfa234ee150eebb87507cebec48afdf53f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 24 Jul 2023 20:00:31 -0700 Subject: [PATCH 010/326] Reinstate all of the contacts popovers' functionality in the new collaboration panel --- crates/collab_ui/src/panel.rs | 1377 +++++++++++++++- .../panel/{contacts => }/contact_finder.rs | 0 crates/collab_ui/src/panel/contacts.rs | 140 -- .../src/panel/contacts/contacts_list.rs | 1384 ----------------- crates/gpui/src/elements.rs | 12 +- styles/src/style_tree/contacts_popover.ts | 6 +- 6 files changed, 1342 insertions(+), 1577 deletions(-) rename crates/collab_ui/src/panel/{contacts => }/contact_finder.rs (100%) delete mode 100644 crates/collab_ui/src/panel/contacts.rs delete mode 100644 crates/collab_ui/src/panel/contacts/contacts_list.rs diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 8fec29133f..28cb57cf79 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -1,39 +1,51 @@ -mod contacts; +mod contact_finder; mod panel_settings; -use std::sync::Arc; - use anyhow::Result; -use client::Client; +use call::ActiveCall; +use client::{proto::PeerId, Client, Contact, User, UserStore}; +use contact_finder::{build_contact_finder, ContactFinder}; use context_menu::ContextMenu; use db::kvp::KEY_VALUE_STORE; +use editor::{Cancel, Editor}; +use futures::StreamExt; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, - elements::{ChildView, Flex, Label, ParentElement, Stack}, - serde_json, AppContext, AsyncAppContext, Element, Entity, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + elements::{ + Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, + }, + geometry::{rect::RectF, vector::vec2f}, + platform::{CursorStyle, MouseButton, PromptLevel}, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, + Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::Fs; +use menu::{Confirm, SelectNext, SelectPrev}; +use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}; +use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; +use std::{mem, sync::Arc}; +use theme::IconButton; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, Workspace, }; -use self::{ - contacts::Contacts, - panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}, -}; - actions!(collab_panel, [ToggleFocus]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); - contacts::init(cx); + contact_finder::init(cx); + + cx.add_action(CollabPanel::cancel); + cx.add_action(CollabPanel::select_next); + cx.add_action(CollabPanel::select_prev); + cx.add_action(CollabPanel::confirm); } pub struct CollabPanel { @@ -42,7 +54,19 @@ pub struct CollabPanel { has_focus: bool, pending_serialization: Task>, context_menu: ViewHandle, - contacts: ViewHandle, + contact_finder: Option>, + + // from contacts list + filter_editor: ViewHandle, + entries: Vec, + selection: Option, + user_store: ModelHandle, + project: ModelHandle, + match_candidates: Vec, + list_state: ListState, + subscriptions: Vec, + collapsed_sections: Vec
, + workspace: WeakViewHandle, } #[derive(Serialize, Deserialize)] @@ -54,6 +78,40 @@ struct SerializedChannelsPanel { pub enum Event { DockPositionChanged, Focus, + Dismissed, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + ActiveCall, + Requests, + Online, + Offline, +} + +#[derive(Clone)] +enum ContactEntry { + Header(Section), + CallParticipant { + user: Arc, + is_pending: bool, + }, + ParticipantProject { + project_id: u64, + worktree_root_names: Vec, + host_user_id: u64, + is_last: bool, + }, + ParticipantScreen { + peer_id: PeerId, + is_last: bool, + }, + IncomingRequest(Arc), + OutgoingRequest(Arc), + Contact { + contact: Arc, + calling: bool, + }, } impl Entity for CollabPanel { @@ -62,35 +120,151 @@ impl Entity for CollabPanel { impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { - cx.add_view(|cx| { + cx.add_view::(|cx| { let view_id = cx.view_id(); - let this = Self { + let filter_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| { + theme.contact_list.user_query_editor.clone() + })), + cx, + ); + editor.set_placeholder_text("Filter contacts", cx); + editor + }); + + cx.subscribe(&filter_editor, |this, _, event, cx| { + if let editor::Event::BufferEdited = event { + let query = this.filter_editor.read(cx).text(cx); + if !query.is_empty() { + this.selection.take(); + } + this.update_entries(cx); + if !query.is_empty() { + this.selection = this + .entries + .iter() + .position(|entry| !matches!(entry, ContactEntry::Header(_))); + } + } + }) + .detach(); + + let list_state = + ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { + let theme = theme::current(cx).clone(); + let is_selected = this.selection == Some(ix); + let current_project_id = this.project.read(cx).remote_id(); + + match &this.entries[ix] { + ContactEntry::Header(section) => { + let is_collapsed = this.collapsed_sections.contains(section); + Self::render_header( + *section, + &theme.contact_list, + is_selected, + is_collapsed, + cx, + ) + } + ContactEntry::CallParticipant { user, is_pending } => { + Self::render_call_participant( + user, + *is_pending, + is_selected, + &theme.contact_list, + ) + } + ContactEntry::ParticipantProject { + project_id, + worktree_root_names, + host_user_id, + is_last, + } => Self::render_participant_project( + *project_id, + worktree_root_names, + *host_user_id, + Some(*project_id) == current_project_id, + *is_last, + is_selected, + &theme.contact_list, + cx, + ), + ContactEntry::ParticipantScreen { peer_id, is_last } => { + Self::render_participant_screen( + *peer_id, + *is_last, + is_selected, + &theme.contact_list, + cx, + ) + } + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contact_list, + true, + is_selected, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contact_list, + false, + is_selected, + cx, + ), + ContactEntry::Contact { contact, calling } => Self::render_contact( + contact, + *calling, + &this.project, + &theme.contact_list, + is_selected, + cx, + ), + } + }); + + let mut this = Self { width: None, has_focus: false, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - contacts: cx.add_view(|cx| { - Contacts::new( - workspace.project().clone(), - workspace.user_store().clone(), - workspace.weak_handle(), - cx, - ) - }), + filter_editor, + contact_finder: None, + entries: Vec::default(), + selection: None, + user_store: workspace.user_store().clone(), + project: workspace.project().clone(), + subscriptions: Vec::default(), + match_candidates: Vec::default(), + collapsed_sections: Vec::default(), + workspace: workspace.weak_handle(), + list_state, }; + this.update_entries(cx); // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this: &mut CollabPanel, 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); - } - }) - .detach(); + this.subscriptions + .push( + cx.observe_global::(move |this: &mut CollabPanel, 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); + } + }), + ); + + let active_call = ActiveCall::global(cx); + this.subscriptions + .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx))); + this.subscriptions + .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); this }) @@ -141,11 +315,1015 @@ impl CollabPanel { .log_err(), ); } + + fn update_entries(&mut self, cx: &mut ViewContext) { + let user_store = self.user_store.read(cx); + let query = self.filter_editor.read(cx).text(cx); + let executor = cx.background().clone(); + + let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + let old_entries = mem::take(&mut self.entries); + + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + let mut participant_entries = Vec::new(); + + // Populate the active user. + if let Some(user) = user_store.current_user() { + self.match_candidates.clear(); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + let user_id = user.id; + participant_entries.push(ContactEntry::CallParticipant { + user, + is_pending: false, + }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_last: projects.peek().is_none(), + }); + } + } + } + + // Populate remote participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.remote_participants().iter().map(|(_, participant)| { + StringMatchCandidate { + id: participant.user.id as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + } + })); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + for mat in matches { + let user_id = mat.candidate_id as u64; + let participant = &room.remote_participants()[&user_id]; + participant_entries.push(ContactEntry::CallParticipant { + user: participant.user.clone(), + is_pending: false, + }); + let mut projects = participant.projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: participant.user.id, + is_last: projects.peek().is_none() && participant.video_tracks.is_empty(), + }); + } + if !participant.video_tracks.is_empty() { + participant_entries.push(ContactEntry::ParticipantScreen { + peer_id: participant.peer_id, + is_last: true, + }); + } + } + + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend( + room.pending_participants() + .iter() + .enumerate() + .map(|(id, participant)| StringMatchCandidate { + id, + string: participant.github_login.clone(), + char_bag: participant.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + is_pending: true, + })); + + if !participant_entries.is_empty() { + self.entries.push(ContactEntry::Header(Section::ActiveCall)); + if !self.collapsed_sections.contains(&Section::ActiveCall) { + self.entries.extend(participant_entries); + } + } + } + + let mut request_entries = Vec::new(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); + } + + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } + + if !request_entries.is_empty() { + self.entries.push(ContactEntry::Header(Section::Requests)); + if !self.collapsed_sections.contains(&Section::Requests) { + self.entries.append(&mut request_entries); + } + } + + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); + + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + + let (mut online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + online_contacts.retain(|contact| { + let contact = &contacts[contact.candidate_id]; + !room.contains_participant(contact.user.id) + }); + } + + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ContactEntry::Header(section)); + if !self.collapsed_sections.contains(§ion) { + let active_call = &ActiveCall::global(cx).read(cx); + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ContactEntry::Contact { + contact: contact.clone(), + calling: active_call.pending_invites().contains(&contact.user.id), + }); + } + } + } + } + } + + if let Some(prev_selected_entry) = prev_selected_entry { + self.selection.take(); + for (ix, entry) in self.entries.iter().enumerate() { + if *entry == prev_selected_entry { + self.selection = Some(ix); + break; + } + } + } + + let old_scroll_top = self.list_state.logical_scroll_top(); + self.list_state.reset(self.entries.len()); + + // Attempt to maintain the same scroll position. + if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { + let new_scroll_top = self + .entries + .iter() + .position(|entry| entry == old_top_entry) + .map(|item_ix| ListOffset { + item_ix, + offset_in_item: old_scroll_top.offset_in_item, + }) + .or_else(|| { + let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_after_old_top)?; + Some(ListOffset { + item_ix, + offset_in_item: 0., + }) + }) + .or_else(|| { + let entry_before_old_top = + old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_before_old_top)?; + Some(ListOffset { + item_ix, + offset_in_item: 0., + }) + }); + + self.list_state + .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); + } + + cx.notify(); + } + + fn render_call_participant( + user: &User, + is_pending: bool, + is_selected: bool, + theme: &theme::ContactList, + ) -> AnyElement { + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::from_data(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .with_children(if is_pending { + Some( + Label::new("Calling", theme.calling_indicator.text.clone()) + .contained() + .with_style(theme.calling_indicator.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()), + ) + .into_any() + } + + fn render_participant_project( + project_id: u64, + worktree_root_names: &[String], + host_user_id: u64, + is_current: bool, + is_last: bool, + is_selected: bool, + theme: &theme::ContactList, + cx: &mut ViewContext, + ) -> AnyElement { + enum JoinProject {} + + let font_cache = cx.font_cache(); + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + let row = &theme.project_row.inactive_state().default; + let tree_branch = theme.tree_branch; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; + let project_name = if worktree_root_names.is_empty() { + "untitled".to_string() + } else { + worktree_root_names.join(", ") + }; + + MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + let row = theme + .project_row + .in_state(is_selected) + .style_for(mouse_state); + + Flex::row() + .with_child( + Stack::new() + .with_child(Canvas::new(move |scene, bounds, _, _, _| { + let start_x = + bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + })) + .constrained() + .with_width(host_avatar_height), + ) + .with_child( + Label::new(project_name, row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(row.container) + }) + .with_cursor_style(if !is_current { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if !is_current { + if let Some(workspace) = this.workspace.upgrade(cx) { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_remote_project(project_id, host_user_id, app_state, cx) + .detach_and_log_err(cx); + } + } + }) + .into_any() + } + + fn render_participant_screen( + peer_id: PeerId, + is_last: bool, + is_selected: bool, + theme: &theme::ContactList, + cx: &mut ViewContext, + ) -> AnyElement { + enum OpenSharedScreen {} + + let font_cache = cx.font_cache(); + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + let row = &theme.project_row.inactive_state().default; + let tree_branch = theme.tree_branch; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; + + MouseEventHandler::::new( + peer_id.as_u64() as usize, + cx, + |mouse_state, _| { + let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + let row = theme + .project_row + .in_state(is_selected) + .style_for(mouse_state); + + Flex::row() + .with_child( + Stack::new() + .with_child(Canvas::new(move |scene, bounds, _, _, _| { + let start_x = bounds.min_x() + (bounds.width() / 2.) + - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + })) + .constrained() + .with_width(host_avatar_height), + ) + .with_child( + Svg::new("icons/disable_screen_sharing_12.svg") + .with_color(row.icon.color) + .constrained() + .with_width(row.icon.width) + .aligned() + .left() + .contained() + .with_style(row.icon.container), + ) + .with_child( + Label::new("Screen", row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(row.container) + }, + ) + .with_cursor_style(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() + } + + fn render_header( + section: Section, + theme: &theme::ContactList, + is_selected: bool, + is_collapsed: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum Header {} + enum LeaveCallContactList {} + + let header_style = theme + .header_row + .in_state(is_selected) + .style_for(&mut Default::default()); + let text = match section { + Section::ActiveCall => "Collaborators", + Section::Requests => "Contact Requests", + Section::Online => "Online", + Section::Offline => "Offline", + }; + let leave_call = if section == Section::ActiveCall { + Some( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.leave_call.style_for(state); + Label::new("Leave Call", style.text.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, |_, _, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }) + .aligned(), + ) + } else { + None + }; + + let icon_size = theme.section_icon_size; + MouseEventHandler::::new(section as usize, cx, |_, _| { + Flex::row() + .with_child( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .with_color(header_style.text.color) + .constrained() + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size), + ) + .with_child( + Label::new(text, header_style.text.clone()) + .aligned() + .left() + .contained() + .with_margin_left(theme.contact_username.container.margin.left) + .flex(1., true), + ) + .with_children(leave_call) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(header_style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_expanded(section, cx); + }) + .into_any() + } + + fn render_contact( + contact: &Contact, + calling: bool, + project: &ModelHandle, + theme: &theme::ContactList, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + let online = contact.online; + let busy = contact.busy || calling; + let user_id = contact.user.id; + let github_login = contact.user.github_login.clone(); + let initial_project = project.clone(); + let mut event_handler = + MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { + Flex::row() + .with_children(contact.user.avatar.clone().map(|avatar| { + let status_badge = if contact.online { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(if busy { + theme.contact_status_busy + } else { + theme.contact_status_free + }) + .aligned(), + ) + } else { + None + }; + Stack::new() + .with_child( + Image::from_data(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left(), + ) + .with_children(status_badge) + })) + .with_child( + Label::new( + contact.user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::::new( + contact.user.id as usize, + cx, + |mouse_state, _| { + let button_style = theme.contact_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + .flex_float() + }, + ) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_contact(user_id, &github_login, cx); + }) + .flex_float(), + ) + .with_children(if calling { + Some( + Label::new("Calling", theme.calling_indicator.text.clone()) + .contained() + .with_style(theme.calling_indicator.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()), + ) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if online && !busy { + this.call(user_id, Some(initial_project.clone()), cx); + } + }); + + if online { + event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand); + } + + event_handler.into_any() + } + + fn render_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactList, + is_incoming: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum Decline {} + enum Accept {} + enum Cancel {} + + let mut row = Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::from_data(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + })) + .with_child( + Label::new( + user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ); + + let user_id = user.id; + let github_login = user.github_login.clone(); + let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + let button_spacing = theme.contact_button_spacing; + + if is_incoming { + row.add_child( + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state) + }; + render_icon_button(button_style, "icons/x_mark_8.svg").aligned() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.respond_to_contact_request(user_id, false, cx); + }) + .contained() + .with_margin_right(button_spacing), + ); + + row.add_child( + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state) + }; + render_icon_button(button_style, "icons/check_8.svg") + .aligned() + .flex_float() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.respond_to_contact_request(user_id, true, cx); + }), + ); + } else { + row.add_child( + MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + let button_style = if is_contact_request_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state) + }; + render_icon_button(button_style, "icons/x_mark_8.svg") + .aligned() + .flex_float() + }) + .with_padding(Padding::uniform(2.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.remove_contact(user_id, &github_login, cx); + }) + .flex_float(), + ); + } + + row.constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()), + ) + .into_any() + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.contact_finder.take().is_some() { + cx.notify(); + return; + } + + let did_clear = self.filter_editor.update(cx, |editor, cx| { + if editor.buffer().read(cx).len(cx) > 0 { + editor.set_text("", cx); + true + } else { + false + } + }); + + if !did_clear { + cx.emit(Event::Dismissed); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if self.entries.len() > ix + 1 { + self.selection = Some(ix + 1); + } + } else if !self.entries.is_empty() { + self.selection = Some(0); + } + self.list_state.reset(self.entries.len()); + if let Some(ix) = self.selection { + self.list_state.scroll_to(ListOffset { + item_ix: ix, + offset_in_item: 0., + }); + } + cx.notify(); + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if ix > 0 { + self.selection = Some(ix - 1); + } else { + self.selection = None; + } + } + self.list_state.reset(self.entries.len()); + if let Some(ix) = self.selection { + self.list_state.scroll_to(ListOffset { + item_ix: ix, + offset_in_item: 0., + }); + } + cx.notify(); + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + if let Some(entry) = self.entries.get(selection) { + match entry { + ContactEntry::Header(section) => { + self.toggle_expanded(*section, cx); + } + ContactEntry::Contact { contact, calling } => { + if contact.online && !contact.busy && !calling { + self.call(contact.user.id, Some(self.project.clone()), cx); + } + } + ContactEntry::ParticipantProject { + project_id, + host_user_id, + .. + } => { + if let Some(workspace) = self.workspace.upgrade(cx) { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_remote_project( + *project_id, + *host_user_id, + app_state, + cx, + ) + .detach_and_log_err(cx); + } + } + ContactEntry::ParticipantScreen { peer_id, .. } => { + if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(*peer_id, cx) + }); + } + } + _ => {} + } + } + } + } + + fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { + self.collapsed_sections.remove(ix); + } else { + self.collapsed_sections.push(section); + } + self.update_entries(cx); + } + + fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + if self.contact_finder.take().is_none() { + let child = cx.add_view(|cx| { + let finder = build_contact_finder(self.user_store.clone(), cx); + finder.set_query(self.filter_editor.read(cx).text(cx), cx); + finder + }); + cx.focus(&child); + // self.subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { + // // PickerEvent::Dismiss => cx.emit(Event::Dismissed), + // })); + self.contact_finder = Some(child); + } + cx.notify(); + } + + fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { + let user_store = self.user_store.clone(); + let prompt_message = format!( + "Are you sure you want to remove \"{}\" from your contacts?", + github_login + ); + let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + let window_id = cx.window_id(); + cx.spawn(|_, mut cx| async move { + if answer.next().await == Some(0) { + if let Err(e) = user_store + .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) + .await + { + cx.prompt( + window_id, + PromptLevel::Info, + &format!("Failed to remove contact: {}", e), + &["Ok"], + ); + } + } + }) + .detach(); + } + + fn respond_to_contact_request( + &mut self, + user_id: u64, + accept: bool, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(user_id, accept, cx) + }) + .detach(); + } + + fn call( + &mut self, + recipient_user_id: u64, + initial_project: Option>, + cx: &mut ViewContext, + ) { + ActiveCall::global(cx) + .update(cx, |call, cx| { + call.invite(recipient_user_id, initial_project, cx) + }) + .detach_and_log_err(cx); + } } impl View for CollabPanel { fn ui_name() -> &'static str { - "ChannelsPanel" + "CollabPanel" } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { @@ -160,28 +1338,58 @@ impl View for CollabPanel { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { + enum AddContact {} let theme = theme::current(cx).clone(); - enum ChannelsPanelScrollTag {} Stack::new() - .with_child( - // Full panel column + .with_child(if let Some(finder) = &self.contact_finder { + ChildView::new(&finder, cx).into_any() + } else { Flex::column() .with_child( - Flex::column() + Flex::row() .with_child( - Flex::row().with_child( - Label::new( - "Contacts", - theme.editor.invalid_information_diagnostic.message.clone(), - ) - .into_any(), - ), + ChildView::new(&self.filter_editor, cx) + .contained() + .with_style(theme.contact_list.user_query_editor.container) + .flex(1.0, true), ) - .with_child(ChildView::new(&self.contacts, cx)), + .with_child( + MouseEventHandler::::new(0, cx, |_, _| { + render_icon_button( + &theme.contact_list.add_contact_button, + "icons/user_plus_16.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| { + this.toggle_contact_finder(cx); + }) + .with_tooltip::( + 0, + "Search for new contact".into(), + None, + theme.tooltip.clone(), + cx, + ) + .constrained() + .with_height(theme.contact_list.user_query_editor_height) + .with_width(theme.contact_list.user_query_editor_height), + ) + .constrained() + .with_width(self.size(cx)), ) - .scrollable::(0, None, cx), - ) + .with_child( + List::new(self.list_state.clone()) + .constrained() + .with_width(self.size(cx)) + .flex(1., true) + .into_any(), + ) + .constrained() + .with_width(self.size(cx)) + .into_any() + }) .with_child(ChildView::new(&self.context_menu, cx)) .into_any_named("channels panel") .into_any() @@ -245,3 +1453,76 @@ impl Panel for CollabPanel { matches!(event, Event::Focus) } } + +impl PartialEq for ContactEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ContactEntry::Header(section_1) => { + if let ContactEntry::Header(section_2) = other { + return section_1 == section_2; + } + } + ContactEntry::CallParticipant { user: user_1, .. } => { + if let ContactEntry::CallParticipant { user: user_2, .. } = other { + return user_1.id == user_2.id; + } + } + ContactEntry::ParticipantProject { + project_id: project_id_1, + .. + } => { + if let ContactEntry::ParticipantProject { + project_id: project_id_2, + .. + } = other + { + return project_id_1 == project_id_2; + } + } + ContactEntry::ParticipantScreen { + peer_id: peer_id_1, .. + } => { + if let ContactEntry::ParticipantScreen { + peer_id: peer_id_2, .. + } = other + { + return peer_id_1 == peer_id_2; + } + } + ContactEntry::IncomingRequest(user_1) => { + if let ContactEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::OutgoingRequest(user_1) => { + if let ContactEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::Contact { + contact: contact_1, .. + } => { + if let ContactEntry::Contact { + contact: contact_2, .. + } = other + { + return contact_1.user.id == contact_2.user.id; + } + } + } + false + } +} + +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() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) +} diff --git a/crates/collab_ui/src/panel/contacts/contact_finder.rs b/crates/collab_ui/src/panel/contact_finder.rs similarity index 100% rename from crates/collab_ui/src/panel/contacts/contact_finder.rs rename to crates/collab_ui/src/panel/contact_finder.rs diff --git a/crates/collab_ui/src/panel/contacts.rs b/crates/collab_ui/src/panel/contacts.rs deleted file mode 100644 index a1c1061f5e..0000000000 --- a/crates/collab_ui/src/panel/contacts.rs +++ /dev/null @@ -1,140 +0,0 @@ -mod contact_finder; -mod contacts_list; - -use client::UserStore; -use gpui::{ - actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View, - ViewContext, ViewHandle, WeakViewHandle, -}; -use picker::PickerEvent; -use project::Project; -use workspace::Workspace; - -use self::{contacts_list::ContactList, contact_finder::{ContactFinder, build_contact_finder}}; - -actions!(contacts_popover, [ToggleContactFinder]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(Contacts::toggle_contact_finder); - contact_finder::init(cx); - contacts_list::init(cx); -} - -pub enum Event { - Dismissed, -} - -enum Child { - ContactList(ViewHandle), - ContactFinder(ViewHandle), -} - -pub struct Contacts { - child: Child, - project: ModelHandle, - user_store: ModelHandle, - workspace: WeakViewHandle, - _subscription: Option, -} - -impl Contacts { - pub fn new( - project: ModelHandle, - user_store: ModelHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - let mut this = Self { - child: Child::ContactList(cx.add_view(|cx| { - ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx) - })), - project, - user_store, - workspace, - _subscription: None, - }; - this.show_contact_list(String::new(), cx); - this - } - - fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { - match &self.child { - Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx), - Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx), - } - } - - fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext) { - let child = cx.add_view(|cx| { - let finder = build_contact_finder(self.user_store.clone(), cx); - finder.set_query(editor_text, cx); - finder - }); - cx.focus(&child); - self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { - PickerEvent::Dismiss => cx.emit(Event::Dismissed), - })); - self.child = Child::ContactFinder(child); - cx.notify(); - } - - fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext) { - let child = cx.add_view(|cx| { - ContactList::new( - self.project.clone(), - self.user_store.clone(), - self.workspace.clone(), - cx, - ) - .with_editor_text(editor_text, cx) - }); - cx.focus(&child); - self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event { - contacts_list::Event::Dismissed => cx.emit(Event::Dismissed), - contacts_list::Event::ToggleContactFinder => { - this.toggle_contact_finder(&Default::default(), cx) - } - })); - self.child = Child::ContactList(child); - cx.notify(); - } -} - -impl Entity for Contacts { - type Event = Event; -} - -impl View for Contacts { - fn ui_name() -> &'static str { - "ContactsPopover" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx).clone(); - let child = match &self.child { - Child::ContactList(child) => ChildView::new(child, cx), - Child::ContactFinder(child) => ChildView::new(child, cx), - }; - - MouseEventHandler::::new(0, cx, |_, _| { - Flex::column() - .with_child(child) - .contained() - .with_style(theme.contacts_popover.container) - .constrained() - .with_width(theme.contacts_popover.width) - .with_height(theme.contacts_popover.height) - }) - .on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed)) - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - match &self.child { - Child::ContactList(child) => cx.focus(child), - Child::ContactFinder(child) => cx.focus(child), - } - } - } -} diff --git a/crates/collab_ui/src/panel/contacts/contacts_list.rs b/crates/collab_ui/src/panel/contacts/contacts_list.rs deleted file mode 100644 index f37d64cd05..0000000000 --- a/crates/collab_ui/src/panel/contacts/contacts_list.rs +++ /dev/null @@ -1,1384 +0,0 @@ -use call::ActiveCall; -use client::{proto::PeerId, Contact, User, UserStore}; -use editor::{Cancel, Editor}; -use futures::StreamExt; -use fuzzy::{match_strings, StringMatchCandidate}; -use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - impl_actions, - keymap_matcher::KeymapContext, - platform::{CursorStyle, MouseButton, PromptLevel}, - AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use menu::{Confirm, SelectNext, SelectPrev}; -use project::Project; -use serde::Deserialize; -use std::{mem, sync::Arc}; -use theme::IconButton; -use workspace::Workspace; - -impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(ContactList::remove_contact); - cx.add_action(ContactList::respond_to_contact_request); - cx.add_action(ContactList::cancel); - cx.add_action(ContactList::select_next); - cx.add_action(ContactList::select_prev); - cx.add_action(ContactList::confirm); -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -enum Section { - ActiveCall, - Requests, - Online, - Offline, -} - -#[derive(Clone)] -enum ContactEntry { - Header(Section), - CallParticipant { - user: Arc, - is_pending: bool, - }, - ParticipantProject { - project_id: u64, - worktree_root_names: Vec, - host_user_id: u64, - is_last: bool, - }, - ParticipantScreen { - peer_id: PeerId, - is_last: bool, - }, - IncomingRequest(Arc), - OutgoingRequest(Arc), - Contact { - contact: Arc, - calling: bool, - }, -} - -impl PartialEq for ContactEntry { - fn eq(&self, other: &Self) -> bool { - match self { - ContactEntry::Header(section_1) => { - if let ContactEntry::Header(section_2) = other { - return section_1 == section_2; - } - } - ContactEntry::CallParticipant { user: user_1, .. } => { - if let ContactEntry::CallParticipant { user: user_2, .. } = other { - return user_1.id == user_2.id; - } - } - ContactEntry::ParticipantProject { - project_id: project_id_1, - .. - } => { - if let ContactEntry::ParticipantProject { - project_id: project_id_2, - .. - } = other - { - return project_id_1 == project_id_2; - } - } - ContactEntry::ParticipantScreen { - peer_id: peer_id_1, .. - } => { - if let ContactEntry::ParticipantScreen { - peer_id: peer_id_2, .. - } = other - { - return peer_id_1 == peer_id_2; - } - } - ContactEntry::IncomingRequest(user_1) => { - if let ContactEntry::IncomingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::OutgoingRequest(user_1) => { - if let ContactEntry::OutgoingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::Contact { - contact: contact_1, .. - } => { - if let ContactEntry::Contact { - contact: contact_2, .. - } = other - { - return contact_1.user.id == contact_2.user.id; - } - } - } - false - } -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RequestContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RemoveContact { - user_id: u64, - github_login: String, -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub enum Event { - ToggleContactFinder, - Dismissed, -} - -pub struct ContactList { - entries: Vec, - match_candidates: Vec, - list_state: ListState, - project: ModelHandle, - workspace: WeakViewHandle, - user_store: ModelHandle, - filter_editor: ViewHandle, - collapsed_sections: Vec
, - selection: Option, - _subscriptions: Vec, -} - -impl ContactList { - pub fn new( - project: ModelHandle, - user_store: ModelHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| { - theme.contact_list.user_query_editor.clone() - })), - cx, - ); - editor.set_placeholder_text("Filter contacts", cx); - editor - }); - - cx.subscribe(&filter_editor, |this, _, event, cx| { - if let editor::Event::BufferEdited = event { - let query = this.filter_editor.read(cx).text(cx); - if !query.is_empty() { - this.selection.take(); - } - this.update_entries(cx); - if !query.is_empty() { - this.selection = this - .entries - .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_))); - } - } - }) - .detach(); - - let list_state = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { - let theme = theme::current(cx).clone(); - let is_selected = this.selection == Some(ix); - let current_project_id = this.project.read(cx).remote_id(); - - match &this.entries[ix] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( - *section, - &theme.contact_list, - is_selected, - is_collapsed, - cx, - ) - } - ContactEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant( - user, - *is_pending, - is_selected, - &theme.contact_list, - ) - } - ContactEntry::ParticipantProject { - project_id, - worktree_root_names, - host_user_id, - is_last, - } => Self::render_participant_project( - *project_id, - worktree_root_names, - *host_user_id, - Some(*project_id) == current_project_id, - *is_last, - is_selected, - &theme.contact_list, - cx, - ), - ContactEntry::ParticipantScreen { peer_id, is_last } => { - Self::render_participant_screen( - *peer_id, - *is_last, - is_selected, - &theme.contact_list, - cx, - ) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contact_list, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contact_list, - false, - is_selected, - cx, - ), - ContactEntry::Contact { contact, calling } => Self::render_contact( - contact, - *calling, - &this.project, - &theme.contact_list, - is_selected, - cx, - ), - } - }); - - let active_call = ActiveCall::global(cx); - let mut subscriptions = Vec::new(); - subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); - - let mut this = Self { - list_state, - selection: None, - collapsed_sections: Default::default(), - entries: Default::default(), - match_candidates: Default::default(), - filter_editor, - _subscriptions: subscriptions, - project, - workspace, - user_store, - }; - this.update_entries(cx); - this - } - - pub fn editor_text(&self, cx: &AppContext) -> String { - self.filter_editor.read(cx).text(cx) - } - - pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext) -> Self { - self.filter_editor - .update(cx, |picker, cx| picker.set_text(editor_text, cx)); - self - } - - fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - let user_id = request.user_id; - let github_login = &request.github_login; - let user_store = self.user_store.clone(); - let prompt_message = format!( - "Are you sure you want to remove \"{}\" from your contacts?", - github_login - ); - let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); - let window_id = cx.window_id(); - cx.spawn(|_, mut cx| async move { - if answer.next().await == Some(0) { - if let Err(e) = user_store - .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) - .await - { - cx.prompt( - window_id, - PromptLevel::Info, - &format!("Failed to remove contact: {}", e), - &["Ok"], - ); - } - } - }) - .detach(); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } - - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - let did_clear = self.filter_editor.update(cx, |editor, cx| { - if editor.buffer().read(cx).len(cx) > 0 { - editor.set_text("", cx); - true - } else { - false - } - }); - - if !did_clear { - cx.emit(Event::Dismissed); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if self.entries.len() > ix + 1 { - self.selection = Some(ix + 1); - } - } else if !self.entries.is_empty() { - self.selection = Some(0); - } - self.list_state.reset(self.entries.len()); - if let Some(ix) = self.selection { - self.list_state.scroll_to(ListOffset { - item_ix: ix, - offset_in_item: 0., - }); - } - cx.notify(); - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if ix > 0 { - self.selection = Some(ix - 1); - } else { - self.selection = None; - } - } - self.list_state.reset(self.entries.len()); - if let Some(ix) = self.selection { - self.list_state.scroll_to(ListOffset { - item_ix: ix, - offset_in_item: 0., - }); - } - cx.notify(); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ContactEntry::Header(section) => { - self.toggle_expanded(*section, cx); - } - ContactEntry::Contact { contact, calling } => { - if contact.online && !contact.busy && !calling { - self.call(contact.user.id, Some(self.project.clone()), cx); - } - } - ContactEntry::ParticipantProject { - project_id, - host_user_id, - .. - } => { - if let Some(workspace) = self.workspace.upgrade(cx) { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project( - *project_id, - *host_user_id, - app_state, - cx, - ) - .detach_and_log_err(cx); - } - } - ContactEntry::ParticipantScreen { peer_id, .. } => { - if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(*peer_id, cx) - }); - } - } - _ => {} - } - } - } - } - - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { - if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { - self.collapsed_sections.remove(ix); - } else { - self.collapsed_sections.push(section); - } - self.update_entries(cx); - } - - fn update_entries(&mut self, cx: &mut ViewContext) { - let user_store = self.user_store.read(cx); - let query = self.filter_editor.read(cx).text(cx); - let executor = cx.background().clone(); - - let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - let old_entries = mem::take(&mut self.entries); - - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - let mut participant_entries = Vec::new(); - - // Populate the active user. - if let Some(user) = user_store.current_user() { - self.match_candidates.clear(); - self.match_candidates.push(StringMatchCandidate { - id: 0, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - if !matches.is_empty() { - let user_id = user.id; - participant_entries.push(ContactEntry::CallParticipant { - user, - is_pending: false, - }); - let mut projects = room.local_participant().projects.iter().peekable(); - while let Some(project) = projects.next() { - participant_entries.push(ContactEntry::ParticipantProject { - project_id: project.id, - worktree_root_names: project.worktree_root_names.clone(), - host_user_id: user_id, - is_last: projects.peek().is_none(), - }); - } - } - } - - // Populate remote participants. - self.match_candidates.clear(); - self.match_candidates - .extend(room.remote_participants().iter().map(|(_, participant)| { - StringMatchCandidate { - id: participant.user.id as usize, - string: participant.user.github_login.clone(), - char_bag: participant.user.github_login.chars().collect(), - } - })); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - for mat in matches { - let user_id = mat.candidate_id as u64; - let participant = &room.remote_participants()[&user_id]; - participant_entries.push(ContactEntry::CallParticipant { - user: participant.user.clone(), - is_pending: false, - }); - let mut projects = participant.projects.iter().peekable(); - while let Some(project) = projects.next() { - participant_entries.push(ContactEntry::ParticipantProject { - project_id: project.id, - worktree_root_names: project.worktree_root_names.clone(), - host_user_id: participant.user.id, - is_last: projects.peek().is_none() && participant.video_tracks.is_empty(), - }); - } - if !participant.video_tracks.is_empty() { - participant_entries.push(ContactEntry::ParticipantScreen { - peer_id: participant.peer_id, - is_last: true, - }); - } - } - - // Populate pending participants. - self.match_candidates.clear(); - self.match_candidates - .extend( - room.pending_participants() - .iter() - .enumerate() - .map(|(id, participant)| StringMatchCandidate { - id, - string: participant.github_login.clone(), - char_bag: participant.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { - user: room.pending_participants()[mat.candidate_id].clone(), - is_pending: true, - })); - - if !participant_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::ActiveCall)); - if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(participant_entries); - } - } - } - - let mut request_entries = Vec::new(); - let incoming = user_store.incoming_contact_requests(); - if !incoming.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - incoming - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - ); - } - - let outgoing = user_store.outgoing_contact_requests(); - if !outgoing.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - outgoing - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - ); - } - - if !request_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::Requests)); - if !self.collapsed_sections.contains(&Section::Requests) { - self.entries.append(&mut request_entries); - } - } - - let contacts = user_store.contacts(); - if !contacts.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - contacts - .iter() - .enumerate() - .map(|(ix, contact)| StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }), - ); - - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - - let (mut online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - online_contacts.retain(|contact| { - let contact = &contacts[contact.candidate_id]; - !room.contains_participant(contact.user.id) - }); - } - - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section)); - if !self.collapsed_sections.contains(§ion) { - let active_call = &ActiveCall::global(cx).read(cx); - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact { - contact: contact.clone(), - calling: active_call.pending_invites().contains(&contact.user.id), - }); - } - } - } - } - } - - if let Some(prev_selected_entry) = prev_selected_entry { - self.selection.take(); - for (ix, entry) in self.entries.iter().enumerate() { - if *entry == prev_selected_entry { - self.selection = Some(ix); - break; - } - } - } - - let old_scroll_top = self.list_state.logical_scroll_top(); - self.list_state.reset(self.entries.len()); - - // Attempt to maintain the same scroll position. - if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { - let new_scroll_top = self - .entries - .iter() - .position(|entry| entry == old_top_entry) - .map(|item_ix| ListOffset { - item_ix, - offset_in_item: old_scroll_top.offset_in_item, - }) - .or_else(|| { - let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; - let item_ix = self - .entries - .iter() - .position(|entry| entry == entry_after_old_top)?; - Some(ListOffset { - item_ix, - offset_in_item: 0., - }) - }) - .or_else(|| { - let entry_before_old_top = - old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; - let item_ix = self - .entries - .iter() - .position(|entry| entry == entry_before_old_top)?; - Some(ListOffset { - item_ix, - offset_in_item: 0., - }) - }); - - self.list_state - .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); - } - - cx.notify(); - } - - fn render_call_participant( - user: &User, - is_pending: bool, - is_selected: bool, - theme: &theme::ContactList, - ) -> AnyElement { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true), - ) - .with_children(if is_pending { - Some( - Label::new("Calling", theme.calling_indicator.text.clone()) - .contained() - .with_style(theme.calling_indicator.container) - .aligned(), - ) - } else { - None - }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), - ) - .into_any() - } - - fn render_participant_project( - project_id: u64, - worktree_root_names: &[String], - host_user_id: u64, - is_current: bool, - is_last: bool, - is_selected: bool, - theme: &theme::ContactList, - cx: &mut ViewContext, - ) -> AnyElement { - enum JoinProject {} - - let font_cache = cx.font_cache(); - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; - let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - let project_name = if worktree_root_names.is_empty() { - "untitled".to_string() - } else { - worktree_root_names.join(", ") - }; - - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { - let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - let row = theme - .project_row - .in_state(is_selected) - .style_for(mouse_state); - - Flex::row() - .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - })) - .constrained() - .with_width(host_avatar_height), - ) - .with_child( - Label::new(project_name, row.name.text.clone()) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - }) - .with_cursor_style(if !is_current { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if !is_current { - if let Some(workspace) = this.workspace.upgrade(cx) { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project(project_id, host_user_id, app_state, cx) - .detach_and_log_err(cx); - } - } - }) - .into_any() - } - - fn render_participant_screen( - peer_id: PeerId, - is_last: bool, - is_selected: bool, - theme: &theme::ContactList, - cx: &mut ViewContext, - ) -> AnyElement { - enum OpenSharedScreen {} - - let font_cache = cx.font_cache(); - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; - let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - - MouseEventHandler::::new( - peer_id.as_u64() as usize, - cx, - |mouse_state, _| { - let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - let row = theme - .project_row - .in_state(is_selected) - .style_for(mouse_state); - - Flex::row() - .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - })) - .constrained() - .with_width(host_avatar_height), - ) - .with_child( - Svg::new("icons/disable_screen_sharing_12.svg") - .with_color(row.icon.color) - .constrained() - .with_width(row.icon.width) - .aligned() - .left() - .contained() - .with_style(row.icon.container), - ) - .with_child( - Label::new("Screen", row.name.text.clone()) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - }, - ) - .with_cursor_style(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() - } - - fn render_header( - section: Section, - theme: &theme::ContactList, - is_selected: bool, - is_collapsed: bool, - cx: &mut ViewContext, - ) -> AnyElement { - enum Header {} - enum LeaveCallContactList {} - - let header_style = theme - .header_row - .in_state(is_selected) - .style_for(&mut Default::default()); - let text = match section { - Section::ActiveCall => "Collaborators", - Section::Requests => "Contact Requests", - Section::Online => "Online", - Section::Offline => "Offline", - }; - let leave_call = if section == Section::ActiveCall { - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.leave_call.style_for(state); - Label::new("Leave Call", style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, |_, _, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }) - .aligned(), - ) - } else { - None - }; - - let icon_size = theme.section_icon_size; - MouseEventHandler::::new(section as usize, cx, |_, _| { - Flex::row() - .with_child( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .with_color(header_style.text.color) - .constrained() - .with_max_width(icon_size) - .with_max_height(icon_size) - .aligned() - .constrained() - .with_width(icon_size), - ) - .with_child( - Label::new(text, header_style.text.clone()) - .aligned() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true), - ) - .with_children(leave_call) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_expanded(section, cx); - }) - .into_any() - } - - fn render_contact( - contact: &Contact, - calling: bool, - project: &ModelHandle, - theme: &theme::ContactList, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - let online = contact.online; - let busy = contact.busy || calling; - let user_id = contact.user.id; - let github_login = contact.user.github_login.clone(); - let initial_project = project.clone(); - let mut event_handler = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { - Flex::row() - .with_children(contact.user.avatar.clone().map(|avatar| { - let status_badge = if contact.online { - Some( - Empty::new() - .collapsed() - .contained() - .with_style(if busy { - theme.contact_status_busy - } else { - theme.contact_status_free - }) - .aligned(), - ) - } else { - None - }; - Stack::new() - .with_child( - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left(), - ) - .with_children(status_badge) - })) - .with_child( - Label::new( - contact.user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::::new( - contact.user.id as usize, - cx, - |mouse_state, _| { - let button_style = theme.contact_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - }, - ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_contact( - &RemoveContact { - user_id, - github_login: github_login.clone(), - }, - cx, - ); - }) - .flex_float(), - ) - .with_children(if calling { - Some( - Label::new("Calling", theme.calling_indicator.text.clone()) - .contained() - .with_style(theme.calling_indicator.container) - .aligned(), - ) - } else { - None - }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), - ) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if online && !busy { - this.call(user_id, Some(initial_project.clone()), cx); - } - }); - - if online { - event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand); - } - - event_handler.into_any() - } - - fn render_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactList, - is_incoming: bool, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - enum Decline {} - enum Accept {} - enum Cancel {} - - let mut row = Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true), - ); - - let user_id = user.id; - let github_login = user.github_login.clone(); - let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - let button_spacing = theme.contact_button_spacing; - - if is_incoming { - row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state) - }; - render_icon_button(button_style, "icons/x_mark_8.svg").aligned() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.respond_to_contact_request( - &RespondToContactRequest { - user_id, - accept: false, - }, - cx, - ); - }) - .contained() - .with_margin_right(button_spacing), - ); - - row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state) - }; - render_icon_button(button_style, "icons/check_8.svg") - .aligned() - .flex_float() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.respond_to_contact_request( - &RespondToContactRequest { - user_id, - accept: true, - }, - cx, - ); - }), - ); - } else { - row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_contact( - &RemoveContact { - user_id, - github_login: github_login.clone(), - }, - cx, - ); - }) - .flex_float(), - ); - } - - row.constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), - ) - .into_any() - } - - fn call( - &mut self, - recipient_user_id: u64, - initial_project: Option>, - cx: &mut ViewContext, - ) { - ActiveCall::global(cx) - .update(cx, |call, cx| { - call.invite(recipient_user_id, initial_project, cx) - }) - .detach_and_log_err(cx); - } -} - -impl Entity for ContactList { - type Event = Event; -} - -impl View for ContactList { - fn ui_name() -> &'static str { - "ContactList" - } - - fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { - Self::reset_to_default_keymap_context(keymap); - keymap.add_identifier("menu"); - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum AddContact {} - let theme = theme::current(cx).clone(); - - Flex::column() - .with_child( - Flex::row() - // .with_child( - // ChildView::new(&self.filter_editor, cx) - // .contained() - // .with_style(theme.contact_list.user_query_editor.container) - // ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - render_icon_button( - &theme.contact_list.add_contact_button, - "icons/user_plus_16.svg", - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - cx.emit(Event::ToggleContactFinder) - }) - .with_tooltip::( - 0, - "Search for new contact".into(), - None, - theme.tooltip.clone(), - cx, - ), - ) - .constrained() - .with_height(theme.contact_list.user_query_editor_height), - ) - // .with_child(List::new(self.list_state.clone())) - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.focus(&self.filter_editor); - } - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.emit(Event::Dismissed); - } - } -} - -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() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) -} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 746238aaa9..533a5de159 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -271,8 +271,16 @@ impl> AnyElementState for ElementState { | ElementState::PostLayout { mut element, .. } | ElementState::PostPaint { mut element, .. } => { let (size, layout) = element.layout(constraint, view, cx); - debug_assert!(size.x().is_finite(), "Element for {:?} had infinite x size after layout", element.view_name()); - debug_assert!(size.y().is_finite(), "Element for {:?} had infinite x size after layout", element.view_name()); + debug_assert!( + size.x().is_finite(), + "Element for {:?} had infinite x size after layout", + element.view_name() + ); + debug_assert!( + size.y().is_finite(), + "Element for {:?} had infinite y size after layout", + element.view_name() + ); result = size; ElementState::PostLayout { diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 0ce63d088a..7ca258d166 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -5,10 +5,10 @@ export default function contacts_popover(): any { const theme = useTheme() return { - background: background(theme.middle), - corner_radius: 6, + // background: background(theme.middle), + // corner_radius: 6, padding: { top: 6, bottom: 6 }, - shadow: theme.popover_shadow, + // shadow: theme.popover_shadow, border: border(theme.middle), width: 300, height: 400, From 87dfce94ae5be393a5dca1f4584321d85b89f971 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 25 Jul 2023 10:02:01 -0700 Subject: [PATCH 011/326] Rename contact list theme to collab panel --- crates/collab_ui/src/panel.rs | 36 +++++++++---------- crates/collab_ui/src/panel/contact_finder.rs | 19 ++++------ crates/theme/src/theme.rs | 6 ++-- styles/src/style_tree/app.ts | 6 ++-- .../{contact_list.ts => collab_panel.ts} | 9 ++--- 5 files changed, 36 insertions(+), 40 deletions(-) rename styles/src/style_tree/{contact_list.ts => collab_panel.ts} (95%) diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 28cb57cf79..4fee5f66f1 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -126,7 +126,7 @@ impl CollabPanel { let filter_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( Some(Arc::new(|theme| { - theme.contact_list.user_query_editor.clone() + theme.collab_panel.user_query_editor.clone() })), cx, ); @@ -162,7 +162,7 @@ impl CollabPanel { let is_collapsed = this.collapsed_sections.contains(section); Self::render_header( *section, - &theme.contact_list, + &theme.collab_panel, is_selected, is_collapsed, cx, @@ -173,7 +173,7 @@ impl CollabPanel { user, *is_pending, is_selected, - &theme.contact_list, + &theme.collab_panel, ) } ContactEntry::ParticipantProject { @@ -188,7 +188,7 @@ impl CollabPanel { Some(*project_id) == current_project_id, *is_last, is_selected, - &theme.contact_list, + &theme.collab_panel, cx, ), ContactEntry::ParticipantScreen { peer_id, is_last } => { @@ -196,14 +196,14 @@ impl CollabPanel { *peer_id, *is_last, is_selected, - &theme.contact_list, + &theme.collab_panel, cx, ) } ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), - &theme.contact_list, + &theme.collab_panel, true, is_selected, cx, @@ -211,7 +211,7 @@ impl CollabPanel { ContactEntry::OutgoingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), - &theme.contact_list, + &theme.collab_panel, false, is_selected, cx, @@ -220,7 +220,7 @@ impl CollabPanel { contact, *calling, &this.project, - &theme.contact_list, + &theme.collab_panel, is_selected, cx, ), @@ -617,7 +617,7 @@ impl CollabPanel { user: &User, is_pending: bool, is_selected: bool, - theme: &theme::ContactList, + theme: &theme::CollabPanel, ) -> AnyElement { Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -666,7 +666,7 @@ impl CollabPanel { is_current: bool, is_last: bool, is_selected: bool, - theme: &theme::ContactList, + theme: &theme::CollabPanel, cx: &mut ViewContext, ) -> AnyElement { enum JoinProject {} @@ -765,7 +765,7 @@ impl CollabPanel { peer_id: PeerId, is_last: bool, is_selected: bool, - theme: &theme::ContactList, + theme: &theme::CollabPanel, cx: &mut ViewContext, ) -> AnyElement { enum OpenSharedScreen {} @@ -865,7 +865,7 @@ impl CollabPanel { fn render_header( section: Section, - theme: &theme::ContactList, + theme: &theme::CollabPanel, is_selected: bool, is_collapsed: bool, cx: &mut ViewContext, @@ -944,7 +944,7 @@ impl CollabPanel { contact: &Contact, calling: bool, project: &ModelHandle, - theme: &theme::ContactList, + theme: &theme::CollabPanel, is_selected: bool, cx: &mut ViewContext, ) -> AnyElement { @@ -1046,7 +1046,7 @@ impl CollabPanel { fn render_contact_request( user: Arc, user_store: ModelHandle, - theme: &theme::ContactList, + theme: &theme::CollabPanel, is_incoming: bool, is_selected: bool, cx: &mut ViewContext, @@ -1351,13 +1351,13 @@ impl View for CollabPanel { .with_child( ChildView::new(&self.filter_editor, cx) .contained() - .with_style(theme.contact_list.user_query_editor.container) + .with_style(theme.collab_panel.user_query_editor.container) .flex(1.0, true), ) .with_child( MouseEventHandler::::new(0, cx, |_, _| { render_icon_button( - &theme.contact_list.add_contact_button, + &theme.collab_panel.add_contact_button, "icons/user_plus_16.svg", ) }) @@ -1373,8 +1373,8 @@ impl View for CollabPanel { cx, ) .constrained() - .with_height(theme.contact_list.user_query_editor_height) - .with_width(theme.contact_list.user_query_editor_height), + .with_height(theme.collab_panel.user_query_editor_height) + .with_width(theme.collab_panel.user_query_editor_height), ) .constrained() .with_width(self.size(cx)), diff --git a/crates/collab_ui/src/panel/contact_finder.rs b/crates/collab_ui/src/panel/contact_finder.rs index 3264a144ed..a5868f8d2f 100644 --- a/crates/collab_ui/src/panel/contact_finder.rs +++ b/crates/collab_ui/src/panel/contact_finder.rs @@ -97,7 +97,7 @@ impl PickerDelegate for ContactFinderDelegate { selected: bool, cx: &gpui::AppContext, ) -> AnyElement> { - let theme = &theme::current(cx); + let theme = &theme::current(cx).contact_finder; let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); @@ -109,27 +109,22 @@ impl PickerDelegate for ContactFinderDelegate { ContactRequestStatus::RequestAccepted => None, }; let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { - &theme.contact_finder.disabled_contact_button + &theme.disabled_contact_button } else { - &theme.contact_finder.contact_button + &theme.contact_button }; - let style = theme - .contact_finder - .picker - .item - .in_state(selected) - .style_for(mouse_state); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); Flex::row() .with_children(user.avatar.clone().map(|avatar| { Image::from_data(avatar) - .with_style(theme.contact_finder.contact_avatar) + .with_style(theme.contact_avatar) .aligned() .left() })) .with_child( Label::new(user.github_login.clone(), style.label.clone()) .contained() - .with_style(theme.contact_finder.contact_username) + .with_style(theme.contact_username) .aligned() .left(), ) @@ -150,7 +145,7 @@ impl PickerDelegate for ContactFinderDelegate { .contained() .with_style(style.container) .constrained() - .with_height(theme.contact_finder.row_height) + .with_height(theme.row_height) .into_any() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 56b3b2d156..6673efac2d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -44,13 +44,13 @@ pub struct Theme { pub workspace: Workspace, pub context_menu: ContextMenu, pub contacts_popover: ContactsPopover, - pub contact_list: ContactList, pub toolbar_dropdown_menu: DropdownMenu, pub copilot: Copilot, - pub contact_finder: ContactFinder, + pub collab_panel: CollabPanel, pub project_panel: ProjectPanel, pub channels_panel: ChanelsPanelStyle, pub command_palette: CommandPalette, + pub contact_finder: ContactFinder, pub picker: Picker, pub editor: Editor, pub search: Search, @@ -220,7 +220,7 @@ pub struct ContactsPopover { } #[derive(Deserialize, Default, JsonSchema)] -pub struct ContactList { +pub struct CollabPanel { pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub add_contact_button: IconButton, diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index d504f8e623..6d7ed27884 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -1,4 +1,3 @@ -import contact_finder from "./contact_finder" import contacts_popover from "./contacts_popover" import command_palette from "./command_palette" import project_panel from "./project_panel" @@ -14,7 +13,8 @@ import simple_message_notification from "./simple_message_notification" import project_shared_notification from "./project_shared_notification" import tooltip from "./tooltip" import terminal from "./terminal" -import contact_list from "./contact_list" +import contact_finder from "./contact_finder" +import collab_panel from "./collab_panel" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" import welcome from "./welcome" @@ -49,8 +49,8 @@ export default function app(): any { project_panel: project_panel(), channels_panel: channels_panel(), contacts_popover: contacts_popover(), + collab_panel: collab_panel(), contact_finder: contact_finder(), - contact_list: contact_list(), toolbar_dropdown_menu: toolbar_dropdown_menu(), search: search(), shared_screen: shared_screen(), diff --git a/styles/src/style_tree/contact_list.ts b/styles/src/style_tree/collab_panel.ts similarity index 95% rename from styles/src/style_tree/contact_list.ts rename to styles/src/style_tree/collab_panel.ts index 1955231f59..c457468e20 100644 --- a/styles/src/style_tree/contact_list.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -7,6 +7,7 @@ import { } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" + export default function contacts_panel(): any { const theme = useTheme() @@ -49,7 +50,7 @@ export default function contacts_panel(): any { } return { - background: background(layer), + // background: background(layer), padding: { top: 12 }, user_query_editor: { background: background(layer, "on"), @@ -88,7 +89,7 @@ export default function contacts_panel(): any { left: side_padding, right: side_padding, }, - background: background(layer, "default"), // posiewic: breaking change + // background: background(layer, "default"), // posiewic: breaking change }, state: { hovered: { @@ -97,7 +98,7 @@ export default function contacts_panel(): any { clicked: { background: background(layer, "pressed"), }, - }, // hack, we want headerRow to be interactive for whatever reason. It probably shouldn't be interactive in the first place. + }, }), state: { active: { @@ -220,7 +221,7 @@ export default function contacts_panel(): any { base: interactive({ base: { ...project_row, - background: background(layer), + // background: background(layer), icon: { margin: { left: name_margin }, color: foreground(layer, "variant"), From 14fdcadcfc638b229ca72a26102e24edd545ed6a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 25 Jul 2023 13:01:36 -0700 Subject: [PATCH 012/326] Add seemingly-redundant export in theme src file to workaround theme build error --- styles/src/common.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/src/common.ts b/styles/src/common.ts index 054b283791..79fc23585f 100644 --- a/styles/src/common.ts +++ b/styles/src/common.ts @@ -1,5 +1,6 @@ import chroma from "chroma-js" export * from "./theme" +export * from "./theme/theme_config" export { chroma } export const font_families = { From fc491945351aa272946e2eb2e0b6e5dff394348c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 25 Jul 2023 17:29:09 -0700 Subject: [PATCH 013/326] Restructure collab panel, make contact finder into a normal modal --- crates/collab_ui/src/collab_titlebar_item.rs | 16 +- crates/collab_ui/src/panel.rs | 445 ++++++++++--------- crates/collab_ui/src/panel/contact_finder.rs | 2 +- crates/theme/src/theme.rs | 18 +- styles/src/style_tree/app.ts | 2 - styles/src/style_tree/collab_panel.ts | 87 +++- styles/src/style_tree/contacts_popover.ts | 9 - styles/src/style_tree/titlebar.ts | 4 + 8 files changed, 325 insertions(+), 258 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 0d273fd1b8..8a6bf5bc83 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,7 +1,6 @@ use crate::{ - contact_notification::ContactNotification, face_pile::FacePile, - toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, - ToggleScreenSharing, + contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute, + toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; @@ -355,6 +354,7 @@ impl CollabTitlebarItem { user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx); }); } + fn render_branches_popover_host<'a>( &'a self, _theme: &'a theme::Titlebar, @@ -368,8 +368,8 @@ impl CollabTitlebarItem { .flex(1., true) .contained() .constrained() - .with_width(theme.contacts_popover.width) - .with_height(theme.contacts_popover.height) + .with_width(theme.titlebar.menu.width) + .with_height(theme.titlebar.menu.height) }) .on_click(MouseButton::Left, |_, _, _| {}) .on_down_out(MouseButton::Left, move |_, this, cx| { @@ -390,6 +390,7 @@ impl CollabTitlebarItem { .into_any() }) } + fn render_project_popover_host<'a>( &'a self, _theme: &'a theme::Titlebar, @@ -403,8 +404,8 @@ impl CollabTitlebarItem { .flex(1., true) .contained() .constrained() - .with_width(theme.contacts_popover.width) - .with_height(theme.contacts_popover.height) + .with_width(theme.titlebar.menu.width) + .with_height(theme.titlebar.menu.height) }) .on_click(MouseButton::Left, |_, _, _| {}) .on_down_out(MouseButton::Left, move |_, this, cx| { @@ -424,6 +425,7 @@ impl CollabTitlebarItem { .into_any() }) } + pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext) { if self.branch_popover.take().is_none() { if let Some(workspace) = self.workspace.upgrade(cx) { diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 4fee5f66f1..e78f3ce22f 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -4,7 +4,7 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; use client::{proto::PeerId, Client, Contact, User, UserStore}; -use contact_finder::{build_contact_finder, ContactFinder}; +use contact_finder::build_contact_finder; use context_menu::ContextMenu; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -54,9 +54,6 @@ pub struct CollabPanel { has_focus: bool, pending_serialization: Task>, context_menu: ViewHandle, - contact_finder: Option>, - - // from contacts list filter_editor: ViewHandle, entries: Vec, selection: Option, @@ -84,14 +81,16 @@ pub enum Event { #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { ActiveCall, + Channels, Requests, + Contacts, Online, Offline, } #[derive(Clone)] enum ContactEntry { - Header(Section), + Header(Section, usize), CallParticipant { user: Arc, is_pending: bool, @@ -130,7 +129,7 @@ impl CollabPanel { })), cx, ); - editor.set_placeholder_text("Filter contacts", cx); + editor.set_placeholder_text("Filter channels, contacts", cx); editor }); @@ -145,7 +144,7 @@ impl CollabPanel { this.selection = this .entries .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_))); + .position(|entry| !matches!(entry, ContactEntry::Header(_, _))); } } }) @@ -158,11 +157,12 @@ impl CollabPanel { let current_project_id = this.project.read(cx).remote_id(); match &this.entries[ix] { - ContactEntry::Header(section) => { + ContactEntry::Header(section, depth) => { let is_collapsed = this.collapsed_sections.contains(section); Self::render_header( *section, - &theme.collab_panel, + &theme, + *depth, is_selected, is_collapsed, cx, @@ -234,7 +234,6 @@ impl CollabPanel { pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), filter_editor, - contact_finder: None, entries: Vec::default(), selection: None, user_store: workspace.user_store().clone(), @@ -431,128 +430,137 @@ impl CollabPanel { })); if !participant_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::ActiveCall)); + self.entries + .push(ContactEntry::Header(Section::ActiveCall, 0)); if !self.collapsed_sections.contains(&Section::ActiveCall) { self.entries.extend(participant_entries); } } } - let mut request_entries = Vec::new(); - let incoming = user_store.incoming_contact_requests(); - if !incoming.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - incoming - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - ); - } + self.entries + .push(ContactEntry::Header(Section::Channels, 0)); - let outgoing = user_store.outgoing_contact_requests(); - if !outgoing.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - outgoing - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - ); - } + self.entries + .push(ContactEntry::Header(Section::Contacts, 0)); - if !request_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::Requests)); - if !self.collapsed_sections.contains(&Section::Requests) { - self.entries.append(&mut request_entries); + if !self.collapsed_sections.contains(&Section::Contacts) { + let mut request_entries = Vec::new(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches.iter().map(|mat| { + ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone()) + }), + ); } - } - let contacts = user_store.contacts(); - if !contacts.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - contacts - .iter() - .enumerate() - .map(|(ix, contact)| StringMatchCandidate { + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches.iter().map(|mat| { + ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone()) + }), + ); + } + + if !request_entries.is_empty() { + self.entries + .push(ContactEntry::Header(Section::Requests, 1)); + if !self.collapsed_sections.contains(&Section::Requests) { + self.entries.append(&mut request_entries); + } + } + + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend(contacts.iter().enumerate().map(|(ix, contact)| { + StringMatchCandidate { id: ix, string: contact.user.github_login.clone(), char_bag: contact.user.github_login.chars().collect(), - }), - ); + } + })); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); - let (mut online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - online_contacts.retain(|contact| { - let contact = &contacts[contact.candidate_id]; - !room.contains_participant(contact.user.id) - }); - } + let (mut online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + online_contacts.retain(|contact| { + let contact = &contacts[contact.candidate_id]; + !room.contains_participant(contact.user.id) + }); + } - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section)); - if !self.collapsed_sections.contains(§ion) { - let active_call = &ActiveCall::global(cx).read(cx); - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact { - contact: contact.clone(), - calling: active_call.pending_invites().contains(&contact.user.id), - }); + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ContactEntry::Header(section, 1)); + if !self.collapsed_sections.contains(§ion) { + let active_call = &ActiveCall::global(cx).read(cx); + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ContactEntry::Contact { + contact: contact.clone(), + calling: active_call + .pending_invites() + .contains(&contact.user.id), + }); + } } } } @@ -865,7 +873,8 @@ impl CollabPanel { fn render_header( section: Section, - theme: &theme::CollabPanel, + theme: &theme::Theme, + depth: usize, is_selected: bool, is_collapsed: bool, cx: &mut ViewContext, @@ -873,69 +882,112 @@ impl CollabPanel { enum Header {} enum LeaveCallContactList {} - let header_style = theme - .header_row - .in_state(is_selected) - .style_for(&mut Default::default()); + let tooltip_style = &theme.tooltip; let text = match section { - Section::ActiveCall => "Collaborators", - Section::Requests => "Contact Requests", + Section::ActiveCall => "Current Call", + Section::Requests => "Requests", + Section::Contacts => "Contacts", + Section::Channels => "Channels", Section::Online => "Online", Section::Offline => "Offline", }; - let leave_call = if section == Section::ActiveCall { - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.leave_call.style_for(state); - Label::new("Leave Call", style.text.clone()) - .contained() - .with_style(style.container) + + enum AddContact {} + let button = match section { + Section::ActiveCall => Some( + MouseEventHandler::::new(0, cx, |_, _| { + render_icon_button( + &theme.collab_panel.leave_call_button, + "icons/radix/exit.svg", + ) }) + .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, _, cx| { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) .detach_and_log_err(cx); }) - .aligned(), - ) - } else { - None + .with_tooltip::( + 0, + "Leave call".into(), + None, + tooltip_style.clone(), + cx, + ), + ), + Section::Contacts => Some( + MouseEventHandler::::new(0, cx, |_, _| { + render_icon_button( + &theme.collab_panel.add_contact_button, + "icons/user_plus_16.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| { + this.toggle_contact_finder(cx); + }) + .with_tooltip::( + 0, + "Search for new contact".into(), + None, + tooltip_style.clone(), + cx, + ), + ), + _ => None, }; - let icon_size = theme.section_icon_size; - MouseEventHandler::::new(section as usize, cx, |_, _| { + let can_collapse = depth > 0; + let icon_size = (&theme.collab_panel).section_icon_size; + MouseEventHandler::::new(section as usize, cx, |state, _| { + let header_style = if depth > 0 { + &theme.collab_panel.subheader_row + } else { + &theme.collab_panel.header_row + } + .in_state(is_selected) + .style_for(state); + Flex::row() - .with_child( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .with_color(header_style.text.color) - .constrained() - .with_max_width(icon_size) - .with_max_height(icon_size) - .aligned() - .constrained() - .with_width(icon_size), - ) + .with_children(if can_collapse { + Some( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .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 + }) .with_child( Label::new(text, header_style.text.clone()) .aligned() .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) .flex(1., true), ) - .with_children(leave_call) + .with_children(button.map(|button| button.aligned().right())) .constrained() - .with_height(theme.row_height) + .with_height(theme.collab_panel.row_height) .contained() .with_style(header_style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_expanded(section, cx); + if can_collapse { + this.toggle_expanded(section, cx); + } }) .into_any() } @@ -954,7 +1006,7 @@ impl CollabPanel { let github_login = contact.user.github_login.clone(); let initial_project = project.clone(); let mut event_handler = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { + MouseEventHandler::::new(contact.user.id as usize, cx, |state, cx| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { let status_badge = if contact.online { @@ -1023,12 +1075,7 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), - ) + .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) }) .on_click(MouseButton::Left, move |_, this, cx| { if online && !busy { @@ -1147,11 +1194,6 @@ impl CollabPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.contact_finder.take().is_some() { - cx.notify(); - return; - } - let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { editor.set_text("", cx); @@ -1206,7 +1248,7 @@ impl CollabPanel { if let Some(selection) = self.selection { if let Some(entry) = self.entries.get(selection) { match entry { - ContactEntry::Header(section) => { + ContactEntry::Header(section, _) => { self.toggle_expanded(*section, cx); } ContactEntry::Contact { contact, calling } => { @@ -1253,19 +1295,17 @@ impl CollabPanel { } fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { - if self.contact_finder.take().is_none() { - let child = cx.add_view(|cx| { - let finder = build_contact_finder(self.user_store.clone(), cx); - finder.set_query(self.filter_editor.read(cx).text(cx), cx); - finder + if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |_, cx| { + cx.add_view(|cx| { + let finder = build_contact_finder(self.user_store.clone(), cx); + finder.set_query(self.filter_editor.read(cx).text(cx), cx); + finder + }) + }); }); - cx.focus(&child); - // self.subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { - // // PickerEvent::Dismiss => cx.emit(Event::Dismissed), - // })); - self.contact_finder = Some(child); } - cx.notify(); } fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { @@ -1338,44 +1378,19 @@ impl View for CollabPanel { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - enum AddContact {} - let theme = theme::current(cx).clone(); + let theme = &theme::current(cx).collab_panel; Stack::new() - .with_child(if let Some(finder) = &self.contact_finder { - ChildView::new(&finder, cx).into_any() - } else { + .with_child( Flex::column() .with_child( Flex::row() .with_child( ChildView::new(&self.filter_editor, cx) .contained() - .with_style(theme.collab_panel.user_query_editor.container) + .with_style(theme.user_query_editor.container) .flex(1.0, true), ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - render_icon_button( - &theme.collab_panel.add_contact_button, - "icons/user_plus_16.svg", - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.toggle_contact_finder(cx); - }) - .with_tooltip::( - 0, - "Search for new contact".into(), - None, - theme.tooltip.clone(), - cx, - ) - .constrained() - .with_height(theme.collab_panel.user_query_editor_height) - .with_width(theme.collab_panel.user_query_editor_height), - ) .constrained() .with_width(self.size(cx)), ) @@ -1386,10 +1401,12 @@ impl View for CollabPanel { .flex(1., true) .into_any(), ) + .contained() + .with_style(theme.container) .constrained() .with_width(self.size(cx)) - .into_any() - }) + .into_any(), + ) .with_child(ChildView::new(&self.context_menu, cx)) .into_any_named("channels panel") .into_any() @@ -1457,9 +1474,9 @@ impl Panel for CollabPanel { impl PartialEq for ContactEntry { fn eq(&self, other: &Self) -> bool { match self { - ContactEntry::Header(section_1) => { - if let ContactEntry::Header(section_2) = other { - return section_1 == section_2; + ContactEntry::Header(section_1, depth_1) => { + if let ContactEntry::Header(section_2, depth_2) = other { + return section_1 == section_2 && depth_1 == depth_2; } } ContactEntry::CallParticipant { user: user_1, .. } => { @@ -1520,9 +1537,9 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .constrained() .with_width(style.icon_width) .aligned() - .contained() - .with_style(style.container) .constrained() .with_width(style.button_width) .with_height(style.button_width) + .contained() + .with_style(style.container) } diff --git a/crates/collab_ui/src/panel/contact_finder.rs b/crates/collab_ui/src/panel/contact_finder.rs index a5868f8d2f..41fff2af43 100644 --- a/crates/collab_ui/src/panel/contact_finder.rs +++ b/crates/collab_ui/src/panel/contact_finder.rs @@ -22,7 +22,7 @@ pub fn build_contact_finder( }, cx, ) - .with_theme(|theme| theme.contact_finder.picker.clone()) + .with_theme(|theme| theme.picker.clone()) } pub struct ContactFinderDelegate { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 6673efac2d..c06a71d2db 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -43,7 +43,6 @@ pub struct Theme { pub meta: ThemeMeta, pub workspace: Workspace, pub context_menu: ContextMenu, - pub contacts_popover: ContactsPopover, pub toolbar_dropdown_menu: DropdownMenu, pub copilot: Copilot, pub collab_panel: CollabPanel, @@ -118,6 +117,7 @@ pub struct Titlebar { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, + pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, pub project_name_divider: ContainedText, pub git_menu_button: Toggleable>, @@ -144,6 +144,12 @@ pub struct Titlebar { pub user_menu: UserMenu, } +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct TitlebarMenu { + pub width: f32, + pub height: f32, +} + #[derive(Clone, Deserialize, Default, JsonSchema)] pub struct UserMenu { pub user_menu_button_online: UserMenuButton, @@ -212,19 +218,15 @@ pub struct CopilotAuthAuthorized { } #[derive(Deserialize, Default, JsonSchema)] -pub struct ContactsPopover { +pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, - pub height: f32, - pub width: f32, -} - -#[derive(Deserialize, Default, JsonSchema)] -pub struct CollabPanel { pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, + pub leave_call_button: IconButton, pub add_contact_button: IconButton, pub header_row: Toggleable>, + pub subheader_row: Toggleable>, pub leave_call: Interactive, pub contact_row: Toggleable>, pub row_height: f32, diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index 6d7ed27884..d017ce90ca 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -1,4 +1,3 @@ -import contacts_popover from "./contacts_popover" import command_palette from "./command_palette" import project_panel from "./project_panel" import search from "./search" @@ -48,7 +47,6 @@ export default function app(): any { project_diagnostics: project_diagnostics(), project_panel: project_panel(), channels_panel: channels_panel(), - contacts_popover: contacts_popover(), collab_panel: collab_panel(), contact_finder: contact_finder(), toolbar_dropdown_menu: toolbar_dropdown_menu(), diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index c457468e20..39ee9b610f 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -50,8 +50,10 @@ export default function contacts_panel(): any { } return { - // background: background(layer), - padding: { top: 12 }, + background: background(layer), + padding: { + top: 12, + }, user_query_editor: { background: background(layer, "on"), corner_radius: 6, @@ -68,12 +70,17 @@ export default function contacts_panel(): any { top: 4, }, margin: { - left: 6, + left: side_padding, + right: side_padding, }, }, user_query_editor_height: 33, add_contact_button: { - margin: { left: 6, right: 12 }, + color: foreground(layer, "on"), + button_width: 28, + icon_width: 16, + }, + leave_call_button: { color: foreground(layer, "on"), button_width: 28, icon_width: 16, @@ -83,13 +90,46 @@ export default function contacts_panel(): any { header_row: toggleable({ base: interactive({ base: { - ...text(layer, "mono", { size: "sm" }), + ...text(layer, "mono", { size: "sm", weight: "bold" }), margin: { top: 14 }, padding: { left: side_padding, right: side_padding, }, - // background: background(layer, "default"), // posiewic: breaking change + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }), + state: { + active: { + default: { + ...text(layer, "mono", "active", { size: "sm" }), + background: background(layer, "active"), + }, + hovered: { + background: background(layer, "hovered"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }, + }), + subheader_row: toggleable({ + base: interactive({ + base: { + ...text(layer, "mono", { size: "sm" }), + // margin: { top: 14 }, + padding: { + left: side_padding, + right: side_padding, + }, }, state: { hovered: { @@ -139,25 +179,38 @@ export default function contacts_panel(): any { }, }, }), - contact_row: { - inactive: { - default: { + contact_row: toggleable({ + base: interactive({ + base: { padding: { left: side_padding, right: side_padding, }, }, - }, - active: { - default: { - background: background(layer, "active"), - padding: { - left: side_padding, - right: side_padding, + state: { + hovered: { + background: background(layer, "hovered"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }), + state: { + active: { + default: { + ...text(layer, "mono", "active", { size: "sm" }), + background: background(layer, "active"), + }, + hovered: { + background: background(layer, "hovered"), + }, + clicked: { + background: background(layer, "pressed"), }, }, }, - }, + }), contact_avatar: { corner_radius: 10, width: 18, diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 7ca258d166..0e76bbb38a 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -4,13 +4,4 @@ import { background, border } from "./components" export default function contacts_popover(): any { const theme = useTheme() - return { - // background: background(theme.middle), - // corner_radius: 6, - padding: { top: 6, bottom: 6 }, - // shadow: theme.popover_shadow, - border: border(theme.middle), - width: 300, - height: 400, - } } diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index fe0c53e87d..a93bf376c0 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -178,6 +178,10 @@ export function titlebar(): any { left: 80, right: 0, }, + menu: { + width: 300, + height: 400, + }, // Project project_name_divider: text(theme.lowest, "sans", "variant"), From 4a088fc4aeb24401c8d011109f3430229934a056 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 25 Jul 2023 18:00:49 -0700 Subject: [PATCH 014/326] Make major collab panel headers non-interactive --- crates/collab_ui/src/panel.rs | 258 ++++++++++++++------------ crates/theme/src/theme.rs | 2 +- styles/src/style_tree/collab_panel.ts | 41 +--- 3 files changed, 146 insertions(+), 155 deletions(-) diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index e78f3ce22f..bf0397ec76 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -444,123 +444,122 @@ impl CollabPanel { self.entries .push(ContactEntry::Header(Section::Contacts, 0)); - if !self.collapsed_sections.contains(&Section::Contacts) { - let mut request_entries = Vec::new(); - let incoming = user_store.incoming_contact_requests(); - if !incoming.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - incoming - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches.iter().map(|mat| { - ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone()) - }), + let mut request_entries = Vec::new(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), ); - } + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); + } - let outgoing = user_store.outgoing_contact_requests(); - if !outgoing.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - outgoing - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches.iter().map(|mat| { - ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone()) - }), + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), ); - } + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } - if !request_entries.is_empty() { - self.entries - .push(ContactEntry::Header(Section::Requests, 1)); - if !self.collapsed_sections.contains(&Section::Requests) { - self.entries.append(&mut request_entries); - } + if !request_entries.is_empty() { + self.entries + .push(ContactEntry::Header(Section::Requests, 1)); + if !self.collapsed_sections.contains(&Section::Requests) { + self.entries.append(&mut request_entries); } + } - let contacts = user_store.contacts(); - if !contacts.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend(contacts.iter().enumerate().map(|(ix, contact)| { - StringMatchCandidate { + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { id: ix, string: contact.user.github_login.clone(), char_bag: contact.user.github_login.chars().collect(), - } - })); + }), + ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); - let (mut online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - online_contacts.retain(|contact| { - let contact = &contacts[contact.candidate_id]; - !room.contains_participant(contact.user.id) - }); - } + let (mut online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + online_contacts.retain(|contact| { + let contact = &contacts[contact.candidate_id]; + !room.contains_participant(contact.user.id) + }); + } - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section, 1)); - if !self.collapsed_sections.contains(§ion) { - let active_call = &ActiveCall::global(cx).read(cx); - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact { - contact: contact.clone(), - calling: active_call - .pending_invites() - .contains(&contact.user.id), - }); - } + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ContactEntry::Header(section, 1)); + if !self.collapsed_sections.contains(§ion) { + let active_call = &ActiveCall::global(cx).read(cx); + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ContactEntry::Contact { + contact: contact.clone(), + calling: active_call.pending_invites().contains(&contact.user.id), + }); } } } @@ -940,13 +939,15 @@ impl CollabPanel { let can_collapse = depth > 0; let icon_size = (&theme.collab_panel).section_icon_size; MouseEventHandler::::new(section as usize, cx, |state, _| { - let header_style = if depth > 0 { - &theme.collab_panel.subheader_row + let header_style = if can_collapse { + theme + .collab_panel + .subheader_row + .in_state(is_selected) + .style_for(state) } else { &theme.collab_panel.header_row - } - .in_state(is_selected) - .style_for(state); + }; Flex::row() .with_children(if can_collapse { @@ -1209,13 +1210,15 @@ impl CollabPanel { } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if self.entries.len() > ix + 1 { - self.selection = Some(ix + 1); + let mut ix = self.selection.map_or(0, |ix| ix + 1); + while let Some(entry) = self.entries.get(ix) { + if entry.is_selectable() { + self.selection = Some(ix); + break; } - } else if !self.entries.is_empty() { - self.selection = Some(0); + ix += 1; } + self.list_state.reset(self.entries.len()); if let Some(ix) = self.selection { self.list_state.scroll_to(ListOffset { @@ -1227,13 +1230,18 @@ impl CollabPanel { } fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if ix > 0 { - self.selection = Some(ix - 1); - } else { - self.selection = None; + if let Some(mut ix) = self.selection.take() { + while ix > 0 { + ix -= 1; + if let Some(entry) = self.entries.get(ix) { + if entry.is_selectable() { + self.selection = Some(ix); + break; + } + } } } + self.list_state.reset(self.entries.len()); if let Some(ix) = self.selection { self.list_state.scroll_to(ListOffset { @@ -1471,6 +1479,16 @@ impl Panel for CollabPanel { } } +impl ContactEntry { + fn is_selectable(&self) -> bool { + if let ContactEntry::Header(_, 0) = self { + false + } else { + true + } + } +} + impl PartialEq for ContactEntry { fn eq(&self, other: &Self) -> bool { match self { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c06a71d2db..e13c8daafc 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -225,7 +225,7 @@ pub struct CollabPanel { pub user_query_editor_height: f32, pub leave_call_button: IconButton, pub add_contact_button: IconButton, - pub header_row: Toggleable>, + pub header_row: ContainedText, 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 39ee9b610f..4f847081ab 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -87,45 +87,18 @@ export default function contacts_panel(): any { }, row_height: 28, section_icon_size: 8, - header_row: toggleable({ - base: interactive({ - base: { - ...text(layer, "mono", { size: "sm", weight: "bold" }), - margin: { top: 14 }, - padding: { - left: side_padding, - right: side_padding, - }, - }, - state: { - hovered: { - background: background(layer, "hovered"), - }, - clicked: { - background: background(layer, "pressed"), - }, - }, - }), - state: { - active: { - default: { - ...text(layer, "mono", "active", { size: "sm" }), - background: background(layer, "active"), - }, - hovered: { - background: background(layer, "hovered"), - }, - clicked: { - background: background(layer, "pressed"), - }, - }, + header_row: { + ...text(layer, "mono", { size: "sm", weight: "bold" }), + margin: { top: 14 }, + padding: { + left: side_padding, + right: side_padding, }, - }), + }, subheader_row: toggleable({ base: interactive({ base: { ...text(layer, "mono", { size: "sm" }), - // margin: { top: 14 }, padding: { left: side_padding, right: side_padding, From 1549c2274f3160a99199efb53b47faafa4182625 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 26 Jul 2023 11:11:48 -0700 Subject: [PATCH 015/326] Create channel adding modal --- crates/collab_ui/src/panel.rs | 101 ++++++++++++++++-------- crates/theme/src/theme.rs | 13 +-- styles/src/style_tree/app.ts | 2 - styles/src/style_tree/channels_panel.ts | 12 --- styles/src/style_tree/collab_panel.ts | 5 ++ 5 files changed, 76 insertions(+), 57 deletions(-) delete mode 100644 styles/src/style_tree/channels_panel.ts diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bf0397ec76..bc79694d53 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -1,3 +1,4 @@ +mod channel_modal; mod contact_finder; mod panel_settings; @@ -16,7 +17,10 @@ use gpui::{ Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, }, - geometry::{rect::RectF, vector::vec2f}, + geometry::{ + rect::RectF, + vector::vec2f, + }, platform::{CursorStyle, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -34,6 +38,8 @@ use workspace::{ Workspace, }; +use self::channel_modal::ChannelModal; + actions!(collab_panel, [ToggleFocus]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -41,6 +47,7 @@ const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); + channel_modal::init(cx); cx.add_action(CollabPanel::cancel); cx.add_action(CollabPanel::select_next); @@ -880,6 +887,7 @@ impl CollabPanel { ) -> AnyElement { enum Header {} enum LeaveCallContactList {} + enum AddChannel {} let tooltip_style = &theme.tooltip; let text = match section { @@ -933,6 +941,22 @@ impl CollabPanel { cx, ), ), + Section::Channels => Some( + MouseEventHandler::::new(0, cx, |_, _| { + render_icon_button(&theme.collab_panel.add_contact_button, "icons/plus_16.svg") + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| { + this.toggle_channel_finder(cx); + }) + .with_tooltip::( + 0, + "Add or join a channel".into(), + None, + tooltip_style.clone(), + cx, + ), + ), _ => None, }; @@ -1316,6 +1340,14 @@ impl CollabPanel { } } + fn toggle_channel_finder(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| ChannelModal::new(cx))); + }); + } + } + fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( @@ -1388,36 +1420,43 @@ impl View for CollabPanel { fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let theme = &theme::current(cx).collab_panel; - Stack::new() - .with_child( - Flex::column() - .with_child( - Flex::row() - .with_child( - ChildView::new(&self.filter_editor, cx) - .contained() - .with_style(theme.user_query_editor.container) - .flex(1.0, true), - ) - .constrained() - .with_width(self.size(cx)), - ) - .with_child( - List::new(self.list_state.clone()) - .constrained() - .with_width(self.size(cx)) - .flex(1., true) - .into_any(), - ) - .contained() - .with_style(theme.container) - .constrained() - .with_width(self.size(cx)) - .into_any(), - ) - .with_child(ChildView::new(&self.context_menu, cx)) - .into_any_named("channels panel") - .into_any() + enum PanelFocus {} + MouseEventHandler::::new(0, cx, |_, cx| { + Stack::new() + .with_child( + Flex::column() + .with_child( + Flex::row() + .with_child( + ChildView::new(&self.filter_editor, cx) + .contained() + .with_style(theme.user_query_editor.container) + .flex(1.0, true), + ) + .constrained() + .with_width(self.size(cx)), + ) + .with_child( + List::new(self.list_state.clone()) + .constrained() + .with_width(self.size(cx)) + .flex(1., true) + .into_any(), + ) + .contained() + .with_style(theme.container) + .constrained() + .with_width(self.size(cx)) + .into_any(), + ) + .with_child(ChildView::new(&self.context_menu, cx)) + .into_any() + }) + .on_click(MouseButton::Left, |_, v, cx| { + cx.focus_self() + }) + .into_any_named("channels panel") + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e13c8daafc..3de878118e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -47,7 +47,6 @@ pub struct Theme { pub copilot: Copilot, pub collab_panel: CollabPanel, pub project_panel: ProjectPanel, - pub channels_panel: ChanelsPanelStyle, pub command_palette: CommandPalette, pub contact_finder: ContactFinder, pub picker: Picker, @@ -225,6 +224,7 @@ pub struct CollabPanel { pub user_query_editor_height: f32, pub leave_call_button: IconButton, pub add_contact_button: IconButton, + pub add_channel_button: IconButton, pub header_row: ContainedText, pub subheader_row: Toggleable>, pub leave_call: Interactive, @@ -1064,17 +1064,6 @@ pub struct Contained { contained: T, } -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct FlexStyle { - // Between item spacing - item_spacing: f32, -} - -#[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ChanelsPanelStyle { - pub contacts_header: TextStyle, -} - #[derive(Clone, Deserialize, Default, JsonSchema)] pub struct SavedConversation { pub container: Interactive, diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index d017ce90ca..fab751d0d1 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -23,7 +23,6 @@ import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" import { useTheme } from "../common" -import channels_panel from "./channels_panel" export default function app(): any { const theme = useTheme() @@ -46,7 +45,6 @@ export default function app(): any { editor: editor(), project_diagnostics: project_diagnostics(), project_panel: project_panel(), - channels_panel: channels_panel(), collab_panel: collab_panel(), contact_finder: contact_finder(), toolbar_dropdown_menu: toolbar_dropdown_menu(), diff --git a/styles/src/style_tree/channels_panel.ts b/styles/src/style_tree/channels_panel.ts deleted file mode 100644 index 126bbbe18c..0000000000 --- a/styles/src/style_tree/channels_panel.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - text, -} from "./components" -import { useTheme } from "../theme" -export default function channels_panel(): any { - const theme = useTheme() - - - return { - contacts_header: text(theme.middle, "sans", "variant", { size: "lg" }), - } -} diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 4f847081ab..8e817add3f 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -80,6 +80,11 @@ export default function contacts_panel(): any { button_width: 28, icon_width: 16, }, + add_channel_button: { + color: foreground(layer, "on"), + button_width: 28, + icon_width: 16, + }, leave_call_button: { color: foreground(layer, "on"), button_width: 28, From 40c293e184a40a054516250045528fd5a3d20c21 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 26 Jul 2023 15:50:01 -0700 Subject: [PATCH 016/326] Add channel_modal file --- crates/collab_ui/src/panel/channel_modal.rs | 95 +++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 crates/collab_ui/src/panel/channel_modal.rs diff --git a/crates/collab_ui/src/panel/channel_modal.rs b/crates/collab_ui/src/panel/channel_modal.rs new file mode 100644 index 0000000000..562536d58c --- /dev/null +++ b/crates/collab_ui/src/panel/channel_modal.rs @@ -0,0 +1,95 @@ +use editor::Editor; +use gpui::{elements::*, AnyViewHandle, Entity, View, ViewContext, ViewHandle, AppContext}; +use menu::Cancel; +use workspace::{item::ItemHandle, Modal}; + +pub fn init(cx: &mut AppContext) { + cx.add_action(ChannelModal::cancel) +} + +pub struct ChannelModal { + has_focus: bool, + input_editor: ViewHandle, +} + +pub enum Event { + Dismiss, +} + +impl Entity for ChannelModal { + type Event = Event; +} + +impl ChannelModal { + pub fn new(cx: &mut ViewContext) -> Self { + let input_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line(None, cx); + editor.set_placeholder_text("Create or add a channel", cx); + editor + }); + + ChannelModal { + has_focus: false, + input_editor, + } + } + + pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.dismiss(cx); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismiss) + } +} + +impl View for ChannelModal { + fn ui_name() -> &'static str { + "Channel Modal" + } + + fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { + let style = theme::current(cx).editor.hint_diagnostic.message.clone(); + let modal_container = theme::current(cx).picker.container.clone(); + + enum ChannelModal {} + MouseEventHandler::::new(0, cx, |_, cx| { + Flex::column() + .with_child(ChildView::new(self.input_editor.as_any(), cx)) + .with_child(Label::new("ADD OR BROWSE CHANNELS HERE", style)) + .contained() + .with_style(modal_container) + .constrained() + .with_max_width(540.) + .with_max_height(420.) + + }) + .on_click(gpui::platform::MouseButton::Left, |_, _, _| {}) // Capture click and down events + .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| { + v.dismiss(cx) + }).into_any_named("channel modal") + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + if cx.is_self_focused() { + cx.focus(&self.input_editor); + } + } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl Modal for ChannelModal { + fn has_focus(&self) -> bool { + self.has_focus + } + + fn dismiss_on_event(event: &Self::Event) -> bool { + match event { + Event::Dismiss => true, + } + } +} From bb70901e715afecdc2d7652e4df81644180b4025 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 26 Jul 2023 17:20:43 -0700 Subject: [PATCH 017/326] WIP --- .../20221109000000_test_schema.sql | 26 ++++ .../20230727150500_add_channels.sql | 19 +++ crates/collab/src/db.rs | 139 +++++++++++++++++- crates/collab/src/db/channel.rs | 39 +++++ crates/collab/src/db/channel_member.rs | 59 ++++++++ crates/collab/src/db/channel_parent.rs | 13 ++ crates/collab/src/db/room.rs | 6 + crates/collab/src/db/user.rs | 8 + crates/collab_ui/src/panel.rs | 10 +- 9 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 crates/collab/migrations/20230727150500_add_channels.sql create mode 100644 crates/collab/src/db/channel.rs create mode 100644 crates/collab/src/db/channel_member.rs create mode 100644 crates/collab/src/db/channel_parent.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index c690b6148a..a446f6b440 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -184,3 +184,29 @@ CREATE UNIQUE INDEX "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); + +CREATE TABLE "channels" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + -- "id_path" TEXT NOT NULL, + "name" VARCHAR NOT NULL, + "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now +) + +CREATE TABLE "channel_parents" ( + "child_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "parent_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + PRIMARY KEY(child_id, parent_id) +) + +-- CREATE UNIQUE INDEX "index_channels_on_id_path" ON "channels" ("id_path"); + +CREATE TABLE "channel_members" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "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, + "updated_at" TIMESTAMP NOT NULL DEFAULT now +) + +CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); diff --git a/crates/collab/migrations/20230727150500_add_channels.sql b/crates/collab/migrations/20230727150500_add_channels.sql new file mode 100644 index 0000000000..a62eb0aaaf --- /dev/null +++ b/crates/collab/migrations/20230727150500_add_channels.sql @@ -0,0 +1,19 @@ +CREATE TABLE "channels" ( + "id" SERIAL PRIMARY KEY, + "id_path" TEXT NOT NULL, + "name" VARCHAR NOT NULL, + "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now +) + +CREATE UNIQUE INDEX "index_channels_on_id_path" ON "channels" ("id_path"); + +CREATE TABLE "channel_members" ( + "id" SERIAL PRIMARY KEY, + "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, + "updated_at" TIMESTAMP NOT NULL DEFAULT now +) + +CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e16fa9edb1..ca7227917c 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,4 +1,7 @@ mod access_token; +mod channel; +mod channel_member; +mod channel_parent; mod contact; mod follower; mod language_server; @@ -36,7 +39,7 @@ use sea_orm::{ DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement, TransactionTrait, }; -use sea_query::{Alias, Expr, OnConflict, Query}; +use sea_query::{Alias, Expr, OnConflict, Query, SelectStatement}; use serde::{Deserialize, Serialize}; pub use signup::{Invite, NewSignup, WaitlistSummary}; use sqlx::migrate::{Migrate, Migration, MigrationSource}; @@ -3027,6 +3030,138 @@ impl Database { .await } + // channels + + pub async fn get_channels(&self, user_id: UserId) -> Result> { + self.transaction(|tx| async move { + let tx = tx; + + let user = user::Model { + id: user_id, + ..Default::default() + }; + let mut channel_ids = user + .find_related(channel_member::Entity) + .select_only() + .column(channel_member::Column::ChannelId) + .all(&*tx) + .await; + + let descendants = Alias::new("descendants"); + let cte_referencing = SelectStatement::new() + .column(channel_parent::Column::ChildId) + .from(channel::Entity) + .and_where( + Expr::col(channel_parent::Column::ParentId) + .in_subquery(SelectStatement::new().from(descendants).take()) + ); + + /* + WITH RECURSIVE descendant_ids(id) AS ( + $1 + UNION ALL + SELECT child_id as id FROM channel_parents WHERE parent_id IN descendants + ) + SELECT * from channels where id in descendant_ids + */ + + + // WITH RECURSIVE descendants(id) AS ( + // // SQL QUERY FOR SELECTING Initial IDs + // UNION + // SELECT id FROM ancestors WHERE p.parent = id + // ) + // SELECT * FROM descendants; + + + + // let descendant_channel_ids = + + + + // let query = sea_query::Query::with().recursive(true); + + + for id_path in id_paths { + // + } + + + // zed/public/plugins + // zed/public/plugins/js + // zed/zed-livekit + // livekit/zed-livekit + // zed - 101 + // livekit - 500 + // zed-livekit - 510 + // public - 150 + // plugins - 200 + // js - 300 + // + // Channel, Parent - edges + // 510 - 500 + // 510 - 101 + // + // Given the channel 'Zed' (101) + // Select * from EDGES where parent = 101 => 510 + // + + + "SELECT * from channels where id_path like '$1?'" + + // https://www.postgresql.org/docs/current/queries-with.html + // https://www.sqlite.org/lang_with.html + + "SELECT channel_id from channel_ancestors where ancestor_id IN $()" + + // | channel_id | ancestor_ids | + // 150 150 + // 150 101 + // 200 101 + // 300 101 + // 200 150 + // 300 150 + // 300 200 + // + // // | channel_id | ancestor_ids | + // 150 101 + // 200 101 + // 300 101 + // 200 150 + // 300 [150, 200] + + channel::Entity::find() + .filter(channel::Column::IdPath.like(id_paths.unwrap())) + + dbg!(&id_paths.unwrap()[0].id_path); + + // let mut channel_members_by_channel_id = HashMap::new(); + // for channel_member in channel_members { + // channel_members_by_channel_id + // .entry(channel_member.channel_id) + // .or_insert_with(Vec::new) + // .push(channel_member); + // } + + // let mut channel_messages = channel_message::Entity::find() + // .filter(channel_message::Column::ChannelId.in_selection(channel_ids)) + // .all(&*tx) + // .await?; + + // let mut channel_messages_by_channel_id = HashMap::new(); + // for channel_message in channel_messages { + // channel_messages_by_channel_id + // .entry(channel_message.channel_id) + // .or_insert_with(Vec::new) + // .push(channel_message); + // } + + todo!(); + // Ok(channels) + }) + .await + } + async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, @@ -3400,6 +3535,8 @@ macro_rules! id_type { } id_type!(AccessTokenId); +id_type!(ChannelId); +id_type!(ChannelMemberId); id_type!(ContactId); id_type!(FollowerId); id_type!(RoomId); diff --git a/crates/collab/src/db/channel.rs b/crates/collab/src/db/channel.rs new file mode 100644 index 0000000000..ebf5c26ac8 --- /dev/null +++ b/crates/collab/src/db/channel.rs @@ -0,0 +1,39 @@ +use super::{ChannelId, RoomId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channels")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelId, + pub room_id: Option, + // pub id_path: String, +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_one = "super::room::Entity")] + Room, + #[sea_orm(has_many = "super::channel_member::Entity")] + Member, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Member.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Room.def() + } +} + +// impl Related for Entity { +// fn to() -> RelationDef { +// Relation::Follower.def() +// } +// } diff --git a/crates/collab/src/db/channel_member.rs b/crates/collab/src/db/channel_member.rs new file mode 100644 index 0000000000..cad7f3853d --- /dev/null +++ b/crates/collab/src/db/channel_member.rs @@ -0,0 +1,59 @@ +use crate::db::channel_member; + +use super::{ChannelId, ChannelMemberId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_members")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelMemberId, + pub channel_id: ChannelId, + pub user_id: UserId, +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +#[derive(Debug)] +pub struct UserToChannel; + +impl Linked for UserToChannel { + type FromEntity = super::user::Entity; + + type ToEntity = super::channel::Entity; + + fn link(&self) -> Vec { + vec![ + channel_member::Relation::User.def().rev(), + channel_member::Relation::Channel.def(), + ] + } +} diff --git a/crates/collab/src/db/channel_parent.rs b/crates/collab/src/db/channel_parent.rs new file mode 100644 index 0000000000..bf6cb44711 --- /dev/null +++ b/crates/collab/src/db/channel_parent.rs @@ -0,0 +1,13 @@ +use super::ChannelId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_parents")] +pub struct Model { + #[sea_orm(primary_key)] + pub child_id: ChannelId, + #[sea_orm(primary_key)] + pub parent_id: ChannelId, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/room.rs b/crates/collab/src/db/room.rs index c3e88670eb..c838d1273b 100644 --- a/crates/collab/src/db/room.rs +++ b/crates/collab/src/db/room.rs @@ -37,4 +37,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Follower.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/user.rs b/crates/collab/src/db/user.rs index c2b157bd0a..2d0e2fdf0b 100644 --- a/crates/collab/src/db/user.rs +++ b/crates/collab/src/db/user.rs @@ -26,6 +26,8 @@ pub enum Relation { RoomParticipant, #[sea_orm(has_many = "super::project::Entity")] HostedProjects, + #[sea_orm(has_many = "super::channel_member::Entity")] + ChannelMemberships, } impl Related for Entity { @@ -46,4 +48,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::ChannelMemberships.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bc79694d53..bdeac59af9 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -17,10 +17,7 @@ use gpui::{ Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, }, - geometry::{ - rect::RectF, - vector::vec2f, - }, + geometry::{rect::RectF, vector::vec2f}, platform::{CursorStyle, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -1452,11 +1449,8 @@ impl View for CollabPanel { .with_child(ChildView::new(&self.context_menu, cx)) .into_any() }) - .on_click(MouseButton::Left, |_, v, cx| { - cx.focus_self() - }) + .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) .into_any_named("channels panel") - } } From 26a94b5244503b46539d4dd5ee632a289974388a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 10:47:45 -0700 Subject: [PATCH 018/326] WIP: Channel CRUD --- .../20221109000000_test_schema.sql | 6 +- crates/collab/src/db.rs | 283 ++++++++++-------- crates/collab/src/db/channel.rs | 1 + crates/collab/src/tests.rs | 1 + 4 files changed, 163 insertions(+), 128 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index a446f6b440..ed7459e4a0 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -191,13 +191,13 @@ CREATE TABLE "channels" ( "name" VARCHAR NOT NULL, "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now -) +); CREATE TABLE "channel_parents" ( "child_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "parent_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, PRIMARY KEY(child_id, parent_id) -) +); -- CREATE UNIQUE INDEX "index_channels_on_id_path" ON "channels" ("id_path"); @@ -207,6 +207,6 @@ CREATE TABLE "channel_members" ( "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "admin" BOOLEAN NOT NULL DEFAULT false, "updated_at" TIMESTAMP NOT NULL DEFAULT now -) +); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ca7227917c..c8bec8a3f9 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,7 +1,7 @@ mod access_token; mod channel; mod channel_member; -mod channel_parent; +// mod channel_parent; mod contact; mod follower; mod language_server; @@ -3032,134 +3032,167 @@ impl Database { // channels - pub async fn get_channels(&self, user_id: UserId) -> Result> { + pub async fn create_channel(&self, name: &str) -> Result { + self.transaction(move |tx| async move { + let tx = tx; + + let channel = channel::ActiveModel { + name: ActiveValue::Set(name.to_string()), + ..Default::default() + }; + + let channel = channel.insert(&*tx).await?; + + Ok(channel.id) + }).await + } + + pub async fn add_channel_member(&self, channel_id: ChannelId, user_id: UserId) -> Result<()> { + self.transaction(move |tx| async move { + let tx = tx; + + let channel_membership = channel_member::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(user_id), + ..Default::default() + }; + + channel_membership.insert(&*tx).await?; + + Ok(()) + }).await + } + + pub async fn get_channels(&self, user_id: UserId) -> Vec { self.transaction(|tx| async move { let tx = tx; - let user = user::Model { - id: user_id, - ..Default::default() - }; - let mut channel_ids = user - .find_related(channel_member::Entity) - .select_only() - .column(channel_member::Column::ChannelId) - .all(&*tx) - .await; - - let descendants = Alias::new("descendants"); - let cte_referencing = SelectStatement::new() - .column(channel_parent::Column::ChildId) - .from(channel::Entity) - .and_where( - Expr::col(channel_parent::Column::ParentId) - .in_subquery(SelectStatement::new().from(descendants).take()) - ); - - /* - WITH RECURSIVE descendant_ids(id) AS ( - $1 - UNION ALL - SELECT child_id as id FROM channel_parents WHERE parent_id IN descendants - ) - SELECT * from channels where id in descendant_ids - */ - - - // WITH RECURSIVE descendants(id) AS ( - // // SQL QUERY FOR SELECTING Initial IDs - // UNION - // SELECT id FROM ancestors WHERE p.parent = id - // ) - // SELECT * FROM descendants; - - - - // let descendant_channel_ids = - - - - // let query = sea_query::Query::with().recursive(true); - - - for id_path in id_paths { - // - } - - - // zed/public/plugins - // zed/public/plugins/js - // zed/zed-livekit - // livekit/zed-livekit - // zed - 101 - // livekit - 500 - // zed-livekit - 510 - // public - 150 - // plugins - 200 - // js - 300 - // - // Channel, Parent - edges - // 510 - 500 - // 510 - 101 - // - // Given the channel 'Zed' (101) - // Select * from EDGES where parent = 101 => 510 - // - - - "SELECT * from channels where id_path like '$1?'" - - // https://www.postgresql.org/docs/current/queries-with.html - // https://www.sqlite.org/lang_with.html - - "SELECT channel_id from channel_ancestors where ancestor_id IN $()" - - // | channel_id | ancestor_ids | - // 150 150 - // 150 101 - // 200 101 - // 300 101 - // 200 150 - // 300 150 - // 300 200 - // - // // | channel_id | ancestor_ids | - // 150 101 - // 200 101 - // 300 101 - // 200 150 - // 300 [150, 200] - - channel::Entity::find() - .filter(channel::Column::IdPath.like(id_paths.unwrap())) - - dbg!(&id_paths.unwrap()[0].id_path); - - // let mut channel_members_by_channel_id = HashMap::new(); - // for channel_member in channel_members { - // channel_members_by_channel_id - // .entry(channel_member.channel_id) - // .or_insert_with(Vec::new) - // .push(channel_member); - // } - - // let mut channel_messages = channel_message::Entity::find() - // .filter(channel_message::Column::ChannelId.in_selection(channel_ids)) - // .all(&*tx) - // .await?; - - // let mut channel_messages_by_channel_id = HashMap::new(); - // for channel_message in channel_messages { - // channel_messages_by_channel_id - // .entry(channel_message.channel_id) - // .or_insert_with(Vec::new) - // .push(channel_message); - // } - - todo!(); - // Ok(channels) }) - .await + // let user = user::Model { + // id: user_id, + // ..Default::default() + // }; + // let mut channel_ids = user + // .find_related(channel_member::Entity) + // .select_only() + // .column(channel_member::Column::ChannelId) + // .all(&*tx) + // .await; + + // // let descendants = Alias::new("descendants"); + // // let cte_referencing = SelectStatement::new() + // // .column(channel_parent::Column::ChildId) + // // .from(channel::Entity) + // // .and_where( + // // Expr::col(channel_parent::Column::ParentId) + // // .in_subquery(SelectStatement::new().from(descendants).take()) + // // ); + + // // /* + // // WITH RECURSIVE descendant_ids(id) AS ( + // // $1 + // // UNION ALL + // // SELECT child_id as id FROM channel_parents WHERE parent_id IN descendants + // // ) + // // SELECT * from channels where id in descendant_ids + // // */ + + + // // // WITH RECURSIVE descendants(id) AS ( + // // // // SQL QUERY FOR SELECTING Initial IDs + // // // UNION + // // // SELECT id FROM ancestors WHERE p.parent = id + // // // ) + // // // SELECT * FROM descendants; + + + + // // // let descendant_channel_ids = + + + + // // // let query = sea_query::Query::with().recursive(true); + + + // // for id_path in id_paths { + // // // + // // } + + + // // // zed/public/plugins + // // // zed/public/plugins/js + // // // zed/zed-livekit + // // // livekit/zed-livekit + // // // zed - 101 + // // // livekit - 500 + // // // zed-livekit - 510 + // // // public - 150 + // // // plugins - 200 + // // // js - 300 + // // // + // // // Channel, Parent - edges + // // // 510 - 500 + // // // 510 - 101 + // // // + // // // Given the channel 'Zed' (101) + // // // Select * from EDGES where parent = 101 => 510 + // // // + + + // // "SELECT * from channels where id_path like '$1?'" + + // // // https://www.postgresql.org/docs/current/queries-with.html + // // // https://www.sqlite.org/lang_with.html + + // // "SELECT channel_id from channel_ancestors where ancestor_id IN $()" + + // // // | channel_id | ancestor_ids | + // // // 150 150 + // // // 150 101 + // // // 200 101 + // // // 300 101 + // // // 200 150 + // // // 300 150 + // // // 300 200 + // // // + // // // // | channel_id | ancestor_ids | + // // // 150 101 + // // // 200 101 + // // // 300 101 + // // // 200 150 + // // // 300 [150, 200] + + // // channel::Entity::find() + // // .filter(channel::Column::IdPath.like(id_paths.unwrap())) + + // // dbg!(&id_paths.unwrap()[0].id_path); + + // // // let mut channel_members_by_channel_id = HashMap::new(); + // // // for channel_member in channel_members { + // // // channel_members_by_channel_id + // // // .entry(channel_member.channel_id) + // // // .or_insert_with(Vec::new) + // // // .push(channel_member); + // // // } + + // // // let mut channel_messages = channel_message::Entity::find() + // // // .filter(channel_message::Column::ChannelId.in_selection(channel_ids)) + // // // .all(&*tx) + // // // .await?; + + // // // let mut channel_messages_by_channel_id = HashMap::new(); + // // // for channel_message in channel_messages { + // // // channel_messages_by_channel_id + // // // .entry(channel_message.channel_id) + // // // .or_insert_with(Vec::new) + // // // .push(channel_message); + // // // } + + // // todo!(); + // // // Ok(channels) + // Err(Error("not implemented")) + // }) + // .await } async fn transaction(&self, f: F) -> Result diff --git a/crates/collab/src/db/channel.rs b/crates/collab/src/db/channel.rs index ebf5c26ac8..f8e2c3b85b 100644 --- a/crates/collab/src/db/channel.rs +++ b/crates/collab/src/db/channel.rs @@ -6,6 +6,7 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, + pub name: String, pub room_id: Option, // pub id_path: String, } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index b1d0bedb2c..2e98cd9b4d 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -35,6 +35,7 @@ use workspace::Workspace; mod integration_tests; mod randomized_integration_tests; +mod channel_tests; struct TestServer { app_state: Arc, From 15631a6fd51ea5647bcc847ed51223cc9fdffd9c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 21:31:01 -0700 Subject: [PATCH 019/326] Add channel_tests.rs --- crates/collab/src/tests/channel_tests.rs | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 crates/collab/src/tests/channel_tests.rs diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs new file mode 100644 index 0000000000..24754adeb3 --- /dev/null +++ b/crates/collab/src/tests/channel_tests.rs @@ -0,0 +1,29 @@ +use gpui::{executor::Deterministic, TestAppContext}; +use std::sync::Arc; + +use super::TestServer; + +#[gpui::test] +async fn test_basic_channels(deterministic: Arc, cx: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx, "user_a").await; + let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); + let db = server._test_db.db(); + + let zed_id = db.create_channel("zed").await.unwrap(); + + db.add_channel_member(zed_id, a_id).await.unwrap(); + + let channels = db.get_channels(a_id).await; + + assert_eq!(channels, vec![zed_id]); +} + +/* +Linear things: +- A way of expressing progress to the team +- A way for us to agree on a scope +- A way to figure out what we're supposed to be doing + +*/ From 0998440bddf09a8425a6890dc8bc89052d62feb9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 28 Jul 2023 13:14:24 -0700 Subject: [PATCH 020/326] implement recursive channel query --- .../20221109000000_test_schema.sql | 3 - crates/collab/src/db.rs | 256 +++++++++--------- crates/collab/src/db/channel.rs | 1 - crates/collab/src/db/channel_parent.rs | 3 + crates/collab/src/tests/channel_tests.rs | 66 ++++- 5 files changed, 191 insertions(+), 138 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index ed7459e4a0..b397438e27 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -187,7 +187,6 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - -- "id_path" TEXT NOT NULL, "name" VARCHAR NOT NULL, "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now @@ -199,8 +198,6 @@ CREATE TABLE "channel_parents" ( PRIMARY KEY(child_id, parent_id) ); --- CREATE UNIQUE INDEX "index_channels_on_id_path" ON "channels" ("id_path"); - CREATE TABLE "channel_members" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index c8bec8a3f9..5755ed73e2 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,7 +1,7 @@ mod access_token; mod channel; mod channel_member; -// mod channel_parent; +mod channel_parent; mod contact; mod follower; mod language_server; @@ -39,7 +39,10 @@ use sea_orm::{ DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement, TransactionTrait, }; -use sea_query::{Alias, Expr, OnConflict, Query, SelectStatement}; +use sea_query::{ + Alias, ColumnRef, CommonTableExpression, Expr, OnConflict, Order, Query, QueryStatementWriter, + SelectStatement, UnionType, WithClause, +}; use serde::{Deserialize, Serialize}; pub use signup::{Invite, NewSignup, WaitlistSummary}; use sqlx::migrate::{Migrate, Migration, MigrationSource}; @@ -3032,7 +3035,11 @@ impl Database { // channels - pub async fn create_channel(&self, name: &str) -> Result { + pub async fn create_root_channel(&self, name: &str) -> Result { + self.create_channel(name, None).await + } + + pub async fn create_channel(&self, name: &str, parent: Option) -> Result { self.transaction(move |tx| async move { let tx = tx; @@ -3043,10 +3050,21 @@ impl Database { let channel = channel.insert(&*tx).await?; + if let Some(parent) = parent { + channel_parent::ActiveModel { + child_id: ActiveValue::Set(channel.id), + parent_id: ActiveValue::Set(parent), + } + .insert(&*tx) + .await?; + } + Ok(channel.id) - }).await + }) + .await } + // Property: Members are only pub async fn add_channel_member(&self, channel_id: ChannelId, user_id: UserId) -> Result<()> { self.transaction(move |tx| async move { let tx = tx; @@ -3060,139 +3078,108 @@ impl Database { channel_membership.insert(&*tx).await?; Ok(()) - }).await + }) + .await } - pub async fn get_channels(&self, user_id: UserId) -> Vec { + pub async fn get_channels(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { let tx = tx; + // This is the SQL statement we want to generate: + let sql = r#" + WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS ( + SELECT channel_id as child_id, NULL as parent_id, 0 + FROM channel_members + WHERE user_id = ? + UNION ALL + SELECT channel_parents.child_id, channel_parents.parent_id, channel_tree.depth + 1 + FROM channel_parents, channel_tree + WHERE channel_parents.parent_id = channel_tree.child_id + ) + SELECT channel_tree.child_id as id, channels.name, channel_tree.parent_id + FROM channel_tree + JOIN channels ON channels.id = channel_tree.child_id + ORDER BY channel_tree.depth; + "#; + + // let root_channel_ids_query = SelectStatement::new() + // .column(channel_member::Column::ChannelId) + // .expr(Expr::value("NULL")) + // .from(channel_member::Entity.table_ref()) + // .and_where( + // Expr::col(channel_member::Column::UserId) + // .eq(Expr::cust_with_values("?", vec![user_id])), + // ); + + // let build_tree_query = SelectStatement::new() + // .column(channel_parent::Column::ChildId) + // .column(channel_parent::Column::ParentId) + // .expr(Expr::col(Alias::new("channel_tree.depth")).add(1i32)) + // .from(Alias::new("channel_tree")) + // .and_where( + // Expr::col(channel_parent::Column::ParentId) + // .equals(Alias::new("channel_tree"), Alias::new("child_id")), + // ) + // .to_owned(); + + // let common_table_expression = CommonTableExpression::new() + // .query( + // root_channel_ids_query + // .union(UnionType::Distinct, build_tree_query) + // .to_owned(), + // ) + // .column(Alias::new("child_id")) + // .column(Alias::new("parent_id")) + // .column(Alias::new("depth")) + // .table_name(Alias::new("channel_tree")) + // .to_owned(); + + // let select = SelectStatement::new() + // .expr_as( + // Expr::col(Alias::new("channel_tree.child_id")), + // Alias::new("id"), + // ) + // .column(channel::Column::Name) + // .column(Alias::new("channel_tree.parent_id")) + // .from(Alias::new("channel_tree")) + // .inner_join( + // channel::Entity.table_ref(), + // Expr::eq( + // channel::Column::Id.into_expr(), + // Expr::tbl(Alias::new("channel_tree"), Alias::new("child_id")), + // ), + // ) + // .order_by(Alias::new("channel_tree.child_id"), Order::Asc) + // .to_owned(); + + // let with_clause = WithClause::new() + // .recursive(true) + // .cte(common_table_expression) + // .to_owned(); + + // let query = select.with(with_clause); + + // let query = SelectStatement::new() + // .column(ColumnRef::Asterisk) + // .from_subquery(query, Alias::new("channel_tree") + // .to_owned(); + + // let stmt = self.pool.get_database_backend().build(&query); + + let stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + vec![user_id.into()], + ); + + Ok(channel_parent::Entity::find() + .from_raw_sql(stmt) + .into_model::() + .all(&*tx) + .await?) }) - // let user = user::Model { - // id: user_id, - // ..Default::default() - // }; - // let mut channel_ids = user - // .find_related(channel_member::Entity) - // .select_only() - // .column(channel_member::Column::ChannelId) - // .all(&*tx) - // .await; - - // // let descendants = Alias::new("descendants"); - // // let cte_referencing = SelectStatement::new() - // // .column(channel_parent::Column::ChildId) - // // .from(channel::Entity) - // // .and_where( - // // Expr::col(channel_parent::Column::ParentId) - // // .in_subquery(SelectStatement::new().from(descendants).take()) - // // ); - - // // /* - // // WITH RECURSIVE descendant_ids(id) AS ( - // // $1 - // // UNION ALL - // // SELECT child_id as id FROM channel_parents WHERE parent_id IN descendants - // // ) - // // SELECT * from channels where id in descendant_ids - // // */ - - - // // // WITH RECURSIVE descendants(id) AS ( - // // // // SQL QUERY FOR SELECTING Initial IDs - // // // UNION - // // // SELECT id FROM ancestors WHERE p.parent = id - // // // ) - // // // SELECT * FROM descendants; - - - - // // // let descendant_channel_ids = - - - - // // // let query = sea_query::Query::with().recursive(true); - - - // // for id_path in id_paths { - // // // - // // } - - - // // // zed/public/plugins - // // // zed/public/plugins/js - // // // zed/zed-livekit - // // // livekit/zed-livekit - // // // zed - 101 - // // // livekit - 500 - // // // zed-livekit - 510 - // // // public - 150 - // // // plugins - 200 - // // // js - 300 - // // // - // // // Channel, Parent - edges - // // // 510 - 500 - // // // 510 - 101 - // // // - // // // Given the channel 'Zed' (101) - // // // Select * from EDGES where parent = 101 => 510 - // // // - - - // // "SELECT * from channels where id_path like '$1?'" - - // // // https://www.postgresql.org/docs/current/queries-with.html - // // // https://www.sqlite.org/lang_with.html - - // // "SELECT channel_id from channel_ancestors where ancestor_id IN $()" - - // // // | channel_id | ancestor_ids | - // // // 150 150 - // // // 150 101 - // // // 200 101 - // // // 300 101 - // // // 200 150 - // // // 300 150 - // // // 300 200 - // // // - // // // // | channel_id | ancestor_ids | - // // // 150 101 - // // // 200 101 - // // // 300 101 - // // // 200 150 - // // // 300 [150, 200] - - // // channel::Entity::find() - // // .filter(channel::Column::IdPath.like(id_paths.unwrap())) - - // // dbg!(&id_paths.unwrap()[0].id_path); - - // // // let mut channel_members_by_channel_id = HashMap::new(); - // // // for channel_member in channel_members { - // // // channel_members_by_channel_id - // // // .entry(channel_member.channel_id) - // // // .or_insert_with(Vec::new) - // // // .push(channel_member); - // // // } - - // // // let mut channel_messages = channel_message::Entity::find() - // // // .filter(channel_message::Column::ChannelId.in_selection(channel_ids)) - // // // .all(&*tx) - // // // .await?; - - // // // let mut channel_messages_by_channel_id = HashMap::new(); - // // // for channel_message in channel_messages { - // // // channel_messages_by_channel_id - // // // .entry(channel_message.channel_id) - // // // .or_insert_with(Vec::new) - // // // .push(channel_message); - // // // } - - // // todo!(); - // // // Ok(channels) - // Err(Error("not implemented")) - // }) - // .await + .await } async fn transaction(&self, f: F) -> Result @@ -3440,6 +3427,13 @@ pub struct NewUserResult { pub signup_device_id: Option, } +#[derive(FromQueryResult, Debug, PartialEq)] +pub struct Channel { + pub id: ChannelId, + pub name: String, + pub parent_id: Option, +} + fn random_invite_code() -> String { nanoid::nanoid!(16) } diff --git a/crates/collab/src/db/channel.rs b/crates/collab/src/db/channel.rs index f8e2c3b85b..48e5d50e3e 100644 --- a/crates/collab/src/db/channel.rs +++ b/crates/collab/src/db/channel.rs @@ -8,7 +8,6 @@ pub struct Model { pub id: ChannelId, pub name: String, pub room_id: Option, - // pub id_path: String, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/channel_parent.rs b/crates/collab/src/db/channel_parent.rs index bf6cb44711..b0072155a3 100644 --- a/crates/collab/src/db/channel_parent.rs +++ b/crates/collab/src/db/channel_parent.rs @@ -11,3 +11,6 @@ pub struct Model { } impl ActiveModelBehavior for ActiveModel {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 24754adeb3..8ab33adcbf 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,6 +1,8 @@ use gpui::{executor::Deterministic, TestAppContext}; use std::sync::Arc; +use crate::db::Channel; + use super::TestServer; #[gpui::test] @@ -11,13 +13,71 @@ async fn test_basic_channels(deterministic: Arc, cx: &mut TestApp let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); let db = server._test_db.db(); - let zed_id = db.create_channel("zed").await.unwrap(); + let zed_id = db.create_root_channel("zed").await.unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id)).await.unwrap(); + let livestreaming_id = db + .create_channel("livestreaming", Some(zed_id)) + .await + .unwrap(); + let replace_id = db.create_channel("replace", Some(zed_id)).await.unwrap(); + let rust_id = db.create_root_channel("rust").await.unwrap(); + let cargo_id = db.create_channel("cargo", Some(rust_id)).await.unwrap(); db.add_channel_member(zed_id, a_id).await.unwrap(); + db.add_channel_member(rust_id, a_id).await.unwrap(); - let channels = db.get_channels(a_id).await; + let channels = db.get_channels(a_id).await.unwrap(); + assert_eq!( + channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: cargo_id, + name: "cargo".to_string(), + parent_id: Some(rust_id), + } + ] + ); +} - assert_eq!(channels, vec![zed_id]); +#[gpui::test] +async fn test_block_cycle_creation(deterministic: Arc, cx: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx, "user_a").await; + let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); + let db = server._test_db.db(); + + let zed_id = db.create_root_channel("zed").await.unwrap(); + let first_id = db.create_channel("first", Some(zed_id)).await.unwrap(); + let second_id = db + .create_channel("second_id", Some(first_id)) + .await + .unwrap(); } /* From b53fb8633ef4cfa352bdb7970e59b8824a5a10a6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 24 Jul 2023 23:59:37 -0600 Subject: [PATCH 021/326] Fix vim selection to include entire range Co-Authored-By: Max Brunsfeld --- assets/keymaps/vim.json | 18 +- crates/editor/src/display_map.rs | 52 ++-- crates/editor/src/editor.rs | 6 +- crates/editor/src/element.rs | 19 +- crates/vim/src/motion.rs | 15 +- crates/vim/src/normal.rs | 13 +- crates/vim/src/normal/change.rs | 11 +- crates/vim/src/normal/delete.rs | 14 +- crates/vim/src/normal/substitute.rs | 4 +- crates/vim/src/normal/yank.rs | 5 +- crates/vim/src/object.rs | 6 + crates/vim/src/state.rs | 21 +- crates/vim/src/test.rs | 2 +- .../src/test/neovim_backed_test_context.rs | 95 ++++-- crates/vim/src/test/neovim_connection.rs | 95 +++--- crates/vim/src/test/vim_test_context.rs | 5 +- crates/vim/src/vim.rs | 25 +- crates/vim/src/visual.rs | 274 +++++++++++------- .../vim/test_data/test_enter_visual_mode.json | 28 +- crates/vim/test_data/test_visual_change.json | 34 ++- .../test_data/test_visual_word_object.json | 82 +++--- 21 files changed, 489 insertions(+), 335 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 94a271f037..a1b6d939f4 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -266,22 +266,8 @@ "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", - "v": [ - "vim::SwitchMode", - { - "Visual": { - "line": false - } - } - ], - "shift-v": [ - "vim::SwitchMode", - { - "Visual": { - "line": true - } - } - ], + "v": "vim::ToggleVisual", + "shift-v": "vim::ToggleVisualLine", "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6e04833f17..be0dd68adf 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -35,6 +35,12 @@ pub enum FoldStatus { Foldable, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Clip { + None, + EndOfLine, +} + pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } @@ -50,7 +56,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, - pub clip_at_line_ends: bool, + pub default_clip: Clip, } impl Entity for DisplayMap { @@ -85,7 +91,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), - clip_at_line_ends: false, + default_clip: Clip::None, } } @@ -109,7 +115,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), - clip_at_line_ends: self.clip_at_line_ends, + default_clip: self.default_clip, } } @@ -296,7 +302,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, - clip_at_line_ends: bool, + default_clip: Clip, } impl DisplaySnapshot { @@ -577,21 +583,33 @@ impl DisplaySnapshot { column } - pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { - let mut clipped = self.block_snapshot.clip_point(point.0, bias); - if self.clip_at_line_ends { - clipped = self.clip_at_line_end(DisplayPoint(clipped)).0 - } - DisplayPoint(clipped) + pub fn move_left(&self, point: DisplayPoint, clip: Clip) -> DisplayPoint { + self.clip_point_with( + DisplayPoint::new(point.row(), point.column().saturating_sub(1)), + Bias::Left, + clip, + ) } - pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint { - let mut point = point.0; - if point.column == self.line_len(point.row) { - point.column = point.column.saturating_sub(1); - point = self.block_snapshot.clip_point(point, Bias::Left); + pub fn move_right(&self, point: DisplayPoint, clip: Clip) -> DisplayPoint { + self.clip_point_with( + DisplayPoint::new(point.row(), point.column() + 1), + Bias::Right, + clip, + ) + } + + pub fn clip_point_with(&self, point: DisplayPoint, bias: Bias, clip: Clip) -> DisplayPoint { + let new_point = DisplayPoint(self.block_snapshot.clip_point(point.0, bias)); + if clip == Clip::EndOfLine && new_point.column() == self.line_len(new_point.row()) { + self.move_left(new_point, Clip::None) + } else { + new_point } - DisplayPoint(point) + } + + pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { + self.clip_point_with(point, bias, self.default_clip) } pub fn folds_in_range(&self, range: Range) -> impl Iterator> @@ -1580,7 +1598,7 @@ pub mod tests { fn assert(text: &str, cx: &mut gpui::AppContext) { let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); - unmarked_snapshot.clip_at_line_ends = true; + unmarked_snapshot.default_clip = Clip::EndOfLine; assert_eq!( unmarked_snapshot.clip_point(markers[1], Bias::Left), markers[0] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b4145edb64..4b36691b60 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1544,10 +1544,10 @@ impl Editor { range.clone() } - pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { - if self.display_map.read(cx).clip_at_line_ends != clip { + pub fn set_default_clip(&mut self, clip: Clip, cx: &mut ViewContext) { + if self.display_map.read(cx).default_clip != clip { self.display_map - .update(cx, |map, _| map.clip_at_line_ends = clip); + .update(cx, |map, _| map.default_clip = clip); } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b9bf74ee85..6440b56728 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -60,6 +60,7 @@ enum FoldMarkers {} struct SelectionLayout { head: DisplayPoint, + reversed: bool, cursor_shape: CursorShape, is_newest: bool, range: Range, @@ -78,6 +79,7 @@ impl SelectionLayout { let point_range = map.expand_to_line(selection.range()); Self { head: selection.head().to_display_point(map), + reversed: selection.reversed, cursor_shape, is_newest, range: point_range.start.to_display_point(map) @@ -87,6 +89,7 @@ impl SelectionLayout { let selection = selection.map(|p| p.to_display_point(map)); Self { head: selection.head(), + reversed: selection.reversed, cursor_shape, is_newest, range: selection.range(), @@ -844,6 +847,7 @@ impl EditorElement { if editor.show_local_cursors(cx) || replica_id != local_replica_id { let cursor_position = selection.head; + if layout .visible_display_row_range .contains(&cursor_position.row()) @@ -851,7 +855,15 @@ impl EditorElement { let cursor_row_layout = &layout.position_map.line_layouts [(cursor_position.row() - start_row) as usize] .line; - let cursor_column = cursor_position.column() as usize; + let mut cursor_column = cursor_position.column() as usize; + + if CursorShape::Block == selection.cursor_shape + && !selection.range.is_empty() + && !selection.reversed + && cursor_column > 0 + { + cursor_column -= 1; + } let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); let mut block_width = @@ -863,7 +875,10 @@ impl EditorElement { layout .position_map .snapshot - .chars_at(cursor_position) + .chars_at(DisplayPoint::new( + cursor_position.row(), + cursor_column as u32, + )) .next() .and_then(|(character, _)| { let font_id = diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index b8bd256d8a..924fefc73f 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use editor::{ char_kind, - display_map::{DisplaySnapshot, ToDisplayPoint}, + display_map::{Clip, DisplaySnapshot, ToDisplayPoint}, movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -295,7 +295,11 @@ impl Motion { SelectionGoal::None, ), EndOfParagraph => ( - map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), + map.clip_point_with( + movement::end_of_paragraph(map, point, times), + Bias::Left, + Clip::EndOfLine, + ), SelectionGoal::None, ), CurrentLine => (end_of_line(map, point), SelectionGoal::None), @@ -383,8 +387,7 @@ impl Motion { fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { for _ in 0..times { - *point.column_mut() = point.column().saturating_sub(1); - point = map.clip_point(point, Bias::Left); + point = map.move_left(point, Clip::None); if point.column() == 0 { break; } @@ -425,9 +428,7 @@ fn up( pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { for _ in 0..times { - let mut new_point = point; - *new_point.column_mut() += 1; - let new_point = map.clip_point(new_point, Bias::Right); + let new_point = map.clip_point(map.move_right(point, Clip::None), Bias::Right); if point == new_point { break; } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 79c990ffeb..a25558fa70 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -16,8 +16,9 @@ use crate::{ }; use collections::{HashMap, HashSet}; use editor::{ - display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection, - DisplayPoint, + display_map::{Clip, ToDisplayPoint}, + scroll::autoscroll::Autoscroll, + Anchor, Bias, ClipboardSelection, DisplayPoint, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::{AutoindentMode, Point, SelectionGoal}; @@ -254,7 +255,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); if let Some(item) = cx.read_from_clipboard() { let mut clipboard_text = Cow::Borrowed(item.text()); if let Some(mut clipboard_selections) = @@ -382,7 +383,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { editor.insert(&clipboard_text, cx); } } - editor.set_clip_at_line_ends(true, cx); + editor.set_default_clip(Clip::EndOfLine, cx); }); }); }); @@ -392,7 +393,7 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); let (map, display_selections) = editor.selections.all_display(cx); // Selections are biased right at the start. So we need to store // anchors that are biased left so that we can restore the selections @@ -425,7 +426,7 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { editor.buffer().update(cx, |buffer, cx| { buffer.edit(edits, None, cx); }); - editor.set_clip_at_line_ends(true, cx); + editor.set_default_clip(Clip::EndOfLine, cx); editor.change_selections(None, cx, |s| { s.select_anchor_ranges(stable_anchors); }); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index d226c70410..6ce3a3ac6c 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,7 +1,10 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; use editor::{ - char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind, - DisplayPoint, + char_kind, + display_map::{Clip, DisplaySnapshot}, + movement, + scroll::autoscroll::Autoscroll, + CharKind, DisplayPoint, }; use gpui::WindowContext; use language::Selection; @@ -15,7 +18,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m vim.update_active_editor(cx, |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); + editor.set_default_clip(Clip::None, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion @@ -42,7 +45,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo let mut objects_found = false; vim.update_active_editor(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); + editor.set_default_clip(Clip::None, cx); editor.transact(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index 56fef78e1d..ed8fcabdc9 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,12 +1,16 @@ 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::{Clip, ToDisplayPoint}, + scroll::autoscroll::Autoscroll, + Bias, +}; use gpui::WindowContext; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); let mut original_columns: HashMap<_, _> = Default::default(); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { @@ -19,7 +23,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m editor.insert("", cx); // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); + editor.set_default_clip(Clip::EndOfLine, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head(); @@ -39,7 +43,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); // Emulates behavior in vim where if we expanded backwards to include a newline // the cursor gets set back to the start of the line let mut should_move_to_start: HashSet<_> = Default::default(); @@ -77,7 +81,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo editor.insert("", cx); // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); + editor.set_default_clip(Clip::EndOfLine, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head(); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index ef72baae31..2f022dca98 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,8 +4,8 @@ use language::Point; use crate::{motion::Motion, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { + vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -21,9 +21,7 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { }) } }); - editor.set_clip_at_line_ends(true, cx); }); - vim.switch_mode(Mode::Insert, true, cx) } #[cfg(test)] diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 7212a865bd..beb468ecb0 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,11 +1,12 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::HashMap; +use editor::display_map::Clip; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -28,7 +29,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 5388dac1a2..837b619fc5 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -481,6 +481,12 @@ mod test { async fn test_visual_word_object(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state("The quick ˇbrown\nfox").await; + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state("The quick «bˇ»rown\nfox").await; + cx.simulate_shared_keystrokes(["i", "w"]).await; + cx.assert_shared_state("The quick «brownˇ»\nfox").await; + cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS) .await; cx.assert_binding_matches_all_exempted( diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index eb52945ced..af78741333 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,3 +1,4 @@ +use editor::display_map::Clip; use gpui::keymap_matcher::KeymapContext; use language::CursorShape; use serde::{Deserialize, Serialize}; @@ -12,6 +13,15 @@ pub enum Mode { Visual { line: bool }, } +impl Mode { + pub fn is_visual(&self) -> bool { + match self { + Mode::Normal | Mode::Insert => false, + Mode::Visual { .. } => true, + } + } +} + impl Default for Mode { fn default() -> Self { Self::Normal @@ -78,12 +88,11 @@ impl VimState { ) } - pub fn clip_at_line_end(&self) -> bool { - !matches!(self.mode, Mode::Insert | Mode::Visual { .. }) - } - - pub fn empty_selections_only(&self) -> bool { - !matches!(self.mode, Mode::Visual { .. }) + pub fn default_clip(&self) -> Clip { + match self.mode { + Mode::Insert | Mode::Visual { .. } => Clip::None, + Mode::Normal => Clip::EndOfLine, + } } pub fn keymap_context_layer(&self) -> KeymapContext { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 98d8cb8749..cd52e2afc8 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -141,7 +141,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { // works in visuial mode cx.simulate_keystrokes(["shift-v", "down", ">"]); - cx.assert_editor_state("aa\n b«b\n cˇ»c"); + cx.assert_editor_state("aa\n b«b\n ccˇ»"); } #[gpui::test] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 7f9a84b666..023ed880d2 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -61,6 +61,9 @@ pub struct NeovimBackedTestContext<'a> { // bindings are exempted. If None, all bindings are ignored for that insertion text. exemptions: HashMap>>, neovim: NeovimConnection, + + last_set_state: Option, + recent_keystrokes: Vec, } impl<'a> NeovimBackedTestContext<'a> { @@ -71,6 +74,9 @@ impl<'a> NeovimBackedTestContext<'a> { cx, exemptions: Default::default(), neovim: NeovimConnection::new(function_name).await, + + last_set_state: None, + recent_keystrokes: Default::default(), } } @@ -102,13 +108,21 @@ impl<'a> NeovimBackedTestContext<'a> { keystroke_texts: [&str; COUNT], ) -> ContextHandle { for keystroke_text in keystroke_texts.into_iter() { + self.recent_keystrokes.push(keystroke_text.to_string()); self.neovim.send_keystroke(keystroke_text).await; } self.simulate_keystrokes(keystroke_texts) } pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle { - let context_handle = self.set_state(marked_text, Mode::Normal); + let mode = if marked_text.contains("»") { + Mode::Visual { line: false } + } else { + Mode::Normal + }; + let context_handle = self.set_state(marked_text, mode); + self.last_set_state = Some(marked_text.to_string()); + self.recent_keystrokes = Vec::new(); self.neovim.set_state(marked_text).await; context_handle } @@ -116,15 +130,25 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn assert_shared_state(&mut self, marked_text: &str) { let neovim = self.neovim_state().await; if neovim != marked_text { + let initial_state = self + .last_set_state + .as_ref() + .unwrap_or(&"N/A".to_string()) + .clone(); panic!( indoc! {"Test is incorrect (currently expected != neovim state) - + # initial state: + {} + # keystrokes: + {} # currently expected: {} # neovim state: {} # zed state: {}"}, + initial_state, + self.recent_keystrokes.join(" "), marked_text, neovim, self.editor_state(), @@ -141,28 +165,40 @@ impl<'a> NeovimBackedTestContext<'a> { ) } + pub async fn neovim_mode(&mut self) -> Mode { + self.neovim.mode().await.unwrap() + } + async fn neovim_selection(&mut self) -> Range { - let mut neovim_selection = self.neovim.selection().await; - // Zed selections adjust themselves to make the end point visually make sense - if neovim_selection.start > neovim_selection.end { - neovim_selection.start.column += 1; - } + let neovim_selection = self.neovim.selection().await; neovim_selection.to_offset(&self.buffer_snapshot()) } pub async fn assert_state_matches(&mut self) { - assert_eq!( - self.neovim.text().await, - self.buffer_text(), - "{}", - self.assertion_context() - ); + let neovim = self.neovim_state().await; + let editor = self.editor_state(); + let initial_state = self + .last_set_state + .as_ref() + .unwrap_or(&"N/A".to_string()) + .clone(); - let selections = vec![self.neovim_selection().await]; - self.assert_editor_selections(selections); - - if let Some(neovim_mode) = self.neovim.mode().await { - assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),); + if neovim != editor { + panic!( + indoc! {"Test failed (zed does not match nvim behaviour) + # initial state: + {} + # keystrokes: + {} + # neovim state: + {} + # zed state: + {}"}, + initial_state, + self.recent_keystrokes.join(" "), + neovim, + editor, + ) } } @@ -207,6 +243,29 @@ impl<'a> NeovimBackedTestContext<'a> { } } + pub fn each_marked_position(&self, marked_positions: &str) -> Vec { + let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions); + let mut ret = Vec::with_capacity(cursor_offsets.len()); + + for cursor_offset in cursor_offsets.iter() { + let mut marked_text = unmarked_text.clone(); + marked_text.insert(*cursor_offset, 'ˇ'); + ret.push(marked_text) + } + + ret + } + + pub async fn assert_neovim_compatible( + &mut self, + marked_positions: &str, + keystrokes: [&str; COUNT], + ) { + self.set_shared_state(&marked_positions).await; + self.simulate_shared_keystrokes(keystrokes).await; + self.assert_state_matches().await; + } + pub async fn assert_binding_matches_all_exempted( &mut self, keystrokes: [&str; COUNT], diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 5bfae4e673..21d69d2152 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -213,6 +213,16 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + async fn read_position(&mut self, cmd: &str) -> u32 { + self.nvim + .command_output(cmd) + .await + .unwrap() + .parse::() + .unwrap() + } + #[cfg(feature = "neovim")] pub async fn state(&mut self) -> (Option, String, Range) { let nvim_buffer = self @@ -226,22 +236,11 @@ impl NeovimConnection { .expect("Could not get buffer text") .join("\n"); - let cursor_row: u32 = self - .nvim - .command_output("echo line('.')") - .await - .unwrap() - .parse::() - .unwrap() - - 1; // Neovim rows start at 1 - let cursor_col: u32 = self - .nvim - .command_output("echo col('.')") - .await - .unwrap() - .parse::() - .unwrap() - - 1; // Neovim columns start at 1 + // nvim columns are 1-based, so -1. + let cursor_row = self.read_position("echo line('.')").await - 1; + let mut cursor_col = self.read_position("echo col('.')").await - 1; + let selection_row = self.read_position("echo line('v')").await - 1; + let mut selection_col = self.read_position("echo col('v')").await - 1; let nvim_mode_text = self .nvim @@ -266,46 +265,32 @@ impl NeovimConnection { _ => None, }; - let (start, end) = if let Some(Mode::Visual { .. }) = mode { - self.nvim - .input("") - .await - .expect("Could not exit visual mode"); - let nvim_buffer = self - .nvim - .get_current_buf() - .await - .expect("Could not get neovim buffer"); - let (start_row, start_col) = nvim_buffer - .get_mark("<") - .await - .expect("Could not get selection start"); - let (end_row, end_col) = nvim_buffer - .get_mark(">") - .await - .expect("Could not get selection end"); - self.nvim - .input("gv") - .await - .expect("Could not reselect visual selection"); - - if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 { - ( - Point::new(end_row as u32 - 1, end_col as u32), - Point::new(start_row as u32 - 1, start_col as u32), - ) - } else { - ( - Point::new(start_row as u32 - 1, start_col as u32), - Point::new(end_row as u32 - 1, end_col as u32), - ) + // Vim uses the index of the first and last character in the selection + // Zed uses the index of the positions between the characters, so we need + // to add one to the end in visual mode. + match mode { + Some(Mode::Visual { .. }) => { + if selection_col > cursor_col { + let selection_line_length = + self.read_position("echo strlen(getline(line('v')))").await; + if selection_line_length > 0 { + selection_col += 1; + } + } else { + let cursor_line_length = + self.read_position("echo strlen(getline(line('.')))").await; + if cursor_line_length > 0 { + cursor_col += 1; + } + } } - } else { - ( - Point::new(cursor_row, cursor_col), - Point::new(cursor_row, cursor_col), - ) - }; + Some(Mode::Insert) | Some(Mode::Normal) | None => {} + } + + let (start, end) = ( + Point::new(selection_row, selection_col), + Point::new(cursor_row, cursor_col), + ); let state = NeovimData::Get { mode, diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ea09e55091..ca1020cf75 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -86,12 +86,13 @@ impl<'a> VimTestContext<'a> { pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { let window_id = self.window_id; + let context_handle = self.cx.set_state(text); self.update_window(window_id, |cx| { Vim::update(cx, |vim, cx| { - vim.switch_mode(mode, false, cx); + vim.switch_mode(mode, true, cx); }) }); - self.cx.set_state(text) + context_handle } #[track_caller] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 22bd196c67..c9386eb0bc 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -13,7 +13,7 @@ mod visual; use anyhow::Result; use collections::CommandPaletteFilter; -use editor::{Bias, Editor, EditorMode, Event}; +use editor::{display_map::Clip, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -181,6 +181,7 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { + let last_mode = self.state.mode; self.state.mode = mode; self.state.operator_stack.clear(); @@ -197,12 +198,16 @@ impl Vim { self.update_active_editor(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { - if self.state.empty_selections_only() { - let new_head = map.clip_point(selection.head(), Bias::Left); - selection.collapse_to(new_head, selection.goal) - } else { - selection - .set_head(map.clip_point(selection.head(), Bias::Left), selection.goal); + if last_mode.is_visual() && !mode.is_visual() { + let mut point = selection.head(); + if !selection.reversed { + point = map.move_left(selection.head(), Clip::None); + } + selection.collapse_to(point, selection.goal) + } else if !last_mode.is_visual() && mode.is_visual() { + if selection.is_empty() { + selection.end = map.move_right(selection.start, Clip::None); + } } }); }) @@ -265,7 +270,7 @@ impl Vim { } Some(Operator::Replace) => match Vim::read(cx).state.mode { Mode::Normal => normal_replace(text, cx), - Mode::Visual { line } => visual_replace(text, line, cx), + Mode::Visual { .. } => visual_replace(text, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, _ => {} @@ -309,7 +314,7 @@ impl Vim { self.update_active_editor(cx, |editor, cx| { if self.enabled && editor.mode() == EditorMode::Full { editor.set_cursor_shape(cursor_shape, cx); - editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); + editor.set_default_clip(state.default_clip(), cx); editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); @@ -326,7 +331,7 @@ impl Vim { fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext) { editor.set_cursor_shape(CursorShape::Bar, cx); - editor.set_clip_at_line_ends(false, cx); + editor.set_default_clip(Clip::None, cx); editor.set_input_enabled(true); editor.selections.line_mode = false; diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d87e4ff974..22d96e2d1c 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -2,7 +2,10 @@ use std::{borrow::Cow, sync::Arc}; use collections::HashMap; use editor::{ - display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection, + display_map::{Clip, ToDisplayPoint}, + movement, + scroll::autoscroll::Autoscroll, + Bias, ClipboardSelection, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::{AutoindentMode, SelectionGoal}; @@ -16,9 +19,21 @@ use crate::{ Vim, }; -actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]); +actions!( + vim, + [ + ToggleVisual, + ToggleVisualLine, + VisualDelete, + VisualChange, + VisualYank, + VisualPaste + ] +); pub fn init(cx: &mut AppContext) { + cx.add_action(toggle_visual); + cx.add_action(toggle_visual_line); cx.add_action(change); cx.add_action(delete); cx.add_action(yank); @@ -32,23 +47,32 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex s.move_with(|map, selection| { let was_reversed = selection.reversed; - if let Some((new_head, goal)) = - motion.move_point(map, selection.head(), selection.goal, times) - { - selection.set_head(new_head, goal); + let mut current_head = selection.head(); - if was_reversed && !selection.reversed { - // Head was at the start of the selection, and now is at the end. We need to move the start - // back by one if possible in order to compensate for this change. - *selection.start.column_mut() = - selection.start.column().saturating_sub(1); - selection.start = map.clip_point(selection.start, Bias::Left); - } else if !was_reversed && selection.reversed { - // Head was at the end of the selection, and now is at the start. We need to move the end - // forward by one if possible in order to compensate for this change. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); - } + // our motions assume the current character is after the cursor, + // but in (forward) visual mode the current character is just + // before the end of the selection. + if !selection.reversed { + current_head = map.move_left(current_head, Clip::None); + } + + let Some((new_head, goal)) = + motion.move_point(map, current_head, selection.goal, times) else { return }; + + selection.set_head(new_head, goal); + + // ensure the current character is included in the selection. + if !selection.reversed { + selection.end = map.move_right(selection.end, Clip::None); + } + + // vim always ensures the anchor character stays selected. + // if our selection has reversed, we need to move the opposite end + // to ensure the anchor is still selected. + if was_reversed && !selection.reversed { + selection.start = map.move_left(selection.start, Clip::None); + } else if !was_reversed && selection.reversed { + selection.end = map.move_right(selection.end, Clip::None); } }); }); @@ -64,14 +88,30 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { - let head = selection.head(); - if let Some(mut range) = object.range(map, head, around) { - if !range.is_empty() { - if let Some((_, end)) = map.reverse_chars_at(range.end).next() { - range.end = end; - } + let mut head = selection.head(); - if selection.is_empty() { + // all our motions assume that the current character is + // after the cursor; however in the case of a visual selection + // the current character is before the cursor. + if !selection.reversed { + head = map.move_left(head, Clip::None); + } + + if let Some(range) = object.range(map, head, around) { + if !range.is_empty() { + let expand_both_ways = if selection.is_empty() { + true + // contains only one character + } else if let Some((_, start)) = + map.reverse_chars_at(selection.end).next() + { + selection.start == start + } else { + false + }; + dbg!(expand_both_ways); + + if expand_both_ways { selection.start = range.start; selection.end = range.end; } else if selection.reversed { @@ -88,10 +128,35 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { }); } +pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| match vim.state.mode { + Mode::Normal | Mode::Insert | Mode::Visual { line: true } => { + vim.switch_mode(Mode::Visual { line: false }, false, cx); + } + Mode::Visual { line: false } => { + vim.switch_mode(Mode::Normal, false, cx); + } + }) +} + +pub fn toggle_visual_line( + _: &mut Workspace, + _: &ToggleVisualLine, + cx: &mut ViewContext, +) { + Vim::update(cx, |vim, cx| match vim.state.mode { + Mode::Normal | Mode::Insert | Mode::Visual { line: false } => { + vim.switch_mode(Mode::Visual { line: true }, false, cx); + } + Mode::Visual { line: true } => { + vim.switch_mode(Mode::Normal, false, cx); + } + }) +} + pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); // Compute edits and resulting anchor selections. If in line mode, adjust // the anchor location and additional newline let mut edits = Vec::new(); @@ -99,13 +164,6 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = editor.selections.line_mode; editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { if line_mode { - original_columns - .insert(selection.id, selection.head().to_point(map).column); - } else if !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); + let mut position = selection.head(); + if !selection.reversed { + position = map.move_left(position, Clip::None); + } + original_columns.insert(selection.id, position.to_point(map).column); } selection.goal = SelectionGoal::None; }); @@ -162,7 +217,6 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); let line_mode = editor.selections.line_mode; - if !line_mode { - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); - } - }); - }); - } copy_selections_content(editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { @@ -203,7 +248,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) }); }); }); - vim.switch_mode(Mode::Normal, false, cx); + vim.switch_mode(Mode::Normal, true, cx); }); } @@ -256,11 +301,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext let mut selection = selection.clone(); if !selection.reversed { - let mut adjusted = selection.end; - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *adjusted.column_mut() = adjusted.column() + 1; - adjusted = display_map.clip_point(adjusted, Bias::Right); + let adjusted = selection.end; // If the selection is empty, move both the start and end forward one // character if selection.is_empty() { @@ -311,11 +352,11 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } }); }); - vim.switch_mode(Mode::Normal, false, cx); + vim.switch_mode(Mode::Normal, true, cx); }); } -pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut WindowContext) { +pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -336,14 +377,7 @@ pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut WindowContext) let mut edits = Vec::new(); for selection in selections.iter() { - let mut selection = selection.clone(); - if !line && !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = display_map.clip_point(selection.end, Bias::Right); - } - + let selection = selection.clone(); for row_range in movement::split_display_range_by_lines(&display_map, selection.range()) { @@ -375,19 +409,42 @@ mod test { #[gpui::test] async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["v", "w", "j"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog" + }) + .await; + + // entering visual mode should select the character + // under cursor + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! { "The «qˇ»uick brown + fox jumps over + the lazy dog"}) .await; - let mut cx = cx.binding(["v", "b", "k"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + + // forwards motions should extend the selection + cx.simulate_shared_keystrokes(["w", "j"]).await; + cx.assert_shared_state(indoc! { "The «quick brown + fox jumps oˇ»ver + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["escape"]).await; + assert_eq!(Mode::Normal, cx.neovim_mode().await); + cx.assert_shared_state(indoc! { "The quick brown + fox jumps ˇover + the lazy dog"}) + .await; + + // motions work backwards + cx.simulate_shared_keystrokes(["v", "k", "b"]).await; + cx.assert_shared_state(indoc! { "The «ˇquick brown + fox jumps o»ver + the lazy dog"}) .await; } @@ -461,22 +518,33 @@ mod test { #[gpui::test] async fn test_visual_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["v", "w", "c"]); - cx.assert("The quick ˇbrown").await; - let mut cx = cx.binding(["v", "w", "j", "c"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("The quick ˇbrown").await; + cx.simulate_shared_keystrokes(["v", "w", "c"]).await; + cx.assert_shared_state("The quick ˇ").await; + + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) .await; - let mut cx = cx.binding(["v", "b", "k", "c"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await; + cx.assert_shared_state(indoc! {" + The ˇver + the lazy dog"}) .await; + + let cases = cx.each_marked_position(indoc! {" + The ˇquick brown + fox jumps ˇover + the ˇlazy dog"}); + for initial_state in cases { + cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"]) + .await; + cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"]) + .await; + } } #[gpui::test] @@ -605,7 +673,7 @@ mod test { cx.set_state( indoc! {" The quick brown - fox «jumpˇ»s over + fox «jumpsˇ» over the lazy dog"}, Mode::Visual { line: false }, ); @@ -629,7 +697,7 @@ mod test { cx.set_state( indoc! {" The quick brown - fox juˇmps over + fox ju«mˇ»ps over the lazy dog"}, Mode::Visual { line: true }, ); @@ -643,7 +711,7 @@ mod test { cx.set_state( indoc! {" The quick brown - the «lazˇ»y dog"}, + the «lazyˇ» dog"}, Mode::Visual { line: false }, ); cx.simulate_keystroke("p"); diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json index bd4e91977f..6b5274b892 100644 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ b/crates/vim/test_data/test_enter_visual_mode.json @@ -1,30 +1,12 @@ {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} {"Key":"v"} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}} {"Key":"w"} {"Key":"j"} -{"Get":{"state":"The «quick brown\nfox jumps ˇ»over\nthe lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} +{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Key":"escape"} +{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} {"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jumps «over\nˇ»the lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe «lazy ˇ»dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} {"Key":"k"} -{"Get":{"state":"«ˇThe »quick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} {"Key":"b"} -{"Key":"k"} -{"Get":{"state":"The «ˇquick brown\nfox jumps »over\nthe lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Get":{"state":"The quick brown\n«ˇfox jumps over\nthe »lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} diff --git a/crates/vim/test_data/test_visual_change.json b/crates/vim/test_data/test_visual_change.json index 8c252e49c5..7d1efe05ce 100644 --- a/crates/vim/test_data/test_visual_change.json +++ b/crates/vim/test_data/test_visual_change.json @@ -9,33 +9,39 @@ {"Key":"j"} {"Key":"c"} {"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"c"} +{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"k"} +{"Key":"c"} +{"Get":{"state":"The ˇrown\nfox jumps over\nthe lazy dog","mode":"Insert"}} {"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} {"Key":"v"} {"Key":"w"} {"Key":"j"} {"Key":"c"} {"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"k"} +{"Key":"c"} +{"Get":{"state":"The quick brown\nˇver\nthe lazy dog","mode":"Insert"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} {"Key":"v"} {"Key":"w"} {"Key":"j"} {"Key":"c"} {"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} {"Key":"v"} -{"Key":"b"} +{"Key":"w"} {"Key":"k"} {"Key":"c"} -{"Get":{"state":"The quick brown\nˇazy dog","mode":"Insert"}} +{"Get":{"state":"The quick brown\nfox jumpsˇazy dog","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index 5514f7385a..b1765459e0 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -1,38 +1,44 @@ +{"Put":{"state":"The quick ˇbrown\nfox"}} +{"Key":"v"} +{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown« ˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ» \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} @@ -52,62 +58,62 @@ {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Thˇ»e-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «browˇ»n \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ» \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ» \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ» fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpˇ»s over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} @@ -117,37 +123,37 @@ {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown« ˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ» \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} @@ -167,62 +173,62 @@ {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «browˇ»n \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ» \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ» \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ» fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpˇ»s over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} From 5edcb747609222f602138798d1a80e50f1160dae Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 27 Jul 2023 21:39:37 -0600 Subject: [PATCH 022/326] Add support for visual ranges ending with a newline These primarily happen when first entering visual mode, but can also be created with objects like `vi{`. Along the way fix the way ranges like `vi{` are selected to be more similar to nvim. --- assets/keymaps/vim.json | 4 +- crates/editor/src/display_map.rs | 59 ++++------ crates/editor/src/editor.rs | 6 +- crates/editor/src/element.rs | 34 +++--- crates/editor/src/movement.rs | 12 ++ crates/editor/src/selections_collection.rs | 1 + crates/vim/src/mode_indicator.rs | 2 +- crates/vim/src/motion.rs | 12 +- crates/vim/src/normal.rs | 13 +-- crates/vim/src/normal/change.rs | 11 +- crates/vim/src/normal/delete.rs | 14 +-- crates/vim/src/normal/yank.rs | 5 +- crates/vim/src/object.rs | 79 ++++++++++++- crates/vim/src/state.rs | 7 +- crates/vim/src/test/neovim_connection.rs | 15 ++- crates/vim/src/vim.rs | 10 +- crates/vim/src/visual.rs | 108 +++++++++++++++--- .../test_enter_visual_line_mode.json | 10 ++ .../vim/test_data/test_enter_visual_mode.json | 8 ++ ...ltiline_surrounding_character_objects.json | 10 ++ .../test_data/test_visual_word_object.json | 16 +-- 21 files changed, 301 insertions(+), 135 deletions(-) create mode 100644 crates/vim/test_data/test_enter_visual_line_mode.json create mode 100644 crates/vim/test_data/test_multiline_surrounding_character_objects.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index a1b6d939f4..ce5ce5aabd 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -101,6 +101,8 @@ "vim::SwitchMode", "Normal" ], + "v": "vim::ToggleVisual", + "shift-v": "vim::ToggleVisualLine", "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion @@ -266,8 +268,6 @@ "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", - "v": "vim::ToggleVisual", - "shift-v": "vim::ToggleVisualLine", "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index be0dd68adf..bf787eb0f7 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -35,12 +35,6 @@ pub enum FoldStatus { Foldable, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Clip { - None, - EndOfLine, -} - pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } @@ -56,7 +50,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, - pub default_clip: Clip, + pub clip_at_line_ends: bool, } impl Entity for DisplayMap { @@ -91,7 +85,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), - default_clip: Clip::None, + clip_at_line_ends: false, } } @@ -115,7 +109,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), - default_clip: self.default_clip, + clip_at_line_ends: self.clip_at_line_ends, } } @@ -302,7 +296,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, - default_clip: Clip, + clip_at_line_ends: bool, } impl DisplaySnapshot { @@ -361,6 +355,9 @@ impl DisplaySnapshot { pub fn expand_to_line(&self, range: Range) -> Range { let mut new_start = self.prev_line_boundary(range.start).0; + if range.end.column == 0 { + return new_start..range.end; + } let mut new_end = self.next_line_boundary(range.end).0; if new_start.row == range.start.row && new_end.row == range.end.row { @@ -583,33 +580,21 @@ impl DisplaySnapshot { column } - pub fn move_left(&self, point: DisplayPoint, clip: Clip) -> DisplayPoint { - self.clip_point_with( - DisplayPoint::new(point.row(), point.column().saturating_sub(1)), - Bias::Left, - clip, - ) - } - - pub fn move_right(&self, point: DisplayPoint, clip: Clip) -> DisplayPoint { - self.clip_point_with( - DisplayPoint::new(point.row(), point.column() + 1), - Bias::Right, - clip, - ) - } - - pub fn clip_point_with(&self, point: DisplayPoint, bias: Bias, clip: Clip) -> DisplayPoint { - let new_point = DisplayPoint(self.block_snapshot.clip_point(point.0, bias)); - if clip == Clip::EndOfLine && new_point.column() == self.line_len(new_point.row()) { - self.move_left(new_point, Clip::None) - } else { - new_point - } - } - pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { - self.clip_point_with(point, bias, self.default_clip) + let mut clipped = self.block_snapshot.clip_point(point.0, bias); + if self.clip_at_line_ends { + clipped = self.clip_at_line_end(DisplayPoint(clipped)).0 + } + DisplayPoint(clipped) + } + + pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint { + let mut point = point.0; + if point.column == self.line_len(point.row) { + point.column = point.column.saturating_sub(1); + point = self.block_snapshot.clip_point(point, Bias::Left); + } + DisplayPoint(point) } pub fn folds_in_range(&self, range: Range) -> impl Iterator> @@ -1598,7 +1583,7 @@ pub mod tests { fn assert(text: &str, cx: &mut gpui::AppContext) { let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); - unmarked_snapshot.default_clip = Clip::EndOfLine; + unmarked_snapshot.clip_at_line_ends = true; assert_eq!( unmarked_snapshot.clip_point(markers[1], Bias::Left), markers[0] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4b36691b60..b4145edb64 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1544,10 +1544,10 @@ impl Editor { range.clone() } - pub fn set_default_clip(&mut self, clip: Clip, cx: &mut ViewContext) { - if self.display_map.read(cx).default_clip != clip { + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + if self.display_map.read(cx).clip_at_line_ends != clip { self.display_map - .update(cx, |map, _| map.default_clip = clip); + .update(cx, |map, _| map.clip_at_line_ends = clip); } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6440b56728..cce302ed3f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -847,23 +847,26 @@ impl EditorElement { if editor.show_local_cursors(cx) || replica_id != local_replica_id { let cursor_position = selection.head; + let mut cursor_column = cursor_position.column() as usize; + let mut cursor_row = cursor_position.row(); - if layout - .visible_display_row_range - .contains(&cursor_position.row()) + if CursorShape::Block == selection.cursor_shape + && !selection.range.is_empty() + && !selection.reversed { - let cursor_row_layout = &layout.position_map.line_layouts - [(cursor_position.row() - start_row) as usize] - .line; - let mut cursor_column = cursor_position.column() as usize; - - if CursorShape::Block == selection.cursor_shape - && !selection.range.is_empty() - && !selection.reversed - && cursor_column > 0 - { + if cursor_column > 0 { cursor_column -= 1; + } else if cursor_row > 0 { + cursor_row -= 1; + cursor_column = + layout.position_map.snapshot.line_len(cursor_row) as usize; } + } + + if layout.visible_display_row_range.contains(&cursor_row) { + let cursor_row_layout = &layout.position_map.line_layouts + [(cursor_row - start_row) as usize] + .line; let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); let mut block_width = @@ -876,7 +879,7 @@ impl EditorElement { .position_map .snapshot .chars_at(DisplayPoint::new( - cursor_position.row(), + cursor_row as u32, cursor_column as u32, )) .next() @@ -903,8 +906,7 @@ impl EditorElement { }; let x = cursor_character_x - scroll_left; - let y = cursor_position.row() as f32 * layout.position_map.line_height - - scroll_top; + let y = cursor_row as f32 * layout.position_map.line_height - scroll_top; if selection.is_newest { editor.pixel_position_of_newest_cursor = Some(vec2f( bounds.origin_x() + x + block_width / 2., diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 1bd37da52f..f70436abeb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -13,6 +13,13 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { map.clip_point(point, Bias::Left) } +pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + if point.column() > 0 { + *point.column_mut() -= 1; + } + map.clip_point(point, Bias::Left) +} + pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let max_column = map.line_len(point.row()); if point.column() < max_column { @@ -24,6 +31,11 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { map.clip_point(point, Bias::Right) } +pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + *point.column_mut() += 1; + map.clip_point(point, Bias::Right) +} + pub fn up( map: &DisplaySnapshot, start: DisplayPoint, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 1921bc0738..5f88eaecd8 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -498,6 +498,7 @@ impl<'a> MutableSelectionsCollection<'a> { T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { let buffer = self.buffer.read(self.cx).snapshot(self.cx); + selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. let mut i = 1; diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 639a7594f1..4d68ec534e 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -87,7 +87,7 @@ impl View for ModeIndicator { Mode::Normal => "-- NORMAL --", Mode::Insert => "-- INSERT --", Mode::Visual { line: false } => "-- VISUAL --", - Mode::Visual { line: true } => "VISUAL LINE ", + Mode::Visual { line: true } => "VISUAL LINE", }; Label::new(text, theme.vim_mode_indicator.text.clone()) .contained() diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 924fefc73f..acf9d46ad3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use editor::{ char_kind, - display_map::{Clip, DisplaySnapshot, ToDisplayPoint}, + display_map::{DisplaySnapshot, ToDisplayPoint}, movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -295,11 +295,7 @@ impl Motion { SelectionGoal::None, ), EndOfParagraph => ( - map.clip_point_with( - movement::end_of_paragraph(map, point, times), - Bias::Left, - Clip::EndOfLine, - ), + map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), CurrentLine => (end_of_line(map, point), SelectionGoal::None), @@ -387,7 +383,7 @@ impl Motion { fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { for _ in 0..times { - point = map.move_left(point, Clip::None); + point = movement::saturating_left(map, point); if point.column() == 0 { break; } @@ -428,7 +424,7 @@ fn up( pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { for _ in 0..times { - let new_point = map.clip_point(map.move_right(point, Clip::None), Bias::Right); + let new_point = movement::saturating_right(map, point); if point == new_point { break; } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index a25558fa70..79c990ffeb 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -16,9 +16,8 @@ use crate::{ }; use collections::{HashMap, HashSet}; use editor::{ - display_map::{Clip, ToDisplayPoint}, - scroll::autoscroll::Autoscroll, - Anchor, Bias, ClipboardSelection, DisplayPoint, + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection, + DisplayPoint, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::{AutoindentMode, Point, SelectionGoal}; @@ -255,7 +254,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); if let Some(item) = cx.read_from_clipboard() { let mut clipboard_text = Cow::Borrowed(item.text()); if let Some(mut clipboard_selections) = @@ -383,7 +382,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext) { editor.insert(&clipboard_text, cx); } } - editor.set_default_clip(Clip::EndOfLine, cx); + editor.set_clip_at_line_ends(true, cx); }); }); }); @@ -393,7 +392,7 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); let (map, display_selections) = editor.selections.all_display(cx); // Selections are biased right at the start. So we need to store // anchors that are biased left so that we can restore the selections @@ -426,7 +425,7 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { editor.buffer().update(cx, |buffer, cx| { buffer.edit(edits, None, cx); }); - editor.set_default_clip(Clip::EndOfLine, cx); + editor.set_clip_at_line_ends(true, cx); editor.change_selections(None, cx, |s| { s.select_anchor_ranges(stable_anchors); }); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 6ce3a3ac6c..d226c70410 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,10 +1,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; use editor::{ - char_kind, - display_map::{Clip, DisplaySnapshot}, - movement, - scroll::autoscroll::Autoscroll, - CharKind, DisplayPoint, + char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind, + DisplayPoint, }; use gpui::WindowContext; use language::Selection; @@ -18,7 +15,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion @@ -45,7 +42,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo let mut objects_found = false; vim.update_active_editor(cx, |editor, cx| { // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index ed8fcabdc9..56fef78e1d 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,16 +1,12 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{ - display_map::{Clip, ToDisplayPoint}, - scroll::autoscroll::Autoscroll, - Bias, -}; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; use gpui::WindowContext; pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { @@ -23,7 +19,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m editor.insert("", cx); // Fixup cursor position after the deletion - editor.set_default_clip(Clip::EndOfLine, cx); + editor.set_clip_at_line_ends(true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head(); @@ -43,7 +39,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); // Emulates behavior in vim where if we expanded backwards to include a newline // the cursor gets set back to the start of the line let mut should_move_to_start: HashSet<_> = Default::default(); @@ -81,7 +77,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo editor.insert("", cx); // Fixup cursor position after the deletion - editor.set_default_clip(Clip::EndOfLine, cx); + editor.set_clip_at_line_ends(true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { let mut cursor = selection.head(); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index beb468ecb0..7212a865bd 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -1,12 +1,11 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::HashMap; -use editor::display_map::Clip; use gpui::WindowContext; pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -29,7 +28,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); let mut original_positions: HashMap<_, _> = Default::default(); editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 837b619fc5..85e6eab692 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -369,7 +369,7 @@ fn surrounding_markers( start = Some(point) } else { *point.column_mut() += char.len_utf8() as u32; - start = Some(point); + start = Some(point) } break; } @@ -420,11 +420,38 @@ fn surrounding_markers( } } - if let (Some(start), Some(end)) = (start, end) { - Some(start..end) - } else { - None + let (Some(mut start), Some(mut end)) = (start, end) else { + return None; + }; + + if !around { + // if a block starts with a newline, move the start to after the newline. + let mut was_newline = false; + for (char, point) in map.chars_at(start) { + if was_newline { + start = point; + } else if char == '\n' { + was_newline = true; + continue; + } + break; + } + // if a block ends with a newline, then whitespace, then the delimeter, + // move the end to after the newline. + let mut new_end = end; + for (char, point) in map.reverse_chars_at(end) { + if char == '\n' { + end = new_end; + break; + } + if !char.is_whitespace() { + break; + } + new_end = point + } } + + Some(start..end) } #[cfg(test)] @@ -681,6 +708,48 @@ mod test { } } + #[gpui::test] + async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + ˇreturn false + }" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "{"]).await; + cx.assert_shared_state(indoc! {" + func empty(a string) bool { + « if a == \"\" { + return true + } + return false + ˇ»}"}) + .await; + cx.set_shared_state(indoc! { + "func empty(a string) bool { + if a == \"\" { + ˇreturn true + } + return false + }" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "{"]).await; + cx.assert_shared_state(indoc! {" + func empty(a string) bool { + if a == \"\" { + « return true + ˇ» } + return false + }"}) + .await; + } + #[gpui::test] async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index af78741333..905bd5fd2a 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,4 +1,3 @@ -use editor::display_map::Clip; use gpui::keymap_matcher::KeymapContext; use language::CursorShape; use serde::{Deserialize, Serialize}; @@ -88,10 +87,10 @@ impl VimState { ) } - pub fn default_clip(&self) -> Clip { + pub fn clip_at_line_ends(&self) -> bool { match self.mode { - Mode::Insert | Mode::Visual { .. } => Clip::None, - Mode::Normal => Clip::EndOfLine, + Mode::Insert | Mode::Visual { .. } => false, + Mode::Normal => true, } } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 21d69d2152..dd9be10723 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -237,10 +237,11 @@ impl NeovimConnection { .join("\n"); // nvim columns are 1-based, so -1. - let cursor_row = self.read_position("echo line('.')").await - 1; + let mut cursor_row = self.read_position("echo line('.')").await - 1; let mut cursor_col = self.read_position("echo col('.')").await - 1; - let selection_row = self.read_position("echo line('v')").await - 1; + let mut selection_row = self.read_position("echo line('v')").await - 1; let mut selection_col = self.read_position("echo col('v')").await - 1; + let total_rows = self.read_position("echo line('$')").await - 1; let nvim_mode_text = self .nvim @@ -273,14 +274,20 @@ impl NeovimConnection { if selection_col > cursor_col { let selection_line_length = self.read_position("echo strlen(getline(line('v')))").await; - if selection_line_length > 0 { + if selection_line_length > selection_col { selection_col += 1; + } else if selection_row < total_rows { + selection_col = 0; + selection_row += 1; } } else { let cursor_line_length = self.read_position("echo strlen(getline(line('.')))").await; - if cursor_line_length > 0 { + if cursor_line_length > cursor_col { cursor_col += 1; + } else if cursor_row < total_rows { + cursor_col = 0; + cursor_row += 1; } } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c9386eb0bc..e8d69d696c 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -13,7 +13,7 @@ mod visual; use anyhow::Result; use collections::CommandPaletteFilter; -use editor::{display_map::Clip, Editor, EditorMode, Event}; +use editor::{movement, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -201,12 +201,12 @@ impl Vim { if last_mode.is_visual() && !mode.is_visual() { let mut point = selection.head(); if !selection.reversed { - point = map.move_left(selection.head(), Clip::None); + point = movement::left(map, selection.head()); } selection.collapse_to(point, selection.goal) } else if !last_mode.is_visual() && mode.is_visual() { if selection.is_empty() { - selection.end = map.move_right(selection.start, Clip::None); + selection.end = movement::right(map, selection.start); } } }); @@ -314,7 +314,7 @@ impl Vim { self.update_active_editor(cx, |editor, cx| { if self.enabled && editor.mode() == EditorMode::Full { editor.set_cursor_shape(cursor_shape, cx); - editor.set_default_clip(state.default_clip(), cx); + editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); @@ -331,7 +331,7 @@ impl Vim { fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext) { editor.set_cursor_shape(CursorShape::Bar, cx); - editor.set_default_clip(Clip::None, cx); + editor.set_clip_at_line_ends(false, cx); editor.set_input_enabled(true); editor.selections.line_mode = false; diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 22d96e2d1c..eae3c80f38 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -2,10 +2,7 @@ use std::{borrow::Cow, sync::Arc}; use collections::HashMap; use editor::{ - display_map::{Clip, ToDisplayPoint}, - movement, - scroll::autoscroll::Autoscroll, - Bias, ClipboardSelection, + display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::{AutoindentMode, SelectionGoal}; @@ -53,7 +50,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // but in (forward) visual mode the current character is just // before the end of the selection. if !selection.reversed { - current_head = map.move_left(current_head, Clip::None); + current_head = movement::left(map, selection.end) } let Some((new_head, goal)) = @@ -63,16 +60,16 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // ensure the current character is included in the selection. if !selection.reversed { - selection.end = map.move_right(selection.end, Clip::None); + selection.end = movement::right(map, selection.end) } // vim always ensures the anchor character stays selected. // if our selection has reversed, we need to move the opposite end // to ensure the anchor is still selected. if was_reversed && !selection.reversed { - selection.start = map.move_left(selection.start, Clip::None); + selection.start = movement::left(map, selection.start); } else if !was_reversed && selection.reversed { - selection.end = map.move_right(selection.end, Clip::None); + selection.end = movement::right(map, selection.end); } }); }); @@ -94,7 +91,7 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { // after the cursor; however in the case of a visual selection // the current character is before the cursor. if !selection.reversed { - head = map.move_left(head, Clip::None); + head = movement::left(map, head); } if let Some(range) = object.range(map, head, around) { @@ -109,7 +106,6 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { } else { false }; - dbg!(expand_both_ways); if expand_both_ways { selection.start = range.start; @@ -206,7 +202,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext, cx: &mut WindowContext) { #[cfg(test)] mod test { use indoc::indoc; + use workspace::item::Item; use crate::{ state::Mode, @@ -417,6 +410,7 @@ mod test { the lazy dog" }) .await; + let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); // entering visual mode should select the character // under cursor @@ -425,6 +419,7 @@ mod test { fox jumps over the lazy dog"}) .await; + cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); // forwards motions should extend the selection cx.simulate_shared_keystrokes(["w", "j"]).await; @@ -446,6 +441,87 @@ mod test { fox jumps o»ver the lazy dog"}) .await; + + // works on empty lines + cx.set_shared_state(indoc! {" + a + ˇ + b + "}) + .await; + let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! {" + a + « + ˇ»b + "}) + .await; + cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + + // toggles off again + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! {" + a + ˇ + b + "}) + .await; + + // works at the end of a document + cx.set_shared_state(indoc! {" + a + b + ˇ"}) + .await; + + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! {" + a + b + ˇ"}) + .await; + assert_eq!(cx.mode(), cx.neovim_mode().await); + } + + #[gpui::test] + async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["shift-v"]).await; + cx.assert_shared_state(indoc! { "The «qˇ»uick brown + fox jumps over + the lazy dog"}) + .await; + assert_eq!(cx.mode(), cx.neovim_mode().await); + cx.simulate_shared_keystrokes(["x"]).await; + cx.assert_shared_state(indoc! { "fox ˇjumps over + the lazy dog"}) + .await; + + // it should work on empty lines + cx.set_shared_state(indoc! {" + a + ˇ + b"}) + .await; + cx.simulate_shared_keystrokes(["shift-v"]).await; + cx.assert_shared_state(indoc! { " + a + « + ˇ»b"}) + .await; + cx.simulate_shared_keystrokes(["x"]).await; + cx.assert_shared_state(indoc! { " + a + ˇb"}) + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json new file mode 100644 index 0000000000..09d7288d82 --- /dev/null +++ b/crates/vim/test_data/test_enter_visual_line_mode.json @@ -0,0 +1,10 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"shift-v"} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}} +{"Key":"x"} +{"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}} +{"Put":{"state":"a\nˇ\nb"}} +{"Key":"shift-v"} +{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}} +{"Key":"x"} +{"Get":{"state":"a\nˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json index 6b5274b892..4fdb4c7667 100644 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ b/crates/vim/test_data/test_enter_visual_mode.json @@ -10,3 +10,11 @@ {"Key":"k"} {"Key":"b"} {"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Put":{"state":"a\nˇ\nb\n"}} +{"Key":"v"} +{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}} +{"Key":"v"} +{"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}} +{"Put":{"state":"a\nb\nˇ"}} +{"Key":"v"} +{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}} diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json new file mode 100644 index 0000000000..f683c0a314 --- /dev/null +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -0,0 +1,10 @@ +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n ˇreturn false\n}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":{"Visual":{"line":false}}}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":{"Visual":{"line":false}}}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index b1765459e0..b1c43bf9a2 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -43,17 +43,17 @@ {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} @@ -118,7 +118,7 @@ {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} @@ -158,17 +158,17 @@ {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} @@ -233,4 +233,4 @@ {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} From 3f2f3bb78dec914f0ab627111851d8b370918304 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 28 Jul 2023 13:46:25 -0600 Subject: [PATCH 023/326] Fix crash when deleting a long line in visual line mode --- crates/vim/src/visual.rs | 18 ++++++++++++++++-- .../vim/test_data/test_visual_line_delete.json | 5 +++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index eae3c80f38..cad82bc1ea 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -197,6 +197,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext = Default::default(); let line_mode = editor.selections.line_mode; + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { if line_mode { @@ -218,9 +219,13 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext Date: Fri, 28 Jul 2023 13:54:03 -0600 Subject: [PATCH 024/326] Add o/O for flipping selection --- assets/keymaps/vim.json | 2 ++ crates/vim/src/visual.rs | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index ce5ce5aabd..550d294ea0 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -360,6 +360,8 @@ "context": "Editor && vim_mode == visual && !VimWaiting", "bindings": { "u": "editor::Undo", + "o": "vim::OtherEnd", + "shift-o": "vim::OtherEnd", "c": "vim::VisualChange", "d": "vim::VisualDelete", "x": "vim::VisualDelete", diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index cad82bc1ea..ed41c70daa 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -24,13 +24,15 @@ actions!( VisualDelete, VisualChange, VisualYank, - VisualPaste + VisualPaste, + OtherEnd, ] ); pub fn init(cx: &mut AppContext) { cx.add_action(toggle_visual); cx.add_action(toggle_visual_line); + cx.add_action(other_end); cx.add_action(change); cx.add_action(delete); cx.add_action(yank); @@ -150,6 +152,18 @@ pub fn toggle_visual_line( }) } +pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|_, selection| { + selection.reversed = !selection.reversed; + }) + }) + }) + }); +} + pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { From 236b755b1dc443f65914e7420cf259ec41caa90a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 28 Jul 2023 14:14:15 -0600 Subject: [PATCH 025/326] Fix substitute in visual line mode --- crates/vim/src/normal/substitute.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 2f022dca98..57388710e9 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,6 +4,7 @@ use language::Point; use crate::{motion::Motion, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { + let line_mode = vim.state.mode == Mode::Visual { line: true }; vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -12,6 +13,17 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { if selection.start == selection.end { Motion::Right.expand_selection(map, selection, count, true); } + if line_mode { + Motion::CurrentLine.expand_selection(map, selection, None, false); + if let Some((point, _)) = Motion::FirstNonWhitespace.move_point( + map, + selection.start, + selection.goal, + None, + ) { + selection.start = point; + } + } }) }); let selections = editor.selections.all::(cx); @@ -67,5 +79,19 @@ mod test { // should transactionally undo selection changes cx.simulate_keystrokes(["escape", "u"]); cx.assert_editor_state("ˇcàfé\n"); + + // it handles visual line mode + cx.set_state( + indoc! {" + alpha + beˇta + gamma"}, + Mode::Normal, + ); + cx.simulate_keystrokes(["shift-v", "s"]); + cx.assert_editor_state(indoc! {" + alpha + ˇ + gamma"}); } } From 0c15ef730535dd96687aac7c24009a5b6b3fda02 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 28 Jul 2023 14:38:45 -0600 Subject: [PATCH 026/326] Merge VisualChange -> Substitute They both are supposed to work the same. --- assets/keymaps/vim.json | 2 +- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/substitute.rs | 84 +++++++++++-- crates/vim/src/visual.rs | 121 +------------------ crates/vim/test_data/test_visual_delete.json | 4 + 5 files changed, 87 insertions(+), 126 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 550d294ea0..880fe20188 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -362,12 +362,12 @@ "u": "editor::Undo", "o": "vim::OtherEnd", "shift-o": "vim::OtherEnd", - "c": "vim::VisualChange", "d": "vim::VisualDelete", "x": "vim::VisualDelete", "y": "vim::VisualYank", "p": "vim::VisualPaste", "s": "vim::Substitute", + "c": "vim::Substitute", "~": "vim::ChangeCase", "r": [ "vim::PushOperator", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 79c990ffeb..5ac3e86165 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -3,7 +3,7 @@ mod change; mod delete; mod scroll; mod search; -mod substitute; +pub mod substitute; mod yank; use std::{borrow::Cow, sync::Arc}; diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 57388710e9..d2429433fe 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,7 +1,7 @@ use gpui::WindowContext; use language::Point; -use crate::{motion::Motion, Mode, Vim}; +use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { let line_mode = vim.state.mode == Mode::Visual { line: true }; @@ -26,19 +26,20 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { } }) }); - let selections = editor.selections.all::(cx); - for selection in selections.into_iter().rev() { - editor.buffer().update(cx, |buffer, cx| { - buffer.edit([(selection.start..selection.end, "")], None, cx) - }) - } + copy_selections_content(editor, line_mode, cx); + let selections = editor.selections.all::(cx).into_iter(); + let edits = selections.map(|selection| (selection.start..selection.end, "")); + editor.edit(edits, cx); }); }); } #[cfg(test)] mod test { - use crate::{state::Mode, test::VimTestContext}; + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; use indoc::indoc; #[gpui::test] @@ -94,4 +95,71 @@ mod test { ˇ gamma"}); } + + #[gpui::test] + async fn test_visual_change(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("The quick ˇbrown").await; + cx.simulate_shared_keystrokes(["v", "w", "c"]).await; + cx.assert_shared_state("The quick ˇ").await; + + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await; + cx.assert_shared_state(indoc! {" + The ˇver + the lazy dog"}) + .await; + + let cases = cx.each_marked_position(indoc! {" + The ˇquick brown + fox jumps ˇover + the ˇlazy dog"}); + for initial_state in cases { + cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"]) + .await; + cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"]) + .await; + } + } + + #[gpui::test] + async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx) + .await + .binding(["shift-v", "c"]); + cx.assert(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + .await; + // Test pasting code copied on change + cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; + cx.assert_state_matches().await; + + cx.assert_all(indoc! {" + The quick brown + fox juˇmps over + the laˇzy dog"}) + .await; + let mut cx = cx.binding(["shift-v", "j", "c"]); + cx.assert(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + .await; + // Test pasting code copied on delete + cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; + cx.assert_state_matches().await; + + cx.assert_all(indoc! {" + The quick brown + fox juˇmps over + the laˇzy dog"}) + .await; + } } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ed41c70daa..a062c5972e 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -22,7 +22,6 @@ actions!( ToggleVisual, ToggleVisualLine, VisualDelete, - VisualChange, VisualYank, VisualPaste, OtherEnd, @@ -33,7 +32,6 @@ pub fn init(cx: &mut AppContext) { cx.add_action(toggle_visual); cx.add_action(toggle_visual_line); cx.add_action(other_end); - cx.add_action(change); cx.add_action(delete); cx.add_action(yank); cx.add_action(paste); @@ -164,48 +162,6 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { - // Compute edits and resulting anchor selections. If in line mode, adjust - // the anchor location and additional newline - let mut edits = Vec::new(); - let mut new_selections = Vec::new(); - let line_mode = editor.selections.line_mode; - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let range = selection.map(|p| p.to_point(map)).range(); - let expanded_range = map.expand_to_line(range); - // If we are at the last line, the anchor needs to be after the newline so that - // it is on a line of its own. Otherwise, the anchor may be after the newline - let anchor = if expanded_range.end == map.buffer_snapshot.max_point() { - map.buffer_snapshot.anchor_after(expanded_range.end) - } else { - map.buffer_snapshot.anchor_before(expanded_range.start) - }; - - edits.push((expanded_range, "\n")); - new_selections.push(selection.map(|_| anchor)); - } else { - let range = selection.map(|p| p.to_point(map)).range(); - let anchor = map.buffer_snapshot.anchor_after(range.end); - edits.push((range, "")); - new_selections.push(selection.map(|_| anchor)); - } - selection.goal = SelectionGoal::None; - }); - }); - copy_selections_content(editor, editor.selections.line_mode, cx); - editor.edit_with_autoindent(edits, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_anchors(new_selections); - }); - }); - vim.switch_mode(Mode::Insert, true, cx); - }); -} - pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { @@ -228,16 +184,13 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext Date: Fri, 28 Jul 2023 15:36:14 -0600 Subject: [PATCH 027/326] Review --- crates/editor/src/selections_collection.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 5f88eaecd8..1921bc0738 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -498,7 +498,6 @@ impl<'a> MutableSelectionsCollection<'a> { T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { let buffer = self.buffer.read(self.cx).snapshot(self.cx); - selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. let mut i = 1; From 758e1f6e5752b1e3fc994d885865a24da1afb45c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 28 Jul 2023 14:56:02 -0700 Subject: [PATCH 028/326] Get DB channels query working with postgres Co-authored-by: Mikayla --- .../20230727150500_add_channels.sql | 21 +++-- crates/collab/src/db.rs | 76 +------------------ crates/collab/src/db/tests.rs | 67 ++++++++++++++++ 3 files changed, 86 insertions(+), 78 deletions(-) diff --git a/crates/collab/migrations/20230727150500_add_channels.sql b/crates/collab/migrations/20230727150500_add_channels.sql index a62eb0aaaf..0073d29c68 100644 --- a/crates/collab/migrations/20230727150500_add_channels.sql +++ b/crates/collab/migrations/20230727150500_add_channels.sql @@ -1,19 +1,28 @@ +DROP TABLE "channel_messages"; +DROP TABLE "channel_memberships"; +DROP TABLE "org_memberships"; +DROP TABLE "orgs"; +DROP TABLE "channels"; + CREATE TABLE "channels" ( "id" SERIAL PRIMARY KEY, - "id_path" TEXT NOT NULL, "name" VARCHAR NOT NULL, "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now -) + "created_at" TIMESTAMP NOT NULL DEFAULT now() +); -CREATE UNIQUE INDEX "index_channels_on_id_path" ON "channels" ("id_path"); +CREATE TABLE "channel_parents" ( + "child_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + "parent_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, + PRIMARY KEY(child_id, parent_id) +); CREATE TABLE "channel_members" ( "id" SERIAL PRIMARY KEY, "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, - "updated_at" TIMESTAMP NOT NULL DEFAULT now -) + "updated_at" TIMESTAMP NOT NULL DEFAULT now() +); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5755ed73e2..d3336824e6 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -39,10 +39,7 @@ use sea_orm::{ DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement, TransactionTrait, }; -use sea_query::{ - Alias, ColumnRef, CommonTableExpression, Expr, OnConflict, Order, Query, QueryStatementWriter, - SelectStatement, UnionType, WithClause, -}; +use sea_query::{Alias, Expr, OnConflict, Query}; use serde::{Deserialize, Serialize}; pub use signup::{Invite, NewSignup, WaitlistSummary}; use sqlx::migrate::{Migrate, Migration, MigrationSource}; @@ -3086,13 +3083,12 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - // This is the SQL statement we want to generate: let sql = r#" WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS ( - SELECT channel_id as child_id, NULL as parent_id, 0 + SELECT channel_id as child_id, CAST(NULL as INTEGER) as parent_id, 0 FROM channel_members - WHERE user_id = ? - UNION ALL + WHERE user_id = $1 + UNION SELECT channel_parents.child_id, channel_parents.parent_id, channel_tree.depth + 1 FROM channel_parents, channel_tree WHERE channel_parents.parent_id = channel_tree.child_id @@ -3103,70 +3099,6 @@ impl Database { ORDER BY channel_tree.depth; "#; - // let root_channel_ids_query = SelectStatement::new() - // .column(channel_member::Column::ChannelId) - // .expr(Expr::value("NULL")) - // .from(channel_member::Entity.table_ref()) - // .and_where( - // Expr::col(channel_member::Column::UserId) - // .eq(Expr::cust_with_values("?", vec![user_id])), - // ); - - // let build_tree_query = SelectStatement::new() - // .column(channel_parent::Column::ChildId) - // .column(channel_parent::Column::ParentId) - // .expr(Expr::col(Alias::new("channel_tree.depth")).add(1i32)) - // .from(Alias::new("channel_tree")) - // .and_where( - // Expr::col(channel_parent::Column::ParentId) - // .equals(Alias::new("channel_tree"), Alias::new("child_id")), - // ) - // .to_owned(); - - // let common_table_expression = CommonTableExpression::new() - // .query( - // root_channel_ids_query - // .union(UnionType::Distinct, build_tree_query) - // .to_owned(), - // ) - // .column(Alias::new("child_id")) - // .column(Alias::new("parent_id")) - // .column(Alias::new("depth")) - // .table_name(Alias::new("channel_tree")) - // .to_owned(); - - // let select = SelectStatement::new() - // .expr_as( - // Expr::col(Alias::new("channel_tree.child_id")), - // Alias::new("id"), - // ) - // .column(channel::Column::Name) - // .column(Alias::new("channel_tree.parent_id")) - // .from(Alias::new("channel_tree")) - // .inner_join( - // channel::Entity.table_ref(), - // Expr::eq( - // channel::Column::Id.into_expr(), - // Expr::tbl(Alias::new("channel_tree"), Alias::new("child_id")), - // ), - // ) - // .order_by(Alias::new("channel_tree.child_id"), Order::Asc) - // .to_owned(); - - // let with_clause = WithClause::new() - // .recursive(true) - // .cte(common_table_expression) - // .to_owned(); - - // let query = select.with(with_clause); - - // let query = SelectStatement::new() - // .column(ColumnRef::Asterisk) - // .from_subquery(query, Alias::new("channel_tree") - // .to_owned(); - - // let stmt = self.pool.get_database_backend().build(&query); - let stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 855dfec91f..53c35ef31b 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -879,6 +879,73 @@ async fn test_invite_codes() { assert!(db.has_contact(user5, user1).await.unwrap()); } +test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { + let a_id = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed").await.unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id)).await.unwrap(); + let livestreaming_id = db + .create_channel("livestreaming", Some(zed_id)) + .await + .unwrap(); + let replace_id = db.create_channel("replace", Some(zed_id)).await.unwrap(); + let rust_id = db.create_root_channel("rust").await.unwrap(); + let cargo_id = db.create_channel("cargo", Some(rust_id)).await.unwrap(); + + db.add_channel_member(zed_id, a_id).await.unwrap(); + db.add_channel_member(rust_id, a_id).await.unwrap(); + + let channels = db.get_channels(a_id).await.unwrap(); + + assert_eq!( + channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + }, + Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + }, + Channel { + id: cargo_id, + name: "cargo".to_string(), + parent_id: Some(rust_id), + } + ] + ); +}); + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); From 4b94bfa04522273d605f1a208fc64db8fa20fb19 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 28 Jul 2023 17:05:56 -0700 Subject: [PATCH 029/326] Set up basic RPC for managing channels Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 189 ++++++++++++++++++ crates/client/src/client.rs | 2 + .../20221109000000_test_schema.sql | 1 + .../20230727150500_add_channels.sql | 1 + crates/collab/src/db.rs | 110 +++++++++- crates/collab/src/db/channel_member.rs | 2 + crates/collab/src/db/tests.rs | 21 +- crates/collab/src/rpc.rs | 100 ++++++++- crates/collab/src/tests.rs | 9 +- crates/collab/src/tests/channel_tests.rs | 149 ++++++++------ crates/rpc/proto/zed.proto | 81 +++----- crates/rpc/src/proto.rs | 26 +-- 12 files changed, 541 insertions(+), 150 deletions(-) create mode 100644 crates/client/src/channel_store.rs diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs new file mode 100644 index 0000000000..a72a189415 --- /dev/null +++ b/crates/client/src/channel_store.rs @@ -0,0 +1,189 @@ +use crate::{Client, Subscription, User, UserStore}; +use anyhow::Result; +use futures::Future; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use rpc::{proto, TypedEnvelope}; +use std::sync::Arc; + +pub struct ChannelStore { + channels: Vec, + channel_invitations: Vec, + client: Arc, + user_store: ModelHandle, + rpc_subscription: Subscription, +} + +#[derive(Debug, PartialEq)] +pub struct Channel { + pub id: u64, + pub name: String, + pub parent_id: Option, +} + +impl Entity for ChannelStore { + type Event = (); +} + +impl ChannelStore { + pub fn new( + client: Arc, + user_store: ModelHandle, + cx: &mut ModelContext, + ) -> Self { + let rpc_subscription = + client.add_message_handler(cx.handle(), Self::handle_update_channels); + Self { + channels: vec![], + channel_invitations: vec![], + client, + user_store, + rpc_subscription, + } + } + + pub fn channels(&self) -> &[Channel] { + &self.channels + } + + pub fn channel_invitations(&self) -> &[Channel] { + &self.channel_invitations + } + + pub fn create_channel( + &self, + name: &str, + parent_id: Option, + ) -> impl Future> { + let client = self.client.clone(); + let name = name.to_owned(); + async move { + Ok(client + .request(proto::CreateChannel { name, parent_id }) + .await? + .channel_id) + } + } + + pub fn invite_member( + &self, + channel_id: u64, + user_id: u64, + admin: bool, + ) -> impl Future> { + let client = self.client.clone(); + async move { + client + .request(proto::InviteChannelMember { + channel_id, + user_id, + admin, + }) + .await?; + Ok(()) + } + } + + pub fn respond_to_channel_invite( + &mut self, + channel_id: u64, + accept: bool, + ) -> impl Future> { + let client = self.client.clone(); + async move { + client + .request(proto::RespondToChannelInvite { channel_id, accept }) + .await?; + Ok(()) + } + } + + pub fn remove_member( + &self, + channel_id: u64, + user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + todo!() + } + + pub fn channel_members( + &self, + channel_id: u64, + cx: &mut ModelContext, + ) -> Task>>> { + todo!() + } + + pub fn add_guest_channel(&self, channel_id: u64) -> Task> { + todo!() + } + + async fn handle_update_channels( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let payload = message.payload; + this.update(&mut cx, |this, cx| { + this.channels + .retain(|channel| !payload.remove_channels.contains(&channel.id)); + this.channel_invitations + .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); + + for channel in payload.channel_invitations { + if let Some(existing_channel) = this + .channel_invitations + .iter_mut() + .find(|c| c.id == channel.id) + { + existing_channel.name = channel.name; + continue; + } + + this.channel_invitations.insert( + 0, + Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }, + ); + } + + for channel in payload.channels { + if let Some(existing_channel) = + this.channels.iter_mut().find(|c| c.id == channel.id) + { + existing_channel.name = channel.name; + continue; + } + + if let Some(parent_id) = channel.parent_id { + if let Some(ix) = this.channels.iter().position(|c| c.id == parent_id) { + this.channels.insert( + ix + 1, + Channel { + id: channel.id, + name: channel.name, + parent_id: Some(parent_id), + }, + ); + } + } else { + this.channels.insert( + 0, + Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }, + ); + } + } + cx.notify(); + }); + + Ok(()) + } +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 78bcc55e93..af33c738ce 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,6 +1,7 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod channel_store; pub mod telemetry; pub mod user; @@ -44,6 +45,7 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; +pub use channel_store::*; pub use rpc::*; pub use telemetry::ClickhouseEvent; pub use user::*; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index b397438e27..1ead36fde2 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -203,6 +203,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, + "accepted" BOOLEAN NOT NULL DEFAULT false, "updated_at" TIMESTAMP NOT NULL DEFAULT now ); diff --git a/crates/collab/migrations/20230727150500_add_channels.sql b/crates/collab/migrations/20230727150500_add_channels.sql index 0073d29c68..0588677792 100644 --- a/crates/collab/migrations/20230727150500_add_channels.sql +++ b/crates/collab/migrations/20230727150500_add_channels.sql @@ -22,6 +22,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, + "accepted" BOOLEAN NOT NULL DEFAULT false, "updated_at" TIMESTAMP NOT NULL DEFAULT now() ); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d3336824e6..46fca04658 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3032,11 +3032,16 @@ impl Database { // channels - pub async fn create_root_channel(&self, name: &str) -> Result { - self.create_channel(name, None).await + pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { + self.create_channel(name, None, creator_id).await } - pub async fn create_channel(&self, name: &str, parent: Option) -> Result { + pub async fn create_channel( + &self, + name: &str, + parent: Option, + creator_id: UserId, + ) -> Result { self.transaction(move |tx| async move { let tx = tx; @@ -3056,19 +3061,50 @@ impl Database { .await?; } + channel_member::ActiveModel { + channel_id: ActiveValue::Set(channel.id), + user_id: ActiveValue::Set(creator_id), + accepted: ActiveValue::Set(true), + admin: ActiveValue::Set(true), + ..Default::default() + } + .insert(&*tx) + .await?; + Ok(channel.id) }) .await } - // Property: Members are only - pub async fn add_channel_member(&self, channel_id: ChannelId, user_id: UserId) -> Result<()> { + pub async fn invite_channel_member( + &self, + channel_id: ChannelId, + invitee_id: UserId, + inviter_id: UserId, + is_admin: bool, + ) -> Result<()> { self.transaction(move |tx| async move { let tx = tx; + // Check if inviter is a member + channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(inviter_id)) + .and(channel_member::Column::Admin.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| { + anyhow!("Inviter does not have permissions to invite the invitee") + })?; + let channel_membership = channel_member::ActiveModel { channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(user_id), + user_id: ActiveValue::Set(invitee_id), + accepted: ActiveValue::Set(false), + admin: ActiveValue::Set(is_admin), ..Default::default() }; @@ -3079,6 +3115,50 @@ impl Database { .await } + pub async fn respond_to_channel_invite( + &self, + channel_id: ChannelId, + user_id: UserId, + accept: bool, + ) -> Result<()> { + self.transaction(move |tx| async move { + let tx = tx; + + let rows_affected = if accept { + channel_member::Entity::update_many() + .set(channel_member::ActiveModel { + accepted: ActiveValue::Set(accept), + ..Default::default() + }) + .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 + } else { + channel_member::ActiveModel { + channel_id: ActiveValue::Unchanged(channel_id), + user_id: ActiveValue::Unchanged(user_id), + ..Default::default() + } + .delete(&*tx) + .await? + .rows_affected + }; + + if rows_affected == 0 { + Err(anyhow!("no such invitation"))?; + } + + Ok(()) + }) + .await + } + pub async fn get_channels(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { let tx = tx; @@ -3087,7 +3167,7 @@ impl Database { WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS ( SELECT channel_id as child_id, CAST(NULL as INTEGER) as parent_id, 0 FROM channel_members - WHERE user_id = $1 + WHERE user_id = $1 AND accepted UNION SELECT channel_parents.child_id, channel_parents.parent_id, channel_tree.depth + 1 FROM channel_parents, channel_tree @@ -3114,6 +3194,22 @@ impl Database { .await } + pub async fn get_channel(&self, channel_id: ChannelId) -> Result { + self.transaction(|tx| async move { + let tx = tx; + let channel = channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such channel"))?; + Ok(Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }) + }) + .await + } + async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, diff --git a/crates/collab/src/db/channel_member.rs b/crates/collab/src/db/channel_member.rs index cad7f3853d..f0f1a852cb 100644 --- a/crates/collab/src/db/channel_member.rs +++ b/crates/collab/src/db/channel_member.rs @@ -10,6 +10,8 @@ pub struct Model { pub id: ChannelMemberId, pub channel_id: ChannelId, pub user_id: UserId, + pub accepted: bool, + pub admin: bool, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 53c35ef31b..03e9eb577b 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -894,18 +894,21 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed").await.unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id)).await.unwrap(); + let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); + let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id)) + .create_channel("livestreaming", Some(zed_id), a_id) + .await + .unwrap(); + let replace_id = db + .create_channel("replace", Some(zed_id), a_id) + .await + .unwrap(); + let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); + let cargo_id = db + .create_channel("cargo", Some(rust_id), a_id) .await .unwrap(); - let replace_id = db.create_channel("replace", Some(zed_id)).await.unwrap(); - let rust_id = db.create_root_channel("rust").await.unwrap(); - let cargo_id = db.create_channel("cargo", Some(rust_id)).await.unwrap(); - - db.add_channel_member(zed_id, a_id).await.unwrap(); - db.add_channel_member(rust_id, a_id).await.unwrap(); let channels = db.get_channels(a_id).await.unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 14d785307d..3d95d484ee 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,7 @@ mod connection_pool; use crate::{ auth, - db::{self, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{self, ChannelId, Database, ProjectId, RoomId, ServerId, User, UserId}, executor::Executor, AppState, Result, }; @@ -239,6 +239,10 @@ impl Server { .add_request_handler(request_contact) .add_request_handler(remove_contact) .add_request_handler(respond_to_contact_request) + .add_request_handler(create_channel) + .add_request_handler(invite_channel_member) + .add_request_handler(remove_channel_member) + .add_request_handler(respond_to_channel_invite) .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) @@ -2084,6 +2088,100 @@ async fn remove_contact( Ok(()) } +async fn create_channel( + request: proto::CreateChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let id = db + .create_channel( + &request.name, + request.parent_id.map(|id| ChannelId::from_proto(id)), + session.user_id, + ) + .await?; + + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: id.to_proto(), + name: request.name, + parent_id: request.parent_id, + }); + session.peer.send(session.connection_id, update)?; + response.send(proto::CreateChannelResponse { + channel_id: id.to_proto(), + })?; + + Ok(()) +} + +async fn invite_channel_member( + request: proto::InviteChannelMember, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let channel = db.get_channel(channel_id).await?; + let invitee_id = UserId::from_proto(request.user_id); + db.invite_channel_member(channel_id, invitee_id, session.user_id, false) + .await?; + + let mut update = proto::UpdateChannels::default(); + update.channel_invitations.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + }); + for connection_id in session + .connection_pool() + .await + .user_connection_ids(invitee_id) + { + session.peer.send(connection_id, update.clone())?; + } + + response.send(proto::Ack {})?; + Ok(()) +} + +async fn remove_channel_member( + request: proto::RemoveChannelMember, + response: Response, + session: Session, +) -> Result<()> { + Ok(()) +} + +async fn respond_to_channel_invite( + request: proto::RespondToChannelInvite, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let channel = db.get_channel(channel_id).await?; + db.respond_to_channel_invite(channel_id, session.user_id, request.accept) + .await?; + + let mut update = proto::UpdateChannels::default(); + update + .remove_channel_invitations + .push(channel_id.to_proto()); + if request.accept { + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + }); + } + session.peer.send(session.connection_id, update)?; + response.send(proto::Ack {})?; + + 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.rs b/crates/collab/src/tests.rs index 2e98cd9b4d..cf302d3b4d 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -7,7 +7,8 @@ use crate::{ use anyhow::anyhow; use call::ActiveCall; use client::{ - self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, + self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError, + UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -33,9 +34,9 @@ use std::{ use util::http::FakeHttpClient; use workspace::Workspace; +mod channel_tests; mod integration_tests; mod randomized_integration_tests; -mod channel_tests; struct TestServer { app_state: Arc, @@ -187,6 +188,8 @@ impl TestServer { let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), @@ -218,6 +221,7 @@ impl TestServer { username: name.to_string(), state: Default::default(), user_store, + channel_store, fs, language_registry: Arc::new(LanguageRegistry::test()), }; @@ -320,6 +324,7 @@ struct TestClient { username: String, state: RefCell, pub user_store: ModelHandle, + pub channel_store: ModelHandle, language_registry: Arc, fs: Arc, } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 8ab33adcbf..4cc0d24d9b 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,85 +1,108 @@ +use client::Channel; use gpui::{executor::Deterministic, TestAppContext}; use std::sync::Arc; -use crate::db::Channel; - use super::TestServer; #[gpui::test] -async fn test_basic_channels(deterministic: Arc, cx: &mut TestAppContext) { +async fn test_basic_channels( + 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, "user_a").await; - let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); - let db = server._test_db.db(); + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; - let zed_id = db.create_root_channel("zed").await.unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id)).await.unwrap(); - let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id)) + let channel_a_id = client_a + .channel_store + .update(cx_a, |channel_store, _| { + channel_store.create_channel("channel-a", None) + }) .await .unwrap(); - let replace_id = db.create_channel("replace", Some(zed_id)).await.unwrap(); - let rust_id = db.create_root_channel("rust").await.unwrap(); - let cargo_id = db.create_channel("cargo", Some(rust_id)).await.unwrap(); - db.add_channel_member(zed_id, a_id).await.unwrap(); - db.add_channel_member(rust_id, a_id).await.unwrap(); - - let channels = db.get_channels(a_id).await.unwrap(); - assert_eq!( - channels, - vec![ - Channel { - id: zed_id, - name: "zed".to_string(), + client_a.channel_store.read_with(cx_a, |channels, _| { + assert_eq!( + channels.channels(), + &[Channel { + id: channel_a_id, + name: "channel-a".to_string(), parent_id: None, - }, - Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - }, - Channel { - id: crdb_id, - name: "crdb".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: livestreaming_id, - name: "livestreaming".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: replace_id, - name: "replace".to_string(), - parent_id: Some(zed_id), - }, - Channel { - id: cargo_id, - name: "cargo".to_string(), - parent_id: Some(rust_id), - } - ] - ); -} + }] + ) + }); -#[gpui::test] -async fn test_block_cycle_creation(deterministic: Arc, cx: &mut TestAppContext) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx, "user_a").await; - let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); - let db = server._test_db.db(); + client_b + .channel_store + .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); - let zed_id = db.create_root_channel("zed").await.unwrap(); - let first_id = db.create_channel("first", Some(zed_id)).await.unwrap(); - let second_id = db - .create_channel("second_id", Some(first_id)) + // Invite client B to channel A as client A. + client_a + .channel_store + .update(cx_a, |channel_store, _| { + channel_store.invite_member(channel_a_id, client_b.user_id().unwrap(), false) + }) .await .unwrap(); + + // Wait for client b to see the invitation + deterministic.run_until_parked(); + + client_b.channel_store.read_with(cx_b, |channels, _| { + assert_eq!( + channels.channel_invitations(), + &[Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + }] + ) + }); + + // Client B now sees that they are in channel A. + client_b + .channel_store + .update(cx_b, |channels, _| { + channels.respond_to_channel_invite(channel_a_id, true) + }) + .await + .unwrap(); + client_b.channel_store.read_with(cx_b, |channels, _| { + assert_eq!(channels.channel_invitations(), &[]); + assert_eq!( + channels.channels(), + &[Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + }] + ) + }); } +// TODO: +// Invariants to test: +// 1. Dag structure is maintained for all operations (can't make a cycle) +// 2. Can't be a member of a super channel, and accept a membership of a sub channel (by definition, a noop) + +// #[gpui::test] +// async fn test_block_cycle_creation(deterministic: Arc, cx: &mut TestAppContext) { +// // deterministic.forbid_parking(); +// // let mut server = TestServer::start(&deterministic).await; +// // let client_a = server.create_client(cx, "user_a").await; +// // let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); +// // let db = server._test_db.db(); + +// // let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); +// // let first_id = db.create_channel("first", Some(zed_id)).await.unwrap(); +// // let second_id = db +// // .create_channel("second_id", Some(first_id)) +// // .await +// // .unwrap(); +// } + /* Linear things: - A way of expressing progress to the team diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a0b98372b1..38ffbe6b7e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -102,17 +102,6 @@ message Envelope { SearchProject search_project = 80; SearchProjectResponse search_project_response = 81; - GetChannels get_channels = 82; - GetChannelsResponse get_channels_response = 83; - JoinChannel join_channel = 84; - JoinChannelResponse join_channel_response = 85; - LeaveChannel leave_channel = 86; - SendChannelMessage send_channel_message = 87; - SendChannelMessageResponse send_channel_message_response = 88; - ChannelMessageSent channel_message_sent = 89; - GetChannelMessages get_channel_messages = 90; - GetChannelMessagesResponse get_channel_messages_response = 91; - UpdateContacts update_contacts = 92; UpdateInviteInfo update_invite_info = 93; ShowContacts show_contacts = 94; @@ -140,6 +129,13 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; RefreshInlayHints refresh_inlay_hints = 118; + + CreateChannel create_channel = 119; + CreateChannelResponse create_channel_response = 120; + InviteChannelMember invite_channel_member = 121; + RemoveChannelMember remove_channel_member = 122; + RespondToChannelInvite respond_to_channel_invite = 123; + UpdateChannels update_channels = 124; } } @@ -867,23 +863,36 @@ message LspDiskBasedDiagnosticsUpdating {} message LspDiskBasedDiagnosticsUpdated {} -message GetChannels {} - -message GetChannelsResponse { +message UpdateChannels { repeated Channel channels = 1; + repeated uint64 remove_channels = 2; + repeated Channel channel_invitations = 3; + repeated uint64 remove_channel_invitations = 4; } -message JoinChannel { +message CreateChannel { + string name = 1; + optional uint64 parent_id = 2; +} + +message CreateChannelResponse { uint64 channel_id = 1; } -message JoinChannelResponse { - repeated ChannelMessage messages = 1; - bool done = 2; +message InviteChannelMember { + uint64 channel_id = 1; + uint64 user_id = 2; + bool admin = 3; } -message LeaveChannel { +message RemoveChannelMember { uint64 channel_id = 1; + uint64 user_id = 2; +} + +message RespondToChannelInvite { + uint64 channel_id = 1; + bool accept = 2; } message GetUsers { @@ -918,31 +927,6 @@ enum ContactRequestResponse { Dismiss = 3; } -message SendChannelMessage { - uint64 channel_id = 1; - string body = 2; - Nonce nonce = 3; -} - -message SendChannelMessageResponse { - ChannelMessage message = 1; -} - -message ChannelMessageSent { - uint64 channel_id = 1; - ChannelMessage message = 2; -} - -message GetChannelMessages { - uint64 channel_id = 1; - uint64 before_message_id = 2; -} - -message GetChannelMessagesResponse { - repeated ChannelMessage messages = 1; - bool done = 2; -} - message UpdateContacts { repeated Contact contacts = 1; repeated uint64 remove_contacts = 2; @@ -1274,14 +1258,7 @@ message Nonce { message Channel { uint64 id = 1; string name = 2; -} - -message ChannelMessage { - uint64 id = 1; - string body = 2; - uint64 timestamp = 3; - uint64 sender_id = 4; - Nonce nonce = 5; + optional uint64 parent_id = 3; } message Contact { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index e24d6cb4b7..1e9e93a2d0 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -143,9 +143,10 @@ messages!( (Call, Foreground), (CallCanceled, Foreground), (CancelCall, Foreground), - (ChannelMessageSent, Foreground), (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), + (CreateChannel, Foreground), + (CreateChannelResponse, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), @@ -158,10 +159,6 @@ messages!( (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (FuzzySearchUsers, Foreground), - (GetChannelMessages, Foreground), - (GetChannelMessagesResponse, Foreground), - (GetChannels, Foreground), - (GetChannelsResponse, Foreground), (GetCodeActions, Background), (GetCodeActionsResponse, Background), (GetHover, Background), @@ -181,14 +178,12 @@ messages!( (GetUsers, Foreground), (Hello, Foreground), (IncomingCall, Foreground), + (InviteChannelMember, Foreground), (UsersResponse, Foreground), - (JoinChannel, Foreground), - (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), - (LeaveChannel, Foreground), (LeaveProject, Foreground), (LeaveRoom, Foreground), (OpenBufferById, Background), @@ -211,18 +206,18 @@ messages!( (RejoinRoom, Foreground), (RejoinRoomResponse, Foreground), (RemoveContact, Foreground), + (RemoveChannelMember, Foreground), (ReloadBuffers, Foreground), (ReloadBuffersResponse, Foreground), (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), (RequestContact, Foreground), (RespondToContactRequest, Foreground), + (RespondToChannelInvite, Foreground), (RoomUpdated, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), - (SendChannelMessage, Foreground), - (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), (ShareProjectResponse, Foreground), (ShowContacts, Foreground), @@ -235,6 +230,7 @@ messages!( (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), + (UpdateChannels, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateFollowers, Foreground), (UpdateInviteInfo, Foreground), @@ -260,13 +256,12 @@ request_messages!( (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), + (CreateChannel, CreateChannelResponse), (DeclineCall, Ack), (DeleteProjectEntry, ProjectEntryResponse), (ExpandProjectEntry, ExpandProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), - (GetChannelMessages, GetChannelMessagesResponse), - (GetChannels, GetChannelsResponse), (GetCodeActions, GetCodeActionsResponse), (GetHover, GetHoverResponse), (GetCompletions, GetCompletionsResponse), @@ -278,7 +273,7 @@ request_messages!( (GetProjectSymbols, GetProjectSymbolsResponse), (FuzzySearchUsers, UsersResponse), (GetUsers, UsersResponse), - (JoinChannel, JoinChannelResponse), + (InviteChannelMember, Ack), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), (LeaveRoom, Ack), @@ -295,12 +290,13 @@ request_messages!( (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), + (RemoveChannelMember, Ack), (RemoveContact, Ack), (RespondToContactRequest, Ack), + (RespondToChannelInvite, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), - (SendChannelMessage, SendChannelMessageResponse), (ShareProject, ShareProjectResponse), (SynchronizeBuffers, SynchronizeBuffersResponse), (Test, Test), @@ -363,8 +359,6 @@ entity_messages!( UpdateDiffBase ); -entity_messages!(channel_id, ChannelMessageSent); - const KIB: usize = 1024; const MIB: usize = KIB * 1024; const MAX_BUFFER_LEN: usize = MIB; From 645c14934411bee4989662945c53d530856f5aaa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 28 Jul 2023 22:23:14 -0600 Subject: [PATCH 030/326] Fix visual selection of trailing newline --- crates/editor/src/display_map.rs | 33 +++++++++-------- crates/editor/src/element.rs | 19 ++++++++-- crates/vim/src/visual.rs | 35 +++++++++++++++++-- .../test_enter_visual_line_mode.json | 5 +++ 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index bf787eb0f7..31183c484d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -353,27 +353,30 @@ impl DisplaySnapshot { } } + // used by line_mode selections and tries to match vim behaviour pub fn expand_to_line(&self, range: Range) -> Range { - let mut new_start = self.prev_line_boundary(range.start).0; - if range.end.column == 0 { - return new_start..range.end; - } - let mut new_end = self.next_line_boundary(range.end).0; + let new_start = if range.start.row == 0 { + Point::new(0, 0) + } else if range.start.row == self.max_buffer_row() + || (range.end.column > 0 && range.end.row == self.max_buffer_row()) + { + Point::new(range.start.row - 1, self.line_len(range.start.row - 1)) + } else { + self.prev_line_boundary(range.start).0 + }; - if new_start.row == range.start.row && new_end.row == range.end.row { - if new_end.row < self.buffer_snapshot.max_point().row { - new_end.row += 1; - new_end.column = 0; - } else if new_start.row > 0 { - new_start.row -= 1; - new_start.column = self.buffer_snapshot.line_len(new_start.row); - } - } + let new_end = if range.end.column == 0 { + range.end + } else if range.end.row < self.max_buffer_row() { + Point::new(range.end.row + 1, 0) + } else { + self.buffer_snapshot.max_point() + }; new_start..new_end } - fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { + pub fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { let inlay_point = self.inlay_snapshot.to_inlay_point(point); let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); let tab_point = self.tab_snapshot.to_tab_point(fold_point); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cce302ed3f..98bf41cdae 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -850,13 +850,16 @@ impl EditorElement { let mut cursor_column = cursor_position.column() as usize; let mut cursor_row = cursor_position.row(); + // highlight the last character in a selection if CursorShape::Block == selection.cursor_shape && !selection.range.is_empty() && !selection.reversed { if cursor_column > 0 { cursor_column -= 1; - } else if cursor_row > 0 { + } else if cursor_row > 0 + && cursor_position != layout.position_map.snapshot.max_point() + { cursor_row -= 1; cursor_column = layout.position_map.snapshot.line_len(cursor_row) as usize; @@ -2186,7 +2189,19 @@ impl Element for EditorElement { for selection in &local_selections { let is_empty = selection.start == selection.end; let selection_start = snapshot.prev_line_boundary(selection.start).1; - let selection_end = snapshot.next_line_boundary(selection.end).1; + let mut selection_end = snapshot.next_line_boundary(selection.end).1; + + // in vim visual mode the newline is considered at the end of the previous line + // instead of at the start of the current line + if editor.cursor_shape == CursorShape::Block + && !is_empty + && !selection.reversed + && selection.end.column == 0 + && selection_end.row() > 0 + && selection_end.row() < snapshot.max_buffer_row() + { + selection_end = DisplayPoint::new(selection_end.row() - 1, 0); + } for row in cmp::max(selection_start.row(), start_row) ..=cmp::min(selection_end.row(), end_row) { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index a062c5972e..8338d32de4 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -49,7 +49,14 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // our motions assume the current character is after the cursor, // but in (forward) visual mode the current character is just // before the end of the selection. - if !selection.reversed { + + // If the file ends with a newline (which is common) we don't do this. + // so that if you go to the end of such a file you can use "up" to go + // to the previous line and have it work somewhat as expected. + if !selection.reversed + && !selection.is_empty() + && !(selection.end.column() == 0 && selection.end == map.max_point()) + { current_head = movement::left(map, selection.end) } @@ -60,7 +67,10 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // ensure the current character is included in the selection. if !selection.reversed { - selection.end = movement::right(map, selection.end) + let next_point = movement::right(map, selection.end); + if !(next_point.column() == 0 && next_point == map.max_point()) { + selection.end = movement::right(map, selection.end) + } } // vim always ensures the anchor character stays selected. @@ -494,6 +504,27 @@ mod test { a ˇb"}) .await; + + // it should work at the end of the document + cx.set_shared_state(indoc! {" + a + b + ˇ"}) + .await; + let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + cx.simulate_shared_keystrokes(["shift-v"]).await; + cx.assert_shared_state(indoc! {" + a + b + ˇ"}) + .await; + assert_eq!(cx.mode(), cx.neovim_mode().await); + cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.simulate_shared_keystrokes(["x"]).await; + cx.assert_shared_state(indoc! {" + a + ˇb"}) + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json index 09d7288d82..6769145412 100644 --- a/crates/vim/test_data/test_enter_visual_line_mode.json +++ b/crates/vim/test_data/test_enter_visual_line_mode.json @@ -8,3 +8,8 @@ {"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}} {"Key":"x"} {"Get":{"state":"a\nˇb","mode":"Normal"}} +{"Put":{"state":"a\nb\nˇ"}} +{"Key":"shift-v"} +{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}} +{"Key":"x"} +{"Get":{"state":"a\nˇb","mode":"Normal"}} From 52a48de9ca94e7b766f40c97c67791982e1a24b2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:44:52 +0200 Subject: [PATCH 031/326] Add WIP Normal button (resuses parts of semantic button, gotta wire it proper) --- crates/search/src/project_search.rs | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1321aadb4f..1951e2c086 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1218,7 +1218,41 @@ impl ProjectSearchBar { ) .into_any() } + fn render_text_search_button(&self, cx: &mut ViewContext) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.semantic.is_none() && !self.is_option_enabled(SearchOptions::REGEX, cx) + } else { + false + }; + let region_id = 4; + enum NormalSearchTag {} + MouseEventHandler::::new(region_id, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); + Label::new("Text", style.text.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_semantic_search(cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + region_id, + format!("Toggle Normal Search"), + None, + tooltip_style, + cx, + ) + .into_any() + } fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { if let Some(search) = self.active_project_search.as_ref() { search.read(cx).search_options.contains(option) @@ -1339,6 +1373,7 @@ impl View for ProjectSearchBar { let semantic_index = SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); + let normal_search = self.render_text_search_button(cx); Flex::row() .with_child( Flex::column() @@ -1395,6 +1430,7 @@ impl View for ProjectSearchBar { .with_child( Flex::column().with_child( Flex::row() + .with_child(normal_search) .with_children(semantic_index) .with_child(regex_button) .flex(1., true) From 92fa879b0c416e48ac25b8d978ad0824d25e544b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 31 Jul 2023 15:27:10 -0700 Subject: [PATCH 032/326] Add ability to join a room from a channel ID co-authored-by: max --- crates/call/src/call.rs | 74 ++++++++++ crates/call/src/room.rs | 9 ++ crates/client/src/channel_store.rs | 4 +- .../20221109000000_test_schema.sql | 4 +- .../20230727150500_add_channels.sql | 3 +- crates/collab/src/db.rs | 129 +++++++++++++++--- crates/collab/src/db/channel.rs | 3 +- crates/collab/src/db/room.rs | 11 +- crates/collab/src/db/tests.rs | 84 ++++++++++-- crates/collab/src/rpc.rs | 115 ++++++++++------ crates/collab/src/tests.rs | 64 ++++++++- crates/collab/src/tests/channel_tests.rs | 55 ++++++++ crates/collab/src/tests/integration_tests.rs | 26 +--- crates/rpc/proto/zed.proto | 5 + crates/rpc/src/proto.rs | 2 + crates/rpc/src/rpc.rs | 2 +- 16 files changed, 485 insertions(+), 105 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 2defd6b40f..1e3a381b40 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -209,6 +209,80 @@ impl ActiveCall { }) } + pub fn join_channel( + &mut self, + channel_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let room = if let Some(room) = self.room().cloned() { + Some(Task::ready(Ok(room)).shared()) + } else { + self.pending_room_creation.clone() + }; + + todo!() + // let invite = if let Some(room) = room { + // cx.spawn_weak(|_, mut cx| async move { + // let room = room.await.map_err(|err| anyhow!("{:?}", err))?; + + // // TODO join_channel: + // // let initial_project_id = if let Some(initial_project) = initial_project { + // // Some( + // // room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) + // // .await?, + // // ) + // // } else { + // // None + // // }; + + // // room.update(&mut cx, |room, cx| { + // // room.call(called_user_id, initial_project_id, cx) + // // }) + // // .await?; + + // anyhow::Ok(()) + // }) + // } else { + // let client = self.client.clone(); + // let user_store = self.user_store.clone(); + // let room = cx + // .spawn(|this, mut cx| async move { + // let create_room = async { + // let room = cx + // .update(|cx| { + // Room::create_from_channel(channel_id, client, user_store, cx) + // }) + // .await?; + + // this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + // .await?; + + // anyhow::Ok(room) + // }; + + // let room = create_room.await; + // this.update(&mut cx, |this, _| this.pending_room_creation = None); + // room.map_err(Arc::new) + // }) + // .shared(); + // self.pending_room_creation = Some(room.clone()); + // cx.foreground().spawn(async move { + // room.await.map_err(|err| anyhow!("{:?}", err))?; + // anyhow::Ok(()) + // }) + // }; + + // cx.spawn(|this, mut cx| async move { + // let result = invite.await; + // this.update(&mut cx, |this, cx| { + // this.pending_invites.remove(&called_user_id); + // this.report_call_event("invite", cx); + // cx.notify(); + // }); + // result + // }) + } + pub fn cancel_invite( &mut self, called_user_id: u64, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 328a94506c..e77b5437b5 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -204,6 +204,15 @@ impl Room { } } + pub(crate) fn create_from_channel( + channel_id: u64, + client: Arc, + user_store: ModelHandle, + cx: &mut AppContext, + ) -> Task>> { + todo!() + } + pub(crate) fn create( called_user_id: u64, initial_project: Option>, diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index a72a189415..e78dafe4e8 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -10,7 +10,7 @@ pub struct ChannelStore { channel_invitations: Vec, client: Arc, user_store: ModelHandle, - rpc_subscription: Subscription, + _rpc_subscription: Subscription, } #[derive(Debug, PartialEq)] @@ -37,7 +37,7 @@ impl ChannelStore { channel_invitations: vec![], client, user_store, - rpc_subscription, + _rpc_subscription: rpc_subscription, } } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 1ead36fde2..6703f98df2 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -36,7 +36,8 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b"); CREATE TABLE "rooms" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "live_kit_room" VARCHAR NOT NULL + "live_kit_room" VARCHAR NOT NULL, + "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE ); CREATE TABLE "projects" ( @@ -188,7 +189,6 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now ); diff --git a/crates/collab/migrations/20230727150500_add_channels.sql b/crates/collab/migrations/20230727150500_add_channels.sql index 0588677792..2d94cb6d97 100644 --- a/crates/collab/migrations/20230727150500_add_channels.sql +++ b/crates/collab/migrations/20230727150500_add_channels.sql @@ -7,7 +7,6 @@ DROP TABLE "channels"; CREATE TABLE "channels" ( "id" SERIAL PRIMARY KEY, "name" VARCHAR NOT NULL, - "room_id" INTEGER REFERENCES rooms (id) ON DELETE SET NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now() ); @@ -27,3 +26,5 @@ CREATE TABLE "channel_members" ( ); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); + +ALTER TABLE rooms ADD COLUMN "channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 46fca04658..5f106023f1 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1337,32 +1337,65 @@ impl Database { &self, room_id: RoomId, user_id: UserId, + channel_id: Option, connection: ConnectionId, ) -> Result> { self.room_transaction(room_id, |tx| async move { - 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 { + if let Some(channel_id) = channel_id { + channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Accepted.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such channel membership"))?; + + 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), + // Redundant for the channel join use case, used for channel and call invitations + 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, + ))), ..Default::default() - }) - .exec(&*tx) + } + .insert(&*tx) .await?; - if result.rows_affected == 0 { - Err(anyhow!("room does not exist or was already joined"))? } else { - let room = self.get_room(room_id, &tx).await?; - Ok(room) + 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 { + 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"))?; + } } + + let room = self.get_room(room_id, &tx).await?; + Ok(room) }) .await } @@ -3071,6 +3104,14 @@ impl Database { .insert(&*tx) .await?; + room::ActiveModel { + channel_id: ActiveValue::Set(Some(channel.id)), + live_kit_room: ActiveValue::Set(format!("channel-{}", channel.id)), + ..Default::default() + } + .insert(&*tx) + .await?; + Ok(channel.id) }) .await @@ -3163,6 +3204,7 @@ impl Database { self.transaction(|tx| async move { let tx = tx; + // Breadth first list of all edges in this user's channels let sql = r#" WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS ( SELECT channel_id as child_id, CAST(NULL as INTEGER) as parent_id, 0 @@ -3173,23 +3215,52 @@ impl Database { FROM channel_parents, channel_tree WHERE channel_parents.parent_id = channel_tree.child_id ) - SELECT channel_tree.child_id as id, channels.name, channel_tree.parent_id + SELECT channel_tree.child_id, channel_tree.parent_id FROM channel_tree - JOIN channels ON channels.id = channel_tree.child_id - ORDER BY channel_tree.depth; + ORDER BY child_id, parent_id IS NOT NULL "#; + #[derive(FromQueryResult, Debug, PartialEq)] + pub struct ChannelParent { + pub child_id: ChannelId, + pub parent_id: Option, + } + let stmt = Statement::from_sql_and_values( self.pool.get_database_backend(), sql, vec![user_id.into()], ); - Ok(channel_parent::Entity::find() + let mut parents_by_child_id = HashMap::default(); + let mut parents = channel_parent::Entity::find() .from_raw_sql(stmt) - .into_model::() - .all(&*tx) - .await?) + .into_model::() + .stream(&*tx).await?; + while let Some(parent) = parents.next().await { + let parent = parent?; + parents_by_child_id.insert(parent.child_id, parent.parent_id); + } + + drop(parents); + + 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, + parent_id: parents_by_child_id.get(&row.id).copied().flatten(), + }); + } + + drop(rows); + + Ok(channels) }) .await } @@ -3210,6 +3281,22 @@ impl Database { .await } + pub async fn get_channel_room(&self, channel_id: ChannelId) -> Result { + self.transaction(|tx| async move { + let tx = tx; + let room = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(room::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid channel"))?; + Ok(room.id) + }) + .await + } + async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, diff --git a/crates/collab/src/db/channel.rs b/crates/collab/src/db/channel.rs index 48e5d50e3e..8834190645 100644 --- a/crates/collab/src/db/channel.rs +++ b/crates/collab/src/db/channel.rs @@ -1,4 +1,4 @@ -use super::{ChannelId, RoomId}; +use super::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -7,7 +7,6 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, - pub room_id: Option, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/room.rs b/crates/collab/src/db/room.rs index c838d1273b..88514ef4f1 100644 --- a/crates/collab/src/db/room.rs +++ b/crates/collab/src/db/room.rs @@ -1,4 +1,4 @@ -use super::RoomId; +use super::{ChannelId, RoomId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: RoomId, pub live_kit_room: String, + pub channel_id: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -17,6 +18,12 @@ pub enum Relation { Project, #[sea_orm(has_many = "super::follower::Entity")] Follower, + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, } impl Related for Entity { @@ -39,7 +46,7 @@ impl Related for Entity { impl Related for Entity { fn to() -> RelationDef { - Relation::Follower.def() + Relation::Channel.def() } } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 03e9eb577b..7ef2b39640 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -494,9 +494,14 @@ test_both_dbs!( ) .await .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); + db.join_room( + room_id, + user2.user_id, + None, + ConnectionId { owner_id, id: 1 }, + ) + .await + .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) @@ -920,11 +925,6 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { name: "zed".to_string(), parent_id: None, }, - Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - }, Channel { id: crdb_id, name: "crdb".to_string(), @@ -940,6 +940,11 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { name: "replace".to_string(), parent_id: Some(zed_id), }, + Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + }, Channel { id: cargo_id, name: "cargo".to_string(), @@ -949,6 +954,69 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { ); }); +test_both_dbs!( + test_joining_channels_postgres, + test_joining_channels_sqlite, + db, + { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + 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 channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); + let room_1 = db.get_channel_room(channel_1).await.unwrap(); + + // can join a room with membership to its channel + let room = db + .join_room( + room_1, + user_1, + Some(channel_1), + ConnectionId { owner_id, id: 1 }, + ) + .await + .unwrap(); + assert_eq!(room.participants.len(), 1); + + drop(room); + // cannot join a room without membership to its channel + assert!(db + .join_room( + room_1, + user_2, + Some(channel_1), + ConnectionId { owner_id, id: 1 } + ) + .await + .is_err()); + } +); + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 3d95d484ee..8cf0b7e48c 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -34,7 +34,10 @@ use futures::{ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ - proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, + proto::{ + self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, + RequestMessage, + }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; use serde::{Serialize, Serializer}; @@ -183,7 +186,7 @@ impl Server { server .add_request_handler(ping) - .add_request_handler(create_room) + .add_request_handler(create_room_request) .add_request_handler(join_room) .add_request_handler(rejoin_room) .add_request_handler(leave_room) @@ -243,6 +246,7 @@ impl Server { .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) .add_request_handler(respond_to_channel_invite) + .add_request_handler(join_channel) .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) @@ -855,48 +859,17 @@ async fn ping(_: proto::Ping, response: Response, _session: Session Ok(()) } -async fn create_room( +async fn create_room_request( _request: proto::CreateRoom, response: Response, session: Session, ) -> Result<()> { - let live_kit_room = nanoid::nanoid!(30); - let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() { - if let Some(_) = live_kit - .create_room(live_kit_room.clone()) - .await - .trace_err() - { - if let Some(token) = live_kit - .room_token(&live_kit_room, &session.user_id.to_string()) - .trace_err() - { - Some(proto::LiveKitConnectionInfo { - server_url: live_kit.url().into(), - token, - }) - } else { - None - } - } else { - None - } - } else { - None - }; + let (room, live_kit_connection_info) = create_room(&session).await?; - { - let room = session - .db() - .await - .create_room(session.user_id, session.connection_id, &live_kit_room) - .await?; - - response.send(proto::CreateRoomResponse { - room: Some(room.clone()), - live_kit_connection_info, - })?; - } + response.send(proto::CreateRoomResponse { + room: Some(room.clone()), + live_kit_connection_info, + })?; update_user_contacts(session.user_id, &session).await?; Ok(()) @@ -912,7 +885,7 @@ async fn join_room( let room = session .db() .await - .join_room(room_id, session.user_id, session.connection_id) + .join_room(room_id, session.user_id, None, session.connection_id) .await?; room_updated(&room, &session.peer); room.clone() @@ -2182,6 +2155,32 @@ async fn respond_to_channel_invite( Ok(()) } +async fn join_channel( + request: proto::JoinChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + todo!(); + // db.check_channel_membership(session.user_id, channel_id) + // .await?; + + let (room, live_kit_connection_info) = create_room(&session).await?; + + // db.set_channel_room(channel_id, room.id).await?; + + response.send(proto::CreateRoomResponse { + room: Some(room.clone()), + live_kit_connection_info, + })?; + + update_user_contacts(session.user_id, &session).await?; + + 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 @@ -2436,6 +2435,42 @@ fn project_left(project: &db::LeftProject, session: &Session) { } } +async fn create_room(session: &Session) -> Result<(proto::Room, Option)> { + let live_kit_room = nanoid::nanoid!(30); + + let live_kit_connection_info = { + let live_kit_room = live_kit_room.clone(); + let live_kit = session.live_kit_client.as_ref(); + + util::async_iife!({ + let live_kit = live_kit?; + + live_kit + .create_room(live_kit_room.clone()) + .await + .trace_err()?; + + let token = live_kit + .room_token(&live_kit_room, &session.user_id.to_string()) + .trace_err()?; + + Some(proto::LiveKitConnectionInfo { + server_url: live_kit.url().into(), + token, + }) + }) + } + .await; + + let room = session + .db() + .await + .create_room(session.user_id, session.connection_id, &live_kit_room) + .await?; + + Ok((room, live_kit_connection_info)) +} + pub trait ResultExt { type Ok; diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index cf302d3b4d..a000fbd92e 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -5,7 +5,7 @@ use crate::{ AppState, }; use anyhow::anyhow; -use call::ActiveCall; +use call::{ActiveCall, Room}; use client::{ self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError, UserStore, @@ -269,6 +269,44 @@ impl TestServer { } } + async fn make_channel( + &self, + channel: &str, + admin: (&TestClient, &mut TestAppContext), + members: &mut [(&TestClient, &mut TestAppContext)], + ) -> u64 { + let (admin_client, admin_cx) = admin; + let channel_id = admin_client + .channel_store + .update(admin_cx, |channel_store, _| { + channel_store.create_channel(channel, None) + }) + .await + .unwrap(); + + for (member_client, member_cx) in members { + admin_client + .channel_store + .update(admin_cx, |channel_store, _| { + channel_store.invite_member(channel_id, member_client.user_id().unwrap(), false) + }) + .await + .unwrap(); + + admin_cx.foreground().run_until_parked(); + + member_client + .channel_store + .update(*member_cx, |channels, _| { + channels.respond_to_channel_invite(channel_id, true) + }) + .await + .unwrap(); + } + + channel_id + } + async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { self.make_contacts(clients).await; @@ -516,3 +554,27 @@ impl Drop for TestClient { self.client.teardown(); } } + +#[derive(Debug, Eq, PartialEq)] +struct RoomParticipants { + remote: Vec, + pending: Vec, +} + +fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomParticipants { + room.read_with(cx, |room, _| { + let mut remote = room + .remote_participants() + .iter() + .map(|(_, participant)| participant.user.github_login.clone()) + .collect::>(); + let mut pending = room + .pending_participants() + .iter() + .map(|user| user.github_login.clone()) + .collect::>(); + remote.sort(); + pending.sort(); + RoomParticipants { remote, pending } + }) +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 4cc0d24d9b..c86238825c 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,7 +1,10 @@ +use call::ActiveCall; use client::Channel; use gpui::{executor::Deterministic, TestAppContext}; use std::sync::Arc; +use crate::tests::{room_participants, RoomParticipants}; + use super::TestServer; #[gpui::test] @@ -82,6 +85,58 @@ async fn test_basic_channels( }); } +#[gpui::test] +async fn test_channel_room( + 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 zed_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + active_call_a + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec!["user_a".to_string()], + pending: vec![] + } + ); + + active_call_b + .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + + let active_call_b = cx_b.read(ActiveCall::global); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + assert_eq!( + room_participants(&room_b, cx_b), + RoomParticipants { + remote: vec!["user_a".to_string(), "user_b".to_string()], + pending: vec![] + } + ); +} + // TODO: // Invariants to test: // 1. Dag structure is maintained for all operations (can't make a cycle) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ab94f16a07..5a27787dbc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1,6 +1,6 @@ use crate::{ rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - tests::{TestClient, TestServer}, + tests::{room_participants, RoomParticipants, TestClient, TestServer}, }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; @@ -8319,30 +8319,6 @@ async fn test_inlay_hint_refresh_is_forwarded( }); } -#[derive(Debug, Eq, PartialEq)] -struct RoomParticipants { - remote: Vec, - pending: Vec, -} - -fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomParticipants { - room.read_with(cx, |room, _| { - let mut remote = room - .remote_participants() - .iter() - .map(|(_, participant)| participant.user.github_login.clone()) - .collect::>(); - let mut pending = room - .pending_participants() - .iter() - .map(|user| user.github_login.clone()) - .collect::>(); - remote.sort(); - pending.sort(); - RoomParticipants { remote, pending } - }) -} - fn extract_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 38ffbe6b7e..8a4a72c268 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -136,6 +136,7 @@ message Envelope { RemoveChannelMember remove_channel_member = 122; RespondToChannelInvite respond_to_channel_invite = 123; UpdateChannels update_channels = 124; + JoinChannel join_channel = 125; } } @@ -870,6 +871,10 @@ message UpdateChannels { repeated uint64 remove_channel_invitations = 4; } +message JoinChannel { + uint64 channel_id = 1; +} + message CreateChannel { string name = 1; optional uint64 parent_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 1e9e93a2d0..c3d65343d6 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -214,6 +214,7 @@ messages!( (RequestContact, Foreground), (RespondToContactRequest, Foreground), (RespondToChannelInvite, Foreground), + (JoinChannel, Foreground), (RoomUpdated, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), @@ -294,6 +295,7 @@ request_messages!( (RemoveContact, Ack), (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), + (JoinChannel, CreateRoomResponse), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 6b430d90e4..3cb8b6bffa 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 = 59; +pub const PROTOCOL_VERSION: u32 = 60; From 003a711deabb3c66b21dc950f35b9ae11edd67d5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 31 Jul 2023 16:48:24 -0700 Subject: [PATCH 033/326] Add room creation from channel join co-authored-by: max --- crates/call/src/call.rs | 98 +++++------------- crates/call/src/room.rs | 59 +++++++++-- crates/collab/src/db.rs | 27 +++-- crates/collab/src/db/tests.rs | 20 ++-- crates/collab/src/rpc.rs | 126 +++++++++++++---------- crates/collab/src/tests/channel_tests.rs | 78 ++++++++------ crates/rpc/src/proto.rs | 2 +- 7 files changed, 229 insertions(+), 181 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 1e3a381b40..3cd868a438 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -209,80 +209,6 @@ impl ActiveCall { }) } - pub fn join_channel( - &mut self, - channel_id: u64, - cx: &mut ModelContext, - ) -> Task> { - let room = if let Some(room) = self.room().cloned() { - Some(Task::ready(Ok(room)).shared()) - } else { - self.pending_room_creation.clone() - }; - - todo!() - // let invite = if let Some(room) = room { - // cx.spawn_weak(|_, mut cx| async move { - // let room = room.await.map_err(|err| anyhow!("{:?}", err))?; - - // // TODO join_channel: - // // let initial_project_id = if let Some(initial_project) = initial_project { - // // Some( - // // room.update(&mut cx, |room, cx| room.share_project(initial_project, cx)) - // // .await?, - // // ) - // // } else { - // // None - // // }; - - // // room.update(&mut cx, |room, cx| { - // // room.call(called_user_id, initial_project_id, cx) - // // }) - // // .await?; - - // anyhow::Ok(()) - // }) - // } else { - // let client = self.client.clone(); - // let user_store = self.user_store.clone(); - // let room = cx - // .spawn(|this, mut cx| async move { - // let create_room = async { - // let room = cx - // .update(|cx| { - // Room::create_from_channel(channel_id, client, user_store, cx) - // }) - // .await?; - - // this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) - // .await?; - - // anyhow::Ok(room) - // }; - - // let room = create_room.await; - // this.update(&mut cx, |this, _| this.pending_room_creation = None); - // room.map_err(Arc::new) - // }) - // .shared(); - // self.pending_room_creation = Some(room.clone()); - // cx.foreground().spawn(async move { - // room.await.map_err(|err| anyhow!("{:?}", err))?; - // anyhow::Ok(()) - // }) - // }; - - // cx.spawn(|this, mut cx| async move { - // let result = invite.await; - // this.update(&mut cx, |this, cx| { - // this.pending_invites.remove(&called_user_id); - // this.report_call_event("invite", cx); - // cx.notify(); - // }); - // result - // }) - } - pub fn cancel_invite( &mut self, called_user_id: u64, @@ -348,6 +274,30 @@ impl ActiveCall { Ok(()) } + pub fn join_channel( + &mut self, + channel_id: u64, + cx: &mut ModelContext, + ) -> Task> { + if let Some(room) = self.room().cloned() { + if room.read(cx).channel_id() == Some(channel_id) { + return Task::ready(Ok(())); + } + } + + let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx); + + cx.spawn(|this, mut cx| async move { + let room = join.await?; + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) + .await?; + this.update(&mut cx, |this, cx| { + this.report_call_event("join channel", cx) + }); + Ok(()) + }) + } + pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { cx.notify(); self.report_call_event("hang up", cx); diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index e77b5437b5..683ff6f4df 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -49,6 +49,7 @@ pub enum Event { pub struct Room { id: u64, + channel_id: Option, live_kit: Option, status: RoomStatus, shared_projects: HashSet>, @@ -93,8 +94,25 @@ impl Entity for Room { } impl Room { + pub fn channel_id(&self) -> Option { + self.channel_id + } + + #[cfg(any(test, feature = "test-support"))] + pub fn is_connected(&self) -> bool { + if let Some(live_kit) = self.live_kit.as_ref() { + matches!( + *live_kit.room.status().borrow(), + live_kit_client::ConnectionState::Connected { .. } + ) + } else { + false + } + } + fn new( id: u64, + channel_id: Option, live_kit_connection_info: Option, client: Arc, user_store: ModelHandle, @@ -185,6 +203,7 @@ impl Room { Self { id, + channel_id, live_kit: live_kit_room, status: RoomStatus::Online, shared_projects: Default::default(), @@ -204,15 +223,6 @@ impl Room { } } - pub(crate) fn create_from_channel( - channel_id: u64, - client: Arc, - user_store: ModelHandle, - cx: &mut AppContext, - ) -> Task>> { - todo!() - } - pub(crate) fn create( called_user_id: u64, initial_project: Option>, @@ -226,6 +236,7 @@ impl Room { let room = cx.add_model(|cx| { Self::new( room_proto.id, + None, response.live_kit_connection_info, client, user_store, @@ -257,6 +268,35 @@ impl Room { }) } + pub(crate) fn join_channel( + channel_id: u64, + client: Arc, + user_store: ModelHandle, + cx: &mut AppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let response = client.request(proto::JoinChannel { channel_id }).await?; + let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; + let room = cx.add_model(|cx| { + Self::new( + room_proto.id, + Some(channel_id), + response.live_kit_connection_info, + client, + user_store, + cx, + ) + }); + + room.update(&mut cx, |room, cx| { + room.apply_room_update(room_proto, cx)?; + anyhow::Ok(()) + })?; + + Ok(room) + }) + } + pub(crate) fn join( call: &IncomingCall, client: Arc, @@ -270,6 +310,7 @@ impl Room { let room = cx.add_model(|cx| { Self::new( room_id, + None, response.live_kit_connection_info, client, user_store, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5f106023f1..f87b68c1ec 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1833,14 +1833,21 @@ impl Database { .await?; let room = self.get_room(room_id, &tx).await?; - if room.participants.is_empty() { - room::Entity::delete_by_id(room_id).exec(&*tx).await?; - } + let deleted = if room.participants.is_empty() { + let result = room::Entity::delete_by_id(room_id) + .filter(room::Column::ChannelId.is_null()) + .exec(&*tx) + .await?; + result.rows_affected > 0 + } else { + false + }; let left_room = LeftRoom { room, left_projects, canceled_calls_to_user_ids, + deleted, }; if left_room.room.participants.is_empty() { @@ -3065,14 +3072,21 @@ impl Database { // channels - pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result { - self.create_channel(name, None, creator_id).await + pub async fn create_root_channel( + &self, + name: &str, + live_kit_room: &str, + creator_id: UserId, + ) -> Result { + self.create_channel(name, None, live_kit_room, creator_id) + .await } pub async fn create_channel( &self, name: &str, parent: Option, + live_kit_room: &str, creator_id: UserId, ) -> Result { self.transaction(move |tx| async move { @@ -3106,7 +3120,7 @@ impl Database { room::ActiveModel { channel_id: ActiveValue::Set(Some(channel.id)), - live_kit_room: ActiveValue::Set(format!("channel-{}", channel.id)), + live_kit_room: ActiveValue::Set(live_kit_room.to_string()), ..Default::default() } .insert(&*tx) @@ -3731,6 +3745,7 @@ pub struct LeftRoom { pub room: proto::Room, pub left_projects: HashMap, pub canceled_calls_to_user_ids: Vec, + pub deleted: bool, } pub struct RefreshedRoom { diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 7ef2b39640..719e8693d4 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -899,19 +899,22 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .unwrap() .user_id; - let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); - let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap(); + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + let crdb_id = db + .create_channel("crdb", Some(zed_id), "2", a_id) + .await + .unwrap(); let livestreaming_id = db - .create_channel("livestreaming", Some(zed_id), a_id) + .create_channel("livestreaming", Some(zed_id), "3", a_id) .await .unwrap(); let replace_id = db - .create_channel("replace", Some(zed_id), a_id) + .create_channel("replace", Some(zed_id), "4", a_id) .await .unwrap(); - let rust_id = db.create_root_channel("rust", a_id).await.unwrap(); + let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); let cargo_id = db - .create_channel("cargo", Some(rust_id), a_id) + .create_channel("cargo", Some(rust_id), "6", a_id) .await .unwrap(); @@ -988,7 +991,10 @@ test_both_dbs!( .unwrap() .user_id; - let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap(); + let channel_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); let room_1 = db.get_channel_room(channel_1).await.unwrap(); // can join a room with membership to its channel diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8cf0b7e48c..0abf2c44a7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -186,7 +186,7 @@ impl Server { server .add_request_handler(ping) - .add_request_handler(create_room_request) + .add_request_handler(create_room) .add_request_handler(join_room) .add_request_handler(rejoin_room) .add_request_handler(leave_room) @@ -859,12 +859,42 @@ async fn ping(_: proto::Ping, response: Response, _session: Session Ok(()) } -async fn create_room_request( +async fn create_room( _request: proto::CreateRoom, response: Response, session: Session, ) -> Result<()> { - let (room, live_kit_connection_info) = create_room(&session).await?; + let live_kit_room = nanoid::nanoid!(30); + + let live_kit_connection_info = { + let live_kit_room = live_kit_room.clone(); + let live_kit = session.live_kit_client.as_ref(); + + util::async_iife!({ + let live_kit = live_kit?; + + live_kit + .create_room(live_kit_room.clone()) + .await + .trace_err()?; + + let token = live_kit + .room_token(&live_kit_room, &session.user_id.to_string()) + .trace_err()?; + + Some(proto::LiveKitConnectionInfo { + server_url: live_kit.url().into(), + token, + }) + }) + } + .await; + + let room = session + .db() + .await + .create_room(session.user_id, session.connection_id, &live_kit_room) + .await?; response.send(proto::CreateRoomResponse { room: Some(room.clone()), @@ -1259,11 +1289,12 @@ async fn update_participant_location( let location = request .location .ok_or_else(|| anyhow!("invalid location"))?; - let room = session - .db() - .await + + let db = session.db().await; + let room = db .update_room_participant_location(room_id, session.connection_id, location) .await?; + room_updated(&room, &session.peer); response.send(proto::Ack {})?; Ok(()) @@ -2067,10 +2098,17 @@ async fn create_channel( session: Session, ) -> Result<()> { let db = session.db().await; + let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); + + if let Some(live_kit) = session.live_kit_client.as_ref() { + live_kit.create_room(live_kit_room.clone()).await?; + } + let id = db .create_channel( &request.name, request.parent_id.map(|id| ChannelId::from_proto(id)), + &live_kit_room, session.user_id, ) .await?; @@ -2160,21 +2198,39 @@ async fn join_channel( response: Response, session: Session, ) -> Result<()> { - let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - todo!(); - // db.check_channel_membership(session.user_id, channel_id) - // .await?; + { + let db = session.db().await; + let room_id = db.get_channel_room(channel_id).await?; - let (room, live_kit_connection_info) = create_room(&session).await?; + let room = db + .join_room( + room_id, + session.user_id, + Some(channel_id), + session.connection_id, + ) + .await?; - // db.set_channel_room(channel_id, room.id).await?; + let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { + let token = live_kit + .room_token(&room.live_kit_room, &session.user_id.to_string()) + .trace_err()?; - response.send(proto::CreateRoomResponse { - room: Some(room.clone()), - live_kit_connection_info, - })?; + Some(LiveKitConnectionInfo { + server_url: live_kit.url().into(), + token, + }) + }); + + response.send(proto::JoinRoomResponse { + room: Some(room.clone()), + live_kit_connection_info, + })?; + + room_updated(&room, &session.peer); + } update_user_contacts(session.user_id, &session).await?; @@ -2367,7 +2423,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { room_id = RoomId::from_proto(left_room.room.id); canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids); live_kit_room = mem::take(&mut left_room.room.live_kit_room); - delete_live_kit_room = left_room.room.participants.is_empty(); + delete_live_kit_room = left_room.deleted; } else { return Ok(()); } @@ -2435,42 +2491,6 @@ fn project_left(project: &db::LeftProject, session: &Session) { } } -async fn create_room(session: &Session) -> Result<(proto::Room, Option)> { - let live_kit_room = nanoid::nanoid!(30); - - let live_kit_connection_info = { - let live_kit_room = live_kit_room.clone(); - let live_kit = session.live_kit_client.as_ref(); - - util::async_iife!({ - let live_kit = live_kit?; - - live_kit - .create_room(live_kit_room.clone()) - .await - .trace_err()?; - - let token = live_kit - .room_token(&live_kit_room, &session.user_id.to_string()) - .trace_err()?; - - Some(proto::LiveKitConnectionInfo { - server_url: live_kit.url().into(), - token, - }) - }) - } - .await; - - let room = session - .db() - .await - .create_room(session.user_id, session.connection_id, &live_kit_room) - .await?; - - Ok((room, live_kit_connection_info)) -} - pub trait ResultExt { type Ok; diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index c86238825c..632bfdca49 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -108,17 +108,52 @@ async fn test_channel_room( .await .unwrap(); + active_call_b + .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) + .await + .unwrap(); + deterministic.run_until_parked(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); assert_eq!( room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: vec![] + } + ); + + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + room_b.read_with(cx_b, |room, _| assert!(room.is_connected())); + assert_eq!( + room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: vec![] } ); + // Make sure that leaving and rejoining works + + active_call_a + .update(cx_a, |active_call, cx| active_call.hang_up(cx)) + .await + .unwrap(); + + active_call_b + .update(cx_b, |active_call, cx| active_call.hang_up(cx)) + .await + .unwrap(); + + // Make sure room exists? + + active_call_a + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) + .await + .unwrap(); + active_call_b .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await @@ -126,42 +161,23 @@ async fn test_channel_room( deterministic.run_until_parked(); - let active_call_b = cx_b.read(ActiveCall::global); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); + room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); + assert_eq!( + room_participants(&room_a, cx_a), + RoomParticipants { + remote: vec!["user_b".to_string()], + pending: vec![] + } + ); + let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone()); + room_b.read_with(cx_b, |room, _| assert!(room.is_connected())); assert_eq!( room_participants(&room_b, cx_b), RoomParticipants { - remote: vec!["user_a".to_string(), "user_b".to_string()], + remote: vec!["user_a".to_string()], pending: vec![] } ); } - -// TODO: -// Invariants to test: -// 1. Dag structure is maintained for all operations (can't make a cycle) -// 2. Can't be a member of a super channel, and accept a membership of a sub channel (by definition, a noop) - -// #[gpui::test] -// async fn test_block_cycle_creation(deterministic: Arc, cx: &mut TestAppContext) { -// // deterministic.forbid_parking(); -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx, "user_a").await; -// // let a_id = crate::db::UserId(client_a.user_id().unwrap() as i32); -// // let db = server._test_db.db(); - -// // let zed_id = db.create_root_channel("zed", a_id).await.unwrap(); -// // let first_id = db.create_channel("first", Some(zed_id)).await.unwrap(); -// // let second_id = db -// // .create_channel("second_id", Some(first_id)) -// // .await -// // .unwrap(); -// } - -/* -Linear things: -- A way of expressing progress to the team -- A way for us to agree on a scope -- A way to figure out what we're supposed to be doing - -*/ diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c3d65343d6..d71ddeed83 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -295,7 +295,7 @@ request_messages!( (RemoveContact, Ack), (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), - (JoinChannel, CreateRoomResponse), + (JoinChannel, JoinRoomResponse), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), From a33d8519f230fe903c05fa66604d1c56382dfa9d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 1 Aug 2023 02:06:40 +0200 Subject: [PATCH 034/326] Move buttons inside of query editor (WIP) --- assets/icons/word_search_14.svg | 6 ++ crates/search/src/project_search.rs | 116 +++++++++++++++++++--------- styles/src/style_tree/search.ts | 12 +-- 3 files changed, 90 insertions(+), 44 deletions(-) create mode 100644 assets/icons/word_search_14.svg diff --git a/assets/icons/word_search_14.svg b/assets/icons/word_search_14.svg new file mode 100644 index 0000000000..adb4976bcc --- /dev/null +++ b/assets/icons/word_search_14.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1951e2c086..35e953a592 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1149,6 +1149,38 @@ impl ProjectSearchBar { ) .into_any() } + fn render_option_button_icon( + &self, + icon: &'static str, + option: SearchOptions, + cx: &mut ViewContext, + ) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + let is_active = self.is_option_enabled(option, cx); + MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); + Svg::new(icon).with_color(style.text.color.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_search_option(option, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + option.bits as usize, + format!("Toggle {}", option.label()), + Some(option.to_toggle_action()), + tooltip_style, + cx, + ) + .into_any() + } fn render_option_button( &self, @@ -1272,8 +1304,8 @@ impl View for ProjectSearchBar { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); + if let Some(_search) = self.active_project_search.as_ref() { + let search = _search.read(cx); let theme = theme::current(cx).clone(); let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { theme.search.invalid_editor @@ -1301,12 +1333,53 @@ impl View for ProjectSearchBar { .aligned() .right() .flex(1.0, true); - + let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); let row_spacing = theme.workspace.toolbar.container.padding.bottom; - let query = ChildView::new(&search.query_editor, cx) +let search = _search.read(cx); + let filter_button = { + let tooltip_style = theme::current(cx).tooltip.clone(); + let is_active = search.filters_enabled; + MouseEventHandler::::new(0, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); + Svg::new("icons/filter_12.svg") + .with_color(style.text.color.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_filters(cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::(0, "Toggle filters".into(), None, tooltip_style, cx) + .into_any() + }; let search = _search.read(cx); + let query = Flex::row() + .with_child( + ChildView::new(&search.query_editor, cx) + .constrained() + .flex(1., true) + .into_any(), + ) + .with_child( + Flex::row() + .with_children([ + filter_button, + self.render_option_button("Case", SearchOptions::CASE_SENSITIVE, cx), + self.render_option_button_icon("icons/word_search_14.svg", SearchOptions::WHOLE_WORD, cx), + + ]) + .flex(1., true) + .contained(), + ) .aligned() .left() .flex(1., true); + let search = _search.read(cx); let matches = search.active_match_index.map(|match_ix| { Label::new( format!( @@ -1321,6 +1394,7 @@ impl View for ProjectSearchBar { .aligned() .left() }); + let filters = search.filters_enabled.then(|| { Flex::row() .with_child( @@ -1346,30 +1420,6 @@ impl View for ProjectSearchBar { .flex(1., false), ) }); - let filter_button = { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = search.filters_enabled; - MouseEventHandler::::new(0, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Label::new("Filter", style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_filters(cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(0, "Toggle filters".into(), None, tooltip_style, cx) - .into_any() - }; - let case_button = self.render_option_button("Case", SearchOptions::CASE_SENSITIVE, cx); - let word_button = self.render_option_button("Word", SearchOptions::WHOLE_WORD, cx); - let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); let semantic_index = SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); @@ -1409,16 +1459,6 @@ impl View for ProjectSearchBar { )) .aligned(), ) - .with_child( - Flex::row() - .with_child(case_button) - .with_child(word_button) - .with_child(filter_button) - .contained() - .with_style(theme.search.option_button_group) - .aligned() - .right(), - ) .contained() .with_margin_bottom(row_spacing), ) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 28940a8367..43689c01bc 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -44,17 +44,17 @@ export default function search(): any { base: { ...text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - corner_radius: 6, + corner_radius: 2, border: border(theme.highest, "on"), margin: { right: 4, }, padding: { - bottom: 2, - left: 10, - right: 10, - top: 2, - }, + bottom: 6, + left: 6, + right: 6, + top: 6, + }, }, state: { hovered: { From 7954b02819bcdfb3573624394f53979f04d0879d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 31 Jul 2023 18:00:14 -0700 Subject: [PATCH 035/326] Start work on displaying channels and invites in collab panel --- crates/client/src/channel_store.rs | 127 +++++++------ crates/client/src/channel_store_tests.rs | 95 ++++++++++ crates/client/src/client.rs | 3 + crates/collab/src/tests.rs | 1 + crates/collab/src/tests/channel_tests.rs | 15 +- crates/collab_ui/src/panel.rs | 215 ++++++++++++++++++++++- crates/workspace/src/workspace.rs | 19 +- crates/zed/src/main.rs | 7 +- 8 files changed, 412 insertions(+), 70 deletions(-) create mode 100644 crates/client/src/channel_store_tests.rs diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index e78dafe4e8..678e712c7d 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -6,18 +6,19 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; pub struct ChannelStore { - channels: Vec, - channel_invitations: Vec, + channels: Vec>, + channel_invitations: Vec>, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Channel { pub id: u64, pub name: String, pub parent_id: Option, + pub depth: usize, } impl Entity for ChannelStore { @@ -41,11 +42,11 @@ impl ChannelStore { } } - pub fn channels(&self) -> &[Channel] { + pub fn channels(&self) -> &[Arc] { &self.channels } - pub fn channel_invitations(&self) -> &[Channel] { + pub fn channel_invitations(&self) -> &[Arc] { &self.channel_invitations } @@ -97,6 +98,10 @@ impl ChannelStore { } } + pub fn is_channel_invite_pending(&self, channel: &Arc) -> bool { + false + } + pub fn remove_member( &self, channel_id: u64, @@ -124,66 +129,74 @@ impl ChannelStore { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - let payload = message.payload; this.update(&mut cx, |this, cx| { - this.channels - .retain(|channel| !payload.remove_channels.contains(&channel.id)); - this.channel_invitations - .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); + this.update_channels(message.payload, cx); + }); + Ok(()) + } - for channel in payload.channel_invitations { - if let Some(existing_channel) = this - .channel_invitations - .iter_mut() - .find(|c| c.id == channel.id) - { - existing_channel.name = channel.name; - continue; + pub(crate) fn update_channels( + &mut self, + payload: proto::UpdateChannels, + cx: &mut ModelContext, + ) { + self.channels + .retain(|channel| !payload.remove_channels.contains(&channel.id)); + self.channel_invitations + .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); + + for channel in payload.channel_invitations { + if let Some(existing_channel) = self + .channel_invitations + .iter_mut() + .find(|c| c.id == channel.id) + { + Arc::make_mut(existing_channel).name = channel.name; + continue; + } + + self.channel_invitations.insert( + 0, + Arc::new(Channel { + id: channel.id, + name: channel.name, + parent_id: None, + depth: 0, + }), + ); + } + + for channel in payload.channels { + if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { + Arc::make_mut(existing_channel).name = channel.name; + continue; + } + + if let Some(parent_id) = channel.parent_id { + if let Some(ix) = self.channels.iter().position(|c| c.id == parent_id) { + let depth = self.channels[ix].depth + 1; + self.channels.insert( + ix + 1, + Arc::new(Channel { + id: channel.id, + name: channel.name, + parent_id: Some(parent_id), + depth, + }), + ); } - - this.channel_invitations.insert( + } else { + self.channels.insert( 0, - Channel { + Arc::new(Channel { id: channel.id, name: channel.name, parent_id: None, - }, + depth: 0, + }), ); } - - for channel in payload.channels { - if let Some(existing_channel) = - this.channels.iter_mut().find(|c| c.id == channel.id) - { - existing_channel.name = channel.name; - continue; - } - - if let Some(parent_id) = channel.parent_id { - if let Some(ix) = this.channels.iter().position(|c| c.id == parent_id) { - this.channels.insert( - ix + 1, - Channel { - id: channel.id, - name: channel.name, - parent_id: Some(parent_id), - }, - ); - } - } else { - this.channels.insert( - 0, - Channel { - id: channel.id, - name: channel.name, - parent_id: None, - }, - ); - } - } - cx.notify(); - }); - - Ok(()) + } + cx.notify(); } } diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs new file mode 100644 index 0000000000..0d4ec6ce35 --- /dev/null +++ b/crates/client/src/channel_store_tests.rs @@ -0,0 +1,95 @@ +use util::http::FakeHttpClient; + +use super::*; + +#[gpui::test] +fn test_update_channels(cx: &mut AppContext) { + let http = FakeHttpClient::with_404_response(); + let client = Client::new(http.clone(), cx); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + + let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + + update_channels( + &channel_store, + proto::UpdateChannels { + channels: vec![ + proto::Channel { + id: 1, + name: "b".to_string(), + parent_id: None, + }, + proto::Channel { + id: 2, + name: "a".to_string(), + parent_id: None, + }, + ], + ..Default::default() + }, + cx, + ); + assert_channels( + &channel_store, + &[ + // + (0, "a"), + (0, "b"), + ], + cx, + ); + + update_channels( + &channel_store, + proto::UpdateChannels { + channels: vec![ + proto::Channel { + id: 3, + name: "x".to_string(), + parent_id: Some(1), + }, + proto::Channel { + id: 4, + name: "y".to_string(), + parent_id: Some(2), + }, + ], + ..Default::default() + }, + cx, + ); + assert_channels( + &channel_store, + &[ + // + (0, "a"), + (1, "y"), + (0, "b"), + (1, "x"), + ], + cx, + ); +} + +fn update_channels( + channel_store: &ModelHandle, + message: proto::UpdateChannels, + cx: &mut AppContext, +) { + channel_store.update(cx, |store, cx| store.update_channels(message, cx)); +} + +fn assert_channels( + channel_store: &ModelHandle, + expected_channels: &[(usize, &str)], + cx: &AppContext, +) { + channel_store.read_with(cx, |store, _| { + let actual = store + .channels() + .iter() + .map(|c| (c.depth, c.name.as_str())) + .collect::>(); + assert_eq!(actual, expected_channels); + }); +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index af33c738ce..a48b2849ae 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,6 +1,9 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; +#[cfg(test)] +mod channel_store_tests; + pub mod channel_store; pub mod telemetry; pub mod user; diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index a000fbd92e..98ad2afb8a 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -193,6 +193,7 @@ impl TestServer { let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), + channel_store: channel_store.clone(), languages: Arc::new(LanguageRegistry::test()), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 632bfdca49..ffd517f52a 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -29,11 +29,12 @@ async fn test_basic_channels( client_a.channel_store.read_with(cx_a, |channels, _| { assert_eq!( channels.channels(), - &[Channel { + &[Arc::new(Channel { id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - }] + depth: 0, + })] ) }); @@ -56,11 +57,12 @@ async fn test_basic_channels( client_b.channel_store.read_with(cx_b, |channels, _| { assert_eq!( channels.channel_invitations(), - &[Channel { + &[Arc::new(Channel { id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - }] + depth: 0, + })] ) }); @@ -76,11 +78,12 @@ async fn test_basic_channels( assert_eq!(channels.channel_invitations(), &[]); assert_eq!( channels.channels(), - &[Channel { + &[Arc::new(Channel { id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - }] + depth: 0, + })] ) }); } diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bdeac59af9..bdd01e4299 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -4,7 +4,7 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{proto::PeerId, Client, Contact, User, UserStore}; +use client::{proto::PeerId, Channel, ChannelStore, Client, Contact, User, UserStore}; use contact_finder::build_contact_finder; use context_menu::ContextMenu; use db::kvp::KEY_VALUE_STORE; @@ -62,6 +62,7 @@ pub struct CollabPanel { entries: Vec, selection: Option, user_store: ModelHandle, + channel_store: ModelHandle, project: ModelHandle, match_candidates: Vec, list_state: ListState, @@ -109,8 +110,10 @@ enum ContactEntry { peer_id: PeerId, is_last: bool, }, + ChannelInvite(Arc), IncomingRequest(Arc), OutgoingRequest(Arc), + Channel(Arc), Contact { contact: Arc, calling: bool, @@ -204,6 +207,16 @@ impl CollabPanel { cx, ) } + ContactEntry::Channel(channel) => { + Self::render_channel(&*channel, &theme.collab_panel, is_selected, cx) + } + ContactEntry::ChannelInvite(channel) => Self::render_channel_invite( + channel.clone(), + this.channel_store.clone(), + &theme.collab_panel, + is_selected, + cx, + ), ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), @@ -241,6 +254,7 @@ impl CollabPanel { entries: Vec::default(), selection: None, user_store: workspace.user_store().clone(), + channel_store: workspace.app_state().channel_store.clone(), project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), @@ -320,6 +334,7 @@ impl CollabPanel { } fn update_entries(&mut self, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); let query = self.filter_editor.read(cx).text(cx); let executor = cx.background().clone(); @@ -445,10 +460,65 @@ impl CollabPanel { self.entries .push(ContactEntry::Header(Section::Channels, 0)); + let channels = channel_store.channels(); + if !channels.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + channels + .iter() + .enumerate() + .map(|(ix, channel)| StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + self.entries.extend( + matches + .iter() + .map(|mat| ContactEntry::Channel(channels[mat.candidate_id].clone())), + ); + } + self.entries .push(ContactEntry::Header(Section::Contacts, 0)); let mut request_entries = Vec::new(); + let channel_invites = channel_store.channel_invitations(); + if !channel_invites.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { + StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + } + })); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches.iter().map(|mat| { + ContactEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) + }), + ); + } + let incoming = user_store.incoming_contact_requests(); if !incoming.is_empty() { self.match_candidates.clear(); @@ -1112,6 +1182,121 @@ impl CollabPanel { event_handler.into_any() } + fn render_channel( + channel: &Channel, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + let channel_id = channel.id; + MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { + Flex::row() + .with_child({ + Svg::new("icons/hash") + // .with_style(theme.contact_avatar) + .aligned() + .left() + }) + .with_child( + Label::new(channel.name.clone(), theme.contact_username.text.clone()) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.join_channel(channel_id, cx); + }) + .into_any() + } + + fn render_channel_invite( + channel: Arc, + user_store: ModelHandle, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum Decline {} + enum Accept {} + + let channel_id = channel.id; + let is_invite_pending = user_store.read(cx).is_channel_invite_pending(&channel); + let button_spacing = theme.contact_button_spacing; + + Flex::row() + .with_child({ + Svg::new("icons/hash") + // .with_style(theme.contact_avatar) + .aligned() + .left() + }) + .with_child( + Label::new(channel.name.clone(), theme.contact_username.text.clone()) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::::new( + channel.id as usize, + cx, + |mouse_state, _| { + let button_style = if is_invite_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state) + }; + render_icon_button(button_style, "icons/x_mark_8.svg").aligned() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.respond_to_channel_invite(channel_id, false, cx); + }) + .contained() + .with_margin_right(button_spacing), + ) + .with_child( + MouseEventHandler::::new( + channel.id as usize, + cx, + |mouse_state, _| { + let button_style = if is_invite_pending { + &theme.disabled_button + } else { + theme.contact_button.style_for(mouse_state) + }; + render_icon_button(button_style, "icons/check_8.svg") + .aligned() + .flex_float() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.respond_to_channel_invite(channel_id, true, cx); + }), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()), + ) + .into_any() + } + fn render_contact_request( user: Arc, user_store: ModelHandle, @@ -1384,6 +1569,18 @@ impl CollabPanel { .detach(); } + fn respond_to_channel_invite( + &mut self, + channel_id: u64, + 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(); + } + fn call( &mut self, recipient_user_id: u64, @@ -1396,6 +1593,12 @@ impl CollabPanel { }) .detach_and_log_err(cx); } + + fn join_channel(&self, channel: u64, cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| call.join_channel(channel, cx)) + .detach_and_log_err(cx); + } } impl View for CollabPanel { @@ -1557,6 +1760,16 @@ impl PartialEq for ContactEntry { return peer_id_1 == peer_id_2; } } + ContactEntry::Channel(channel_1) => { + if let ContactEntry::Channel(channel_2) = other { + return channel_1.id == channel_2.id; + } + } + ContactEntry::ChannelInvite(channel_1) => { + if let ContactEntry::ChannelInvite(channel_2) = other { + return channel_1.id == channel_2.id; + } + } ContactEntry::IncomingRequest(user_1) => { if let ContactEntry::IncomingRequest(user_2) = other { return user_1.id == user_2.id; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 434975216a..95077649a8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -14,7 +14,7 @@ use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + ChannelStore, Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -400,8 +400,9 @@ pub fn register_deserializable_item(cx: &mut AppContext) { pub struct AppState { pub languages: Arc, - pub client: Arc, - pub user_store: ModelHandle, + pub client: Arc, + pub user_store: ModelHandle, + pub channel_store: ModelHandle, pub fs: Arc, pub build_window_options: fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, @@ -424,6 +425,8 @@ impl AppState { let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); theme::init((), cx); client::init(&client, cx); @@ -434,6 +437,7 @@ impl AppState { fs, languages, user_store, + channel_store, initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), background_actions: || &[], @@ -3406,10 +3410,15 @@ impl Workspace { #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { + let client = project.read(cx).client(); + let user_store = project.read(cx).user_store(); + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), - client: project.read(cx).client(), - user_store: project.read(cx).user_store(), + client, + user_store, + channel_store, fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e44ab3e33a..34c1232712 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -7,7 +7,9 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; +use client::{ + self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, +}; use db::kvp::KEY_VALUE_STORE; use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ @@ -140,6 +142,8 @@ fn main() { languages::init(languages.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); + let channel_store = + cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); cx.set_global(client.clone()); @@ -181,6 +185,7 @@ fn main() { languages, client: client.clone(), user_store, + channel_store, fs, build_window_options, initialize_workspace, From b29a535f0451108b16846d77ad69f484c2003c1e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:20:23 +0200 Subject: [PATCH 036/326] Use icons instead of text in toggles --- assets/icons/filter_14.svg | 6 ++++++ assets/icons/word_search_12.svg | 8 ++++++++ crates/search/src/project_search.rs | 29 +++++++++++++++++++++++------ 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 assets/icons/filter_14.svg create mode 100644 assets/icons/word_search_12.svg diff --git a/assets/icons/filter_14.svg b/assets/icons/filter_14.svg new file mode 100644 index 0000000000..379be15b51 --- /dev/null +++ b/assets/icons/filter_14.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/word_search_12.svg b/assets/icons/word_search_12.svg new file mode 100644 index 0000000000..4cf6401fd2 --- /dev/null +++ b/assets/icons/word_search_12.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 35e953a592..443ccbd567 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1164,9 +1164,11 @@ impl ProjectSearchBar { .option_button .in_state(is_active) .style_for(state); - Svg::new(icon).with_color(style.text.color.clone()) + Svg::new(icon) + .with_color(style.text.color.clone()) .contained() .with_style(style.container) + .constrained() }) .on_click(MouseButton::Left, move |_, this, cx| { this.toggle_search_option(option, cx); @@ -1335,7 +1337,7 @@ impl View for ProjectSearchBar { .flex(1.0, true); let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); let row_spacing = theme.workspace.toolbar.container.padding.bottom; -let search = _search.read(cx); + let search = _search.read(cx); let filter_button = { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = search.filters_enabled; @@ -1357,8 +1359,15 @@ let search = _search.read(cx); .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::(0, "Toggle filters".into(), None, tooltip_style, cx) .into_any() - }; let search = _search.read(cx); + }; + let search = _search.read(cx); let query = Flex::row() + .with_child( + Svg::new("icons/magnifying_glass_12.svg").with_color(gpui::color::Color::white()) + //.with_color(tab_theme.label.text.color) + .contained().constrained() + //.with_margin_right(tab_theme.spacing), + ) .with_child( ChildView::new(&search.query_editor, cx) .constrained() @@ -1369,13 +1378,21 @@ let search = _search.read(cx); Flex::row() .with_children([ filter_button, - self.render_option_button("Case", SearchOptions::CASE_SENSITIVE, cx), - self.render_option_button_icon("icons/word_search_14.svg", SearchOptions::WHOLE_WORD, cx), - + self.render_option_button_icon( + "icons/word_search_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + ), + self.render_option_button_icon( + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + ), ]) .flex(1., true) .contained(), ) + .align_children_center() .aligned() .left() .flex(1., true); From 2e2d0a3884e16846329a2b31db11e2f5aca5e5ab Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:07:21 +0200 Subject: [PATCH 037/326] Hide whole word/case sensitive options under semantic search --- crates/search/src/project_search.rs | 46 +++++++++++++++++++---------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 443ccbd567..497085f75e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1360,13 +1360,37 @@ impl View for ProjectSearchBar { .with_tooltip::(0, "Toggle filters".into(), None, tooltip_style, cx) .into_any() }; + let search = _search.read(cx); + let is_semantic_disabled = search.semantic.is_none(); + + let case_sensitive = if is_semantic_disabled { + Some(self.render_option_button_icon( + "icons/word_search_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + )) + } else { + None + }; + + let whole_word = if is_semantic_disabled { + Some(self.render_option_button_icon( + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + )) + } else { + None + }; + let search = _search.read(cx); let query = Flex::row() .with_child( - Svg::new("icons/magnifying_glass_12.svg").with_color(gpui::color::Color::white()) + Svg::new("icons/magnifying_glass_12.svg") + .with_color(gpui::color::Color::white()) //.with_color(tab_theme.label.text.color) - .contained().constrained() - //.with_margin_right(tab_theme.spacing), + .contained() + .constrained(), //.with_margin_right(tab_theme.spacing), ) .with_child( ChildView::new(&search.query_editor, cx) @@ -1376,19 +1400,9 @@ impl View for ProjectSearchBar { ) .with_child( Flex::row() - .with_children([ - filter_button, - self.render_option_button_icon( - "icons/word_search_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - ), - self.render_option_button_icon( - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - ), - ]) + .with_child(filter_button) + .with_children(whole_word) + .with_children(case_sensitive) .flex(1., true) .contained(), ) From c28ba3a11ac34af1b1074813a7853f29f3e3dfb4 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 1 Aug 2023 11:47:30 -0400 Subject: [PATCH 038/326] add cycle mode to project search Co-authored-by: Piotrek --- crates/search/src/project_search.rs | 54 +++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 497085f75e..546bbeea69 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -40,7 +40,13 @@ use workspace::{ actions!( project_search, - [SearchInNew, ToggleFocus, NextField, ToggleSemanticSearch] + [ + SearchInNew, + ToggleFocus, + NextField, + ToggleSemanticSearch, + CycleMode + ] ); #[derive(Default)] @@ -54,6 +60,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_prev_match); + cx.add_action(ProjectSearchBar::cycle_mode); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); @@ -103,6 +110,7 @@ pub struct ProjectSearchView { included_files_editor: ViewHandle, excluded_files_editor: ViewHandle, filters_enabled: bool, + current_mode: SearchMode, } struct SemanticSearchState { @@ -111,6 +119,15 @@ struct SemanticSearchState { _progress_task: Task<()>, } +// TODO: Update the default search mode to get from config +#[derive(Clone, Default, PartialEq)] +enum SearchMode { + #[default] + Text, + Semantic, + Regex, +} + pub struct ProjectSearchBar { active_project_search: Option>, subscription: Option, @@ -596,6 +613,7 @@ impl ProjectSearchView { included_files_editor, excluded_files_editor, filters_enabled, + current_mode: Default::default(), }; this.model_changed(cx); this @@ -910,7 +928,28 @@ impl ProjectSearchBar { subscription: Default::default(), } } + fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |this, cx| { + let mode = &this.current_mode; + let next_text_state = if SemanticIndex::enabled(cx) { + SearchMode::Semantic + } else { + SearchMode::Regex + }; + this.current_mode = match mode { + &SearchMode::Text => next_text_state, + &SearchMode::Semantic => SearchMode::Regex, + SearchMode::Regex => SearchMode::Text, + }; + cx.notify(); + }) + } + } fn search(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| search_view.search(cx)); @@ -1184,14 +1223,15 @@ impl ProjectSearchBar { .into_any() } - fn render_option_button( + fn render_regex_button( &self, icon: &'static str, - option: SearchOptions, + current_mode: SearchMode, cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.is_option_enabled(option, cx); + let is_active = current_mode == SearchMode::Regex; + let option = SearchOptions::REGEX; MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme @@ -1221,7 +1261,7 @@ impl ProjectSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = if let Some(search) = self.active_project_search.as_ref() { let search = search.read(cx); - search.semantic.is_some() + search.current_mode == SearchMode::Semantic } else { false }; @@ -1256,7 +1296,7 @@ impl ProjectSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = if let Some(search) = self.active_project_search.as_ref() { let search = search.read(cx); - search.semantic.is_none() && !self.is_option_enabled(SearchOptions::REGEX, cx) + search.current_mode == SearchMode::Text } else { false }; @@ -1335,7 +1375,7 @@ impl View for ProjectSearchBar { .aligned() .right() .flex(1.0, true); - let regex_button = self.render_option_button("Regex", SearchOptions::REGEX, cx); + let regex_button = self.render_regex_button("Regex", search.current_mode.clone(), cx); let row_spacing = theme.workspace.toolbar.container.padding.bottom; let search = _search.read(cx); let filter_button = { From 444b98e32fc5408770bd02684dfb21a603e5d9e8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:56:09 +0200 Subject: [PATCH 039/326] Update case insensitive button. Co-authored-by: Kyle --- assets/icons/case_insensitive_14.svg | 6 ++++++ crates/search/src/project_search.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 assets/icons/case_insensitive_14.svg diff --git a/assets/icons/case_insensitive_14.svg b/assets/icons/case_insensitive_14.svg new file mode 100644 index 0000000000..79698872bc --- /dev/null +++ b/assets/icons/case_insensitive_14.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 546bbeea69..992f9b143b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1405,7 +1405,7 @@ impl View for ProjectSearchBar { let case_sensitive = if is_semantic_disabled { Some(self.render_option_button_icon( - "icons/word_search_12.svg", + "icons/case_insensitive_14.svg", SearchOptions::CASE_SENSITIVE, cx, )) From 00a9672eca595ee1fbf9578c43fd29b56edabe49 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:02:17 +0200 Subject: [PATCH 040/326] Scale down the case insensitive icon Co-authored-by: Kyle --- assets/icons/case_insensitive_12.svg | 8 ++++++++ assets/icons/case_insensitive_14.svg | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 assets/icons/case_insensitive_12.svg delete mode 100644 assets/icons/case_insensitive_14.svg diff --git a/assets/icons/case_insensitive_12.svg b/assets/icons/case_insensitive_12.svg new file mode 100644 index 0000000000..8c943e7509 --- /dev/null +++ b/assets/icons/case_insensitive_12.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/case_insensitive_14.svg b/assets/icons/case_insensitive_14.svg deleted file mode 100644 index 79698872bc..0000000000 --- a/assets/icons/case_insensitive_14.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - From 7d5ff60ff01c846adfaf98027cf03a9a898447c3 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 1 Aug 2023 12:34:02 -0400 Subject: [PATCH 041/326] added svg right margin in search bar Co-authored-by: Piotr --- crates/search/src/project_search.rs | 13 +++++++------ crates/theme/src/theme.rs | 1 + styles/src/style_tree/search.ts | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 992f9b143b..7a4f1ad1a7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1230,7 +1230,7 @@ impl ProjectSearchBar { cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = current_mode == SearchMode::Regex; + let is_active = current_mode == SearchMode::Regex; //self.is_option_enabled(option, cx); let option = SearchOptions::REGEX; MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); @@ -1405,7 +1405,7 @@ impl View for ProjectSearchBar { let case_sensitive = if is_semantic_disabled { Some(self.render_option_button_icon( - "icons/case_insensitive_14.svg", + "icons/case_insensitive_12.svg", SearchOptions::CASE_SENSITIVE, cx, )) @@ -1424,13 +1424,14 @@ impl View for ProjectSearchBar { }; let search = _search.read(cx); + let icon_style = theme.search.editor_icon.clone(); + // " let query = Flex::row() .with_child( - Svg::new("icons/magnifying_glass_12.svg") - .with_color(gpui::color::Color::white()) - //.with_color(tab_theme.label.text.color) + Svg::for_style(icon_style.icon) .contained() - .constrained(), //.with_margin_right(tab_theme.spacing), + .with_style(icon_style.container) + .constrained(), ) .with_child( ChildView::new(&search.query_editor, cx) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4766f636f3..db6785c6fd 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -385,6 +385,7 @@ pub struct Search { pub match_index: ContainedText, pub results_status: TextStyle, pub dismiss_button: Interactive, + pub editor_icon: IconStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 43689c01bc..c2357f53b6 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -54,7 +54,7 @@ export default function search(): any { left: 6, right: 6, top: 6, - }, + }, }, state: { hovered: { @@ -163,5 +163,18 @@ export default function search(): any { }, }, }), + editor_icon: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/magnifying_glass_12.svg", + dimensions: { + width: 12, + height: 12, + } + }, + container: { + padding: { right: 6 } + } + } } } From cf060f0011464c3d071f41faf5e0a7ab62de5d33 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 1 Aug 2023 13:28:21 -0400 Subject: [PATCH 042/326] added major and minor display text to project search, and fixed icon padding Co-authored-by: Piotr --- crates/search/src/project_search.rs | 73 ++++++++++++++++++++++++----- crates/theme/src/theme.rs | 3 +- styles/src/style_tree/search.ts | 8 +++- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7a4f1ad1a7..44586b6102 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -120,7 +120,7 @@ struct SemanticSearchState { } // TODO: Update the default search mode to get from config -#[derive(Clone, Default, PartialEq)] +#[derive(Copy, Clone, Default, PartialEq)] enum SearchMode { #[default] Text, @@ -282,29 +282,67 @@ impl View for ProjectSearchView { enum Status {} let theme = theme::current(cx).clone(); - let text = if model.pending_search.is_some() { + + // If Search is Active -> Major: Searching..., Minor: None + // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...} + // If Regex -> Major: "Search using Regex", Minor: {ex...} + // If Text -> Major: "Text search all files and folders", Minor: {...} + + let current_mode = self.current_mode; + let major_text = if model.pending_search.is_some() { Cow::Borrowed("Searching...") - } else if let Some(semantic) = &self.semantic { + } else { + match current_mode { + SearchMode::Text => Cow::Borrowed("Text search all files and folders"), + SearchMode::Semantic => { + Cow::Borrowed("Search all files and folders using Natural Language") + } + SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), + } + }; + + let semantic_status = if let Some(semantic) = &self.semantic { if semantic.outstanding_file_count > 0 { - Cow::Owned(format!( - "Indexing. {} of {}...", + let dots_count = semantic.outstanding_file_count % 3 + 1; + let dots: String = std::iter::repeat('.').take(dots_count).collect(); + format!( + "Indexing. {} of {}{dots}", semantic.file_count - semantic.outstanding_file_count, semantic.file_count - )) + ) } else { - Cow::Borrowed("Indexing complete") + "Indexing complete".to_string() } - } else if self.query_editor.read(cx).text(cx).is_empty() { - Cow::Borrowed("Text search all files and folders") } else { - Cow::Borrowed("No results") + "This is an invalid state".to_string() + }; + + let minor_text = match current_mode { + SearchMode::Semantic => [ + semantic_status, + "ex. list all available languages".to_owned(), + ], + _ => [ + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ], }; MouseEventHandler::::new(0, cx, |_, _| { Flex::column() .with_child(Flex::column().contained().flex(1., true)) .with_child( - Label::new(text, theme.search.results_status.clone()) + Flex::column() + .align_children_center() + .with_child(Label::new( + major_text, + theme.search.major_results_status.clone(), + )) + .with_children( + minor_text.into_iter().map(|x| { + Label::new(x, theme.search.minor_results_status.clone()) + }), + ) .aligned() .top() .contained() @@ -1060,6 +1098,9 @@ impl ProjectSearchBar { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { search_view.search_options.toggle(option); + if option.contains(SearchOptions::REGEX) { + search_view.current_mode = SearchMode::Regex; + } search_view.semantic = None; search_view.search(cx); }); @@ -1096,6 +1137,7 @@ impl ProjectSearchBar { if search_view.semantic.is_some() { search_view.semantic = None; } else if let Some(semantic_index) = SemanticIndex::global(cx) { + search_view.current_mode = SearchMode::Semantic; // TODO: confirm that it's ok to send this project search_view.search_options = SearchOptions::none(); @@ -1315,7 +1357,13 @@ impl ProjectSearchBar { .with_style(style.container) }) .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_semantic_search(cx); + if let Some(search) = this.active_project_search.as_mut() { + search.update(cx, |this, cx| { + this.semantic = None; + this.current_mode = SearchMode::Text; + cx.notify(); + }); + } }) .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::( @@ -1425,7 +1473,6 @@ impl View for ProjectSearchBar { let search = _search.read(cx); let icon_style = theme.search.editor_icon.clone(); - // " let query = Flex::row() .with_child( Svg::for_style(icon_style.icon) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index db6785c6fd..b3e65d9fda 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -383,7 +383,8 @@ pub struct Search { pub action_button: Interactive, pub match_background: Color, pub match_index: ContainedText, - pub results_status: TextStyle, + pub major_results_status: TextStyle, + pub minor_results_status: TextStyle, pub dismiss_button: Interactive, pub editor_icon: IconStyle, } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c2357f53b6..9f72cfc424 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -140,9 +140,13 @@ export default function search(): any { right: 6, }, }, - results_status: { + major_results_status: { ...text(theme.highest, "mono", "on"), - size: 18, + size: 15, + }, + minor_results_status: { + ...text(theme.highest, "mono", "variant"), + size: 13, }, dismiss_button: interactive({ base: { From 7434d66fdd84ae250e973135f7ce946d1255d362 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 1 Aug 2023 13:22:06 -0700 Subject: [PATCH 043/326] WIP: Add channel creation to panel UI --- crates/client/src/channel_store.rs | 1 + crates/collab/src/db.rs | 38 +++++++ crates/collab/src/db/tests.rs | 90 ++++++++++++++++ crates/collab/src/rpc.rs | 34 +++++- crates/collab_ui/src/panel.rs | 168 ++++++++++++++++++++--------- script/zed-with-local-servers | 2 +- 6 files changed, 281 insertions(+), 52 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 678e712c7d..dfdb5fe9ed 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -33,6 +33,7 @@ impl ChannelStore { ) -> Self { let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_channels); + Self { channels: vec![], channel_invitations: vec![], diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f87b68c1ec..12e02b06ed 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3214,6 +3214,44 @@ impl Database { .await } + pub async fn get_channel_invites(&self, user_id: UserId) -> Result> { + self.transaction(|tx| async move { + let tx = tx; + + let channel_invites = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(false)), + ) + .all(&*tx) + .await?; + + let channels = channel::Entity::find() + .filter( + channel::Column::Id.is_in( + channel_invites + .into_iter() + .map(|channel_member| channel_member.channel_id), + ), + ) + .all(&*tx) + .await?; + + let channels = channels + .into_iter() + .map(|channel| Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }) + .collect(); + + Ok(channels) + }) + .await + } + pub async fn get_channels(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { let tx = tx; diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 719e8693d4..64ab03e02d 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1023,6 +1023,96 @@ test_both_dbs!( } ); +test_both_dbs!( + test_channel_invites_postgres, + test_channel_invites_sqlite, + db, + { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + 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 channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); + + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, false) + .await + .unwrap(); + + let user_2_invites = db + .get_channel_invites(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + + let user_3_invites = db + .get_channel_invites(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_3_invites, &[channel_1_1]) + } +); + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0abf2c44a7..6461f67c38 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -516,15 +516,19 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, invite_code) = future::try_join( + let (contacts, invite_code, channels, channel_invites) = future::try_join4( this.app_state.db.get_contacts(user_id), - this.app_state.db.get_invite_code_for_user(user_id) + this.app_state.db.get_invite_code_for_user(user_id), + this.app_state.db.get_channels(user_id), + this.app_state.db.get_channel_invites(user_id) ).await?; { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; + this.peer.send(connection_id, build_initial_channels_update(channels, channel_invites))?; + if let Some((code, count)) = invite_code { this.peer.send(connection_id, proto::UpdateInviteInfo { @@ -2097,6 +2101,7 @@ async fn create_channel( response: Response, session: Session, ) -> Result<()> { + dbg!(&request); let db = session.db().await; let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); @@ -2307,6 +2312,31 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { } } +fn build_initial_channels_update( + channels: Vec, + channel_invites: Vec, +) -> proto::UpdateChannels { + let mut update = proto::UpdateChannels::default(); + + for channel in channels { + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + }); + } + + for channel in channel_invites { + update.channel_invitations.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + }); + } + + update +} + fn build_initial_contacts_update( contacts: Vec, pool: &ConnectionPool, diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bdd01e4299..bfaa414a27 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -32,11 +32,10 @@ use theme::IconButton; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, + item::ItemHandle, Workspace, }; -use self::channel_modal::ChannelModal; - actions!(collab_panel, [ToggleFocus]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -52,6 +51,11 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::confirm); } +#[derive(Debug, Default)] +pub struct ChannelEditingState { + root_channel: bool, +} + pub struct CollabPanel { width: Option, fs: Arc, @@ -59,6 +63,8 @@ pub struct CollabPanel { pending_serialization: Task>, context_menu: ViewHandle, filter_editor: ViewHandle, + channel_name_editor: ViewHandle, + channel_editing_state: Option, entries: Vec, selection: Option, user_store: ModelHandle, @@ -93,7 +99,7 @@ enum Section { Offline, } -#[derive(Clone)] +#[derive(Clone, Debug)] enum ContactEntry { Header(Section, usize), CallParticipant { @@ -157,6 +163,23 @@ impl CollabPanel { }) .detach(); + let channel_name_editor = cx.add_view(|cx| { + Editor::single_line( + Some(Arc::new(|theme| { + theme.collab_panel.user_query_editor.clone() + })), + cx, + ) + }); + + cx.subscribe(&channel_name_editor, |this, _, event, cx| { + if let editor::Event::Blurred = event { + this.take_editing_state(cx); + cx.notify(); + } + }) + .detach(); + let list_state = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { let theme = theme::current(cx).clone(); @@ -166,7 +189,7 @@ impl CollabPanel { match &this.entries[ix] { ContactEntry::Header(section, depth) => { let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( + this.render_header( *section, &theme, *depth, @@ -250,8 +273,10 @@ impl CollabPanel { fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + channel_name_editor, filter_editor, entries: Vec::default(), + channel_editing_state: None, selection: None, user_store: workspace.user_store().clone(), channel_store: workspace.app_state().channel_store.clone(), @@ -333,6 +358,13 @@ impl CollabPanel { ); } + fn is_editing_root_channel(&self) -> bool { + self.channel_editing_state + .as_ref() + .map(|state| state.root_channel) + .unwrap_or(false) + } + fn update_entries(&mut self, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); @@ -944,7 +976,23 @@ impl CollabPanel { .into_any() } + fn take_editing_state( + &mut self, + cx: &mut ViewContext, + ) -> Option<(ChannelEditingState, String)> { + let result = self + .channel_editing_state + .take() + .map(|state| (state, self.channel_name_editor.read(cx).text(cx))); + + self.channel_name_editor + .update(cx, |editor, cx| editor.set_text("", cx)); + + result + } + fn render_header( + &self, section: Section, theme: &theme::Theme, depth: usize, @@ -1014,7 +1062,13 @@ impl CollabPanel { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, this, cx| { - this.toggle_channel_finder(cx); + if this.channel_editing_state.is_none() { + this.channel_editing_state = + Some(ChannelEditingState { root_channel: true }); + } + + cx.focus(this.channel_name_editor.as_any()); + cx.notify(); }) .with_tooltip::( 0, @@ -1027,6 +1081,13 @@ impl CollabPanel { _ => None, }; + let addition = match section { + Section::Channels if self.is_editing_root_channel() => { + Some(ChildView::new(self.channel_name_editor.as_any(), cx)) + } + _ => None, + }; + let can_collapse = depth > 0; let icon_size = (&theme.collab_panel).section_icon_size; MouseEventHandler::::new(section as usize, cx, |state, _| { @@ -1040,40 +1101,44 @@ impl CollabPanel { &theme.collab_panel.header_row }; - Flex::row() - .with_children(if can_collapse { - Some( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .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 - }) + Flex::column() .with_child( - Label::new(text, header_style.text.clone()) - .aligned() - .left() - .flex(1., true), + Flex::row() + .with_children(if can_collapse { + Some( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .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 + }) + .with_child( + Label::new(text, header_style.text.clone()) + .aligned() + .left() + .flex(1., true), + ) + .with_children(button.map(|button| button.aligned().right())) + .constrained() + .with_height(theme.collab_panel.row_height) + .contained() + .with_style(header_style.container), ) - .with_children(button.map(|button| button.aligned().right())) - .constrained() - .with_height(theme.collab_panel.row_height) - .contained() - .with_style(header_style.container) + .with_children(addition) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1189,7 +1254,7 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; - MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { + MouseEventHandler::::new(channel.id as usize, cx, |state, _cx| { Flex::row() .with_child({ Svg::new("icons/hash") @@ -1218,7 +1283,7 @@ impl CollabPanel { fn render_channel_invite( channel: Arc, - user_store: ModelHandle, + channel_store: ModelHandle, theme: &theme::CollabPanel, is_selected: bool, cx: &mut ViewContext, @@ -1227,7 +1292,7 @@ impl CollabPanel { enum Accept {} let channel_id = channel.id; - let is_invite_pending = user_store.read(cx).is_channel_invite_pending(&channel); + let is_invite_pending = channel_store.read(cx).is_channel_invite_pending(&channel); let button_spacing = theme.contact_button_spacing; Flex::row() @@ -1401,7 +1466,7 @@ impl CollabPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - let did_clear = self.filter_editor.update(cx, |editor, cx| { + let mut did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { editor.set_text("", cx); true @@ -1410,6 +1475,8 @@ impl CollabPanel { } }); + did_clear |= self.take_editing_state(cx).is_some(); + if !did_clear { cx.emit(Event::Dismissed); } @@ -1496,6 +1563,17 @@ impl CollabPanel { _ => {} } } + } else if let Some((_editing_state, channel_name)) = self.take_editing_state(cx) { + dbg!(&channel_name); + let create_channel = self.channel_store.update(cx, |channel_store, cx| { + channel_store.create_channel(&channel_name, None) + }); + + cx.foreground() + .spawn(async move { + dbg!(create_channel.await).ok(); + }) + .detach(); } } @@ -1522,14 +1600,6 @@ impl CollabPanel { } } - fn toggle_channel_finder(&mut self, cx: &mut ViewContext) { - if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| ChannelModal::new(cx))); - }); - } - } - fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( diff --git a/script/zed-with-local-servers b/script/zed-with-local-servers index f1de38adcf..c47b0e3de0 100755 --- a/script/zed-with-local-servers +++ b/script/zed-with-local-servers @@ -1,3 +1,3 @@ #!/bin/bash -ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:3000 cargo run $@ +ZED_ADMIN_API_TOKEN=secret ZED_IMPERSONATE=as-cii ZED_SERVER_URL=http://localhost:8080 cargo run $@ From 56d4d5d1a8c8fc42cb678f8b618e47364049760f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 1 Aug 2023 13:33:31 -0700 Subject: [PATCH 044/326] Add root channel UI co-authored-by: Max --- crates/collab_ui/src/panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bfaa414a27..53f7eee79a 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -1257,7 +1257,7 @@ impl CollabPanel { MouseEventHandler::::new(channel.id as usize, cx, |state, _cx| { Flex::row() .with_child({ - Svg::new("icons/hash") + Svg::new("icons/file_icons/hash.svg") // .with_style(theme.contact_avatar) .aligned() .left() @@ -1297,7 +1297,7 @@ impl CollabPanel { Flex::row() .with_child({ - Svg::new("icons/hash") + Svg::new("icons/file_icons/hash.svg") // .with_style(theme.contact_avatar) .aligned() .left() From e4871afaf3e2b1ae8113d6e23bfbb8db9484d1dc Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 2 Aug 2023 00:36:46 +0200 Subject: [PATCH 045/326] Improve styling of mglass icon & search switches --- styles/src/style_tree/search.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 9f72cfc424..94b52aa664 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -22,8 +22,8 @@ export default function search(): any { padding: { top: 3, bottom: 3, - left: 12, - right: 8, + left: 10, + right: 4, }, } @@ -47,7 +47,7 @@ export default function search(): any { corner_radius: 2, border: border(theme.highest, "on"), margin: { - right: 4, + right: 2, }, padding: { bottom: 6, @@ -177,7 +177,8 @@ export default function search(): any { } }, container: { - padding: { right: 6 } + margin: { right: 6 }, + padding: { left: 4 } } } } From 74437b3988626aeb7cfef8d297aabe03a16d4a48 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 1 Aug 2023 16:06:21 -0700 Subject: [PATCH 046/326] Add remove channel method Move test client fields into appstate and fix tests Co-authored-by: max --- crates/client/src/channel_store.rs | 12 + crates/client/src/client.rs | 5 +- crates/collab/src/db.rs | 194 +++++++++++---- crates/collab/src/db/tests.rs | 24 ++ crates/collab/src/rpc.rs | 42 +++- crates/collab/src/tests.rs | 106 ++++---- crates/collab/src/tests/channel_tests.rs | 31 ++- crates/collab/src/tests/integration_tests.rs | 232 +++++++++--------- .../src/tests/randomized_integration_tests.rs | 66 ++--- crates/collab_ui/src/collab_ui.rs | 2 +- crates/collab_ui/src/panel.rs | 59 ++++- crates/collab_ui/src/panel/channel_modal.rs | 8 +- crates/rpc/proto/zed.proto | 6 + crates/rpc/src/proto.rs | 2 + crates/workspace/src/workspace.rs | 1 + 15 files changed, 534 insertions(+), 256 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index dfdb5fe9ed..99501bbd2a 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -51,6 +51,10 @@ impl ChannelStore { &self.channel_invitations } + pub fn channel_for_id(&self, channel_id: u64) -> Option> { + self.channels.iter().find(|c| c.id == channel_id).cloned() + } + pub fn create_channel( &self, name: &str, @@ -103,6 +107,14 @@ impl ChannelStore { false } + pub fn remove_channel(&self, channel_id: u64) -> impl Future> { + let client = self.client.clone(); + async move { + client.request(proto::RemoveChannel { channel_id }).await?; + Ok(()) + } + } + pub fn remove_member( &self, channel_id: u64, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index a48b2849ae..1e86cef4cc 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -575,7 +575,10 @@ impl Client { }), ); if prev_handler.is_some() { - panic!("registered handler for the same message twice"); + panic!( + "registered handler for the same message {} twice", + std::any::type_name::() + ); } Subscription::Message { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 12e02b06ed..066c93ec71 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -44,6 +44,7 @@ use serde::{Deserialize, Serialize}; pub use signup::{Invite, NewSignup, WaitlistSummary}; use sqlx::migrate::{Migrate, Migration, MigrationSource}; use sqlx::Connection; +use std::fmt::Write as _; use std::ops::{Deref, DerefMut}; use std::path::Path; use std::time::Duration; @@ -3131,6 +3132,74 @@ impl Database { .await } + pub async fn remove_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result<(Vec, Vec)> { + self.transaction(move |tx| async move { + let tx = tx; + + // Check if user is an admin + channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Admin.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not allowed to remove this channel"))?; + + let mut descendants = self.get_channel_descendants([channel_id], &*tx).await?; + + // Keep channels which have another active + let mut channels_to_keep = channel_parent::Entity::find() + .filter( + channel_parent::Column::ChildId + .is_in(descendants.keys().copied().filter(|&id| id != channel_id)) + .and( + channel_parent::Column::ParentId.is_not_in(descendants.keys().copied()), + ), + ) + .stream(&*tx) + .await?; + + while let Some(row) = channels_to_keep.next().await { + let row = row?; + descendants.remove(&row.child_id); + } + + drop(channels_to_keep); + + let channels_to_remove = descendants.keys().copied().collect::>(); + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } + + let members_to_notify: Vec = channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channels_to_remove.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .distinct() + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + // Channel members and parents should delete via cascade + channel::Entity::delete_many() + .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) + .exec(&*tx) + .await?; + + Ok((channels_to_remove, members_to_notify)) + }) + .await + } + pub async fn invite_channel_member( &self, channel_id: ChannelId, @@ -3256,50 +3325,32 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - // Breadth first list of all edges in this user's channels - let sql = r#" - WITH RECURSIVE channel_tree(child_id, parent_id, depth) AS ( - SELECT channel_id as child_id, CAST(NULL as INTEGER) as parent_id, 0 - FROM channel_members - WHERE user_id = $1 AND accepted - UNION - SELECT channel_parents.child_id, channel_parents.parent_id, channel_tree.depth + 1 - FROM channel_parents, channel_tree - WHERE channel_parents.parent_id = channel_tree.child_id - ) - SELECT channel_tree.child_id, channel_tree.parent_id - FROM channel_tree - ORDER BY child_id, parent_id IS NOT NULL - "#; - - #[derive(FromQueryResult, Debug, PartialEq)] - pub struct ChannelParent { - pub child_id: ChannelId, - pub parent_id: Option, + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, } - let stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - vec![user_id.into()], - ); + let starting_channel_ids: Vec = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .select_only() + .column(channel_member::Column::ChannelId) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; - let mut parents_by_child_id = HashMap::default(); - let mut parents = channel_parent::Entity::find() - .from_raw_sql(stmt) - .into_model::() - .stream(&*tx).await?; - while let Some(parent) = parents.next().await { - let parent = parent?; - parents_by_child_id.insert(parent.child_id, parent.parent_id); - } - - drop(parents); + let parents_by_child_id = self + .get_channel_descendants(starting_channel_ids, &*tx) + .await?; 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?; + .stream(&*tx) + .await?; while let Some(row) = rows.next().await { let row = row?; @@ -3317,18 +3368,73 @@ impl Database { .await } - pub async fn get_channel(&self, channel_id: ChannelId) -> Result { + 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#" + WITH RECURSIVE channel_tree(child_id, parent_id) AS ( + SELECT root_ids.column1 as child_id, CAST(NULL as INTEGER) as parent_id + FROM (VALUES {}) as root_ids + UNION + SELECT channel_parents.child_id, channel_parents.parent_id + FROM channel_parents, channel_tree + WHERE channel_parents.parent_id = channel_tree.child_id + ) + SELECT channel_tree.child_id, channel_tree.parent_id + FROM channel_tree + ORDER BY child_id, parent_id IS NOT NULL + "#, + values + ); + + #[derive(FromQueryResult, Debug, PartialEq)] + pub struct ChannelParent { + pub child_id: ChannelId, + pub parent_id: Option, + } + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + + let mut parents_by_child_id = HashMap::default(); + let mut parents = channel_parent::Entity::find() + .from_raw_sql(stmt) + .into_model::() + .stream(tx) + .await?; + + while let Some(parent) = parents.next().await { + let parent = parent?; + parents_by_child_id.insert(parent.child_id, parent.parent_id); + } + + Ok(parents_by_child_id) + } + + pub async fn get_channel(&self, channel_id: ChannelId) -> Result> { self.transaction(|tx| async move { let tx = tx; - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such channel"))?; - Ok(Channel { + let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + + Ok(channel.map(|channel| Channel { id: channel.id, name: channel.name, parent_id: None, - }) + })) }) .await } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 64ab03e02d..3a47097f7d 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -918,6 +918,11 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .await .unwrap(); + let cargo_ra_id = db + .create_channel("cargo-ra", Some(cargo_id), "7", a_id) + .await + .unwrap(); + let channels = db.get_channels(a_id).await.unwrap(); assert_eq!( @@ -952,9 +957,28 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { id: cargo_id, name: "cargo".to_string(), parent_id: Some(rust_id), + }, + Channel { + id: cargo_ra_id, + name: "cargo-ra".to_string(), + parent_id: Some(cargo_id), } ] ); + + // Remove a single channel + db.remove_channel(crdb_id, a_id).await.unwrap(); + assert!(db.get_channel(crdb_id).await.unwrap().is_none()); + + // Remove a channel tree + let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap(); + channel_ids.sort(); + assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); + assert_eq!(user_ids, &[a_id]); + + assert!(db.get_channel(rust_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_id).await.unwrap().is_none()); + assert!(db.get_channel(cargo_ra_id).await.unwrap().is_none()); }); test_both_dbs!( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6461f67c38..1465c66601 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -243,6 +243,7 @@ impl Server { .add_request_handler(remove_contact) .add_request_handler(respond_to_contact_request) .add_request_handler(create_channel) + .add_request_handler(remove_channel) .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) .add_request_handler(respond_to_channel_invite) @@ -529,7 +530,6 @@ impl Server { this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_channels_update(channels, channel_invites))?; - if let Some((code, count)) = invite_code { this.peer.send(connection_id, proto::UpdateInviteInfo { url: format!("{}{}", this.app_state.config.invite_link_prefix, code), @@ -2101,7 +2101,6 @@ async fn create_channel( response: Response, session: Session, ) -> Result<()> { - dbg!(&request); let db = session.db().await; let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); @@ -2132,6 +2131,35 @@ async fn create_channel( Ok(()) } +async fn remove_channel( + request: proto::RemoveChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + + let channel_id = request.channel_id; + let (removed_channels, member_ids) = db + .remove_channel(ChannelId::from_proto(channel_id), session.user_id) + .await?; + response.send(proto::Ack {})?; + + // Notify members of removed channels + let mut update = proto::UpdateChannels::default(); + update + .remove_channels + .extend(removed_channels.into_iter().map(|id| id.to_proto())); + + 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())?; + } + } + + Ok(()) +} + async fn invite_channel_member( request: proto::InviteChannelMember, response: Response, @@ -2139,7 +2167,10 @@ async fn invite_channel_member( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db.get_channel(channel_id).await?; + let channel = db + .get_channel(channel_id) + .await? + .ok_or_else(|| anyhow!("channel not found"))?; let invitee_id = UserId::from_proto(request.user_id); db.invite_channel_member(channel_id, invitee_id, session.user_id, false) .await?; @@ -2177,7 +2208,10 @@ async fn respond_to_channel_invite( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db.get_channel(channel_id).await?; + let channel = db + .get_channel(channel_id) + .await? + .ok_or_else(|| anyhow!("no such channel"))?; db.respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 98ad2afb8a..e0346dbe7f 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -14,8 +14,8 @@ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; use gpui::{ - elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View, - ViewContext, ViewHandle, WeakViewHandle, + elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, Task, TestAppContext, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use parking_lot::Mutex; @@ -197,7 +197,7 @@ impl TestServer { languages: Arc::new(LanguageRegistry::test()), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _, _| unimplemented!(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], }); @@ -218,13 +218,9 @@ impl TestServer { .unwrap(); let client = TestClient { - client, + app_state, username: name.to_string(), state: Default::default(), - user_store, - channel_store, - fs, - language_registry: Arc::new(LanguageRegistry::test()), }; client.wait_for_current_user(cx).await; client @@ -252,6 +248,7 @@ impl TestServer { let (client_a, cx_a) = left.last_mut().unwrap(); for (client_b, cx_b) in right { client_a + .app_state .user_store .update(*cx_a, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) @@ -260,6 +257,7 @@ impl TestServer { .unwrap(); cx_a.foreground().run_until_parked(); client_b + .app_state .user_store .update(*cx_b, |store, cx| { store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) @@ -278,6 +276,7 @@ impl TestServer { ) -> u64 { let (admin_client, admin_cx) = admin; let channel_id = admin_client + .app_state .channel_store .update(admin_cx, |channel_store, _| { channel_store.create_channel(channel, None) @@ -287,6 +286,7 @@ impl TestServer { for (member_client, member_cx) in members { admin_client + .app_state .channel_store .update(admin_cx, |channel_store, _| { channel_store.invite_member(channel_id, member_client.user_id().unwrap(), false) @@ -297,6 +297,7 @@ impl TestServer { admin_cx.foreground().run_until_parked(); member_client + .app_state .channel_store .update(*member_cx, |channels, _| { channels.respond_to_channel_invite(channel_id, true) @@ -359,13 +360,9 @@ impl Drop for TestServer { } struct TestClient { - client: Arc, username: String, state: RefCell, - pub user_store: ModelHandle, - pub channel_store: ModelHandle, - language_registry: Arc, - fs: Arc, + app_state: Arc, } #[derive(Default)] @@ -379,7 +376,7 @@ impl Deref for TestClient { type Target = Arc; fn deref(&self) -> &Self::Target { - &self.client + &self.app_state.client } } @@ -390,22 +387,45 @@ struct ContactsSummary { } impl TestClient { + pub fn fs(&self) -> &FakeFs { + self.app_state.fs.as_fake() + } + + pub fn channel_store(&self) -> &ModelHandle { + &self.app_state.channel_store + } + + pub fn user_store(&self) -> &ModelHandle { + &self.app_state.user_store + } + + pub fn language_registry(&self) -> &Arc { + &self.app_state.languages + } + + pub fn client(&self) -> &Arc { + &self.app_state.client + } + pub fn current_user_id(&self, cx: &TestAppContext) -> UserId { UserId::from_proto( - self.user_store + self.app_state + .user_store .read_with(cx, |user_store, _| user_store.current_user().unwrap().id), ) } async fn wait_for_current_user(&self, cx: &TestAppContext) { let mut authed_user = self + .app_state .user_store .read_with(cx, |user_store, _| user_store.watch_current_user()); while authed_user.next().await.unwrap().is_none() {} } async fn clear_contacts(&self, cx: &mut TestAppContext) { - self.user_store + self.app_state + .user_store .update(cx, |store, _| store.clear_contacts()) .await; } @@ -443,23 +463,25 @@ impl TestClient { } fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary { - self.user_store.read_with(cx, |store, _| ContactsSummary { - current: store - .contacts() - .iter() - .map(|contact| contact.user.github_login.clone()) - .collect(), - outgoing_requests: store - .outgoing_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - incoming_requests: store - .incoming_contact_requests() - .iter() - .map(|user| user.github_login.clone()) - .collect(), - }) + self.app_state + .user_store + .read_with(cx, |store, _| ContactsSummary { + current: store + .contacts() + .iter() + .map(|contact| contact.user.github_login.clone()) + .collect(), + outgoing_requests: store + .outgoing_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + incoming_requests: store + .incoming_contact_requests() + .iter() + .map(|user| user.github_login.clone()) + .collect(), + }) } async fn build_local_project( @@ -469,10 +491,10 @@ impl TestClient { ) -> (ModelHandle, WorktreeId) { let project = cx.update(|cx| { Project::local( - self.client.clone(), - self.user_store.clone(), - self.language_registry.clone(), - self.fs.clone(), + self.client().clone(), + self.app_state.user_store.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), cx, ) }); @@ -498,8 +520,8 @@ impl TestClient { room.update(guest_cx, |room, cx| { room.join_project( host_project_id, - self.language_registry.clone(), - self.fs.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), cx, ) }) @@ -541,7 +563,9 @@ impl TestClient { // We use a workspace container so that we don't need to remove the window in order to // drop the workspace and we can use a ViewHandle instead. let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None }); - let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx.add_view(window_id, |cx| { + Workspace::new(0, project.clone(), self.app_state.clone(), cx) + }); container.update(cx, |container, cx| { container.workspace = Some(workspace.downgrade()); cx.notify(); @@ -552,7 +576,7 @@ impl TestClient { impl Drop for TestClient { fn drop(&mut self) { - self.client.teardown(); + self.app_state.client.teardown(); } } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index ffd517f52a..14363b74cf 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -19,14 +19,14 @@ async fn test_basic_channels( let client_b = server.create_client(cx_b, "user_b").await; let channel_a_id = client_a - .channel_store + .channel_store() .update(cx_a, |channel_store, _| { channel_store.create_channel("channel-a", None) }) .await .unwrap(); - client_a.channel_store.read_with(cx_a, |channels, _| { + client_a.channel_store().read_with(cx_a, |channels, _| { assert_eq!( channels.channels(), &[Arc::new(Channel { @@ -39,12 +39,12 @@ async fn test_basic_channels( }); client_b - .channel_store + .channel_store() .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); // Invite client B to channel A as client A. client_a - .channel_store + .channel_store() .update(cx_a, |channel_store, _| { channel_store.invite_member(channel_a_id, client_b.user_id().unwrap(), false) }) @@ -54,7 +54,7 @@ async fn test_basic_channels( // Wait for client b to see the invitation deterministic.run_until_parked(); - client_b.channel_store.read_with(cx_b, |channels, _| { + client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!( channels.channel_invitations(), &[Arc::new(Channel { @@ -68,13 +68,13 @@ async fn test_basic_channels( // Client B now sees that they are in channel A. client_b - .channel_store + .channel_store() .update(cx_b, |channels, _| { channels.respond_to_channel_invite(channel_a_id, true) }) .await .unwrap(); - client_b.channel_store.read_with(cx_b, |channels, _| { + client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!(channels.channel_invitations(), &[]); assert_eq!( channels.channels(), @@ -86,6 +86,23 @@ async fn test_basic_channels( })] ) }); + + // Client A deletes the channel + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_a_id) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + client_a + .channel_store() + .read_with(cx_a, |channels, _| assert_eq!(channels.channels(), &[])); + client_b + .channel_store() + .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); } #[gpui::test] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5a27787dbc..93ebb812ad 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -749,7 +749,7 @@ async fn test_server_restarts( let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; client_a - .fs + .fs() .insert_tree("/a", json!({ "a.txt": "a-contents" })) .await; @@ -1221,7 +1221,7 @@ async fn test_share_project( let active_call_c = cx_c.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -1388,7 +1388,7 @@ async fn test_unshare_project( let active_call_b = cx_b.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -1477,7 +1477,7 @@ async fn test_host_disconnect( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -1500,7 +1500,7 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); let (window_id_b, workspace_b) = - cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, cx) @@ -1584,7 +1584,7 @@ async fn test_project_reconnect( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/root-1", json!({ @@ -1612,7 +1612,7 @@ async fn test_project_reconnect( ) .await; client_a - .fs + .fs() .insert_tree( "/root-2", json!({ @@ -1621,7 +1621,7 @@ async fn test_project_reconnect( ) .await; client_a - .fs + .fs() .insert_tree( "/root-3", json!({ @@ -1701,7 +1701,7 @@ async fn test_project_reconnect( // While client A is disconnected, add and remove files from client A's project. client_a - .fs + .fs() .insert_tree( "/root-1/dir1/subdir2", json!({ @@ -1713,7 +1713,7 @@ async fn test_project_reconnect( ) .await; client_a - .fs + .fs() .remove_dir( "/root-1/dir1/subdir1".as_ref(), RemoveOptions { @@ -1835,11 +1835,11 @@ async fn test_project_reconnect( // While client B is disconnected, add and remove files from client A's project client_a - .fs + .fs() .insert_file("/root-1/dir1/subdir2/j.txt", "j-contents".into()) .await; client_a - .fs + .fs() .remove_file("/root-1/dir1/subdir2/i.txt".as_ref(), Default::default()) .await .unwrap(); @@ -1925,8 +1925,8 @@ async fn test_active_call_events( 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.fs.insert_tree("/a", json!({})).await; - client_b.fs.insert_tree("/b", json!({})).await; + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_b, _) = client_b.build_local_project("/b", cx_b).await; @@ -2014,8 +2014,8 @@ async fn test_room_location( 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.fs.insert_tree("/a", json!({})).await; - client_b.fs.insert_tree("/b", json!({})).await; + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); @@ -2204,12 +2204,12 @@ async fn test_propagate_saves_and_fs_changes( Some(tree_sitter_rust::language()), )); for client in [&client_a, &client_b, &client_c] { - client.language_registry.add(rust.clone()); - client.language_registry.add(javascript.clone()); + client.language_registry().add(rust.clone()); + client.language_registry().add(javascript.clone()); } client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -2279,7 +2279,7 @@ async fn test_propagate_saves_and_fs_changes( buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx)); save_b.await.unwrap(); assert_eq!( - client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(), + client_a.fs().load("/a/file1.rs".as_ref()).await.unwrap(), "hi-a, i-am-c, i-am-b, i-am-a" ); @@ -2290,7 +2290,7 @@ async fn test_propagate_saves_and_fs_changes( // Make changes on host's file system, see those changes on guest worktrees. client_a - .fs + .fs() .rename( "/a/file1.rs".as_ref(), "/a/file1.js".as_ref(), @@ -2299,11 +2299,11 @@ async fn test_propagate_saves_and_fs_changes( .await .unwrap(); client_a - .fs + .fs() .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) .await .unwrap(); - client_a.fs.insert_file("/a/file4", "4".into()).await; + client_a.fs().insert_file("/a/file4", "4".into()).await; deterministic.run_until_parked(); worktree_a.read_with(cx_a, |tree, _| { @@ -2397,7 +2397,7 @@ async fn test_git_diff_base_change( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -2441,7 +2441,7 @@ async fn test_git_diff_base_change( " .unindent(); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/.git"), &[(Path::new("a.txt"), diff_base.clone())], ); @@ -2486,7 +2486,7 @@ async fn test_git_diff_base_change( ); }); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/.git"), &[(Path::new("a.txt"), new_diff_base.clone())], ); @@ -2531,7 +2531,7 @@ async fn test_git_diff_base_change( " .unindent(); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/sub/.git"), &[(Path::new("b.txt"), diff_base.clone())], ); @@ -2576,7 +2576,7 @@ async fn test_git_diff_base_change( ); }); - client_a.fs.as_fake().set_index_for_repo( + client_a.fs().set_index_for_repo( Path::new("/dir/sub/.git"), &[(Path::new("b.txt"), new_diff_base.clone())], ); @@ -2635,7 +2635,7 @@ async fn test_git_branch_name( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -2654,8 +2654,7 @@ async fn test_git_branch_name( let project_remote = client_b.build_remote_project(project_id, cx_b).await; client_a - .fs - .as_fake() + .fs() .set_branch_name(Path::new("/dir/.git"), Some("branch-1")); // Wait for it to catch up to the new branch @@ -2680,8 +2679,7 @@ async fn test_git_branch_name( }); client_a - .fs - .as_fake() + .fs() .set_branch_name(Path::new("/dir/.git"), Some("branch-2")); // Wait for buffer_local_a to receive it @@ -2720,7 +2718,7 @@ async fn test_git_status_sync( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -2734,7 +2732,7 @@ async fn test_git_status_sync( const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; - client_a.fs.as_fake().set_status_for_repo_via_git_operation( + client_a.fs().set_status_for_repo_via_git_operation( Path::new("/dir/.git"), &[ (&Path::new(A_TXT), GitFileStatus::Added), @@ -2780,16 +2778,13 @@ async fn test_git_status_sync( assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx); }); - client_a - .fs - .as_fake() - .set_status_for_repo_via_working_copy_change( - Path::new("/dir/.git"), - &[ - (&Path::new(A_TXT), GitFileStatus::Modified), - (&Path::new(B_TXT), GitFileStatus::Modified), - ], - ); + client_a.fs().set_status_for_repo_via_working_copy_change( + Path::new("/dir/.git"), + &[ + (&Path::new(A_TXT), GitFileStatus::Modified), + (&Path::new(B_TXT), GitFileStatus::Modified), + ], + ); // Wait for buffer_local_a to receive it deterministic.run_until_parked(); @@ -2860,7 +2855,7 @@ async fn test_fs_operations( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3133,7 +3128,7 @@ async fn test_local_settings( // As client A, open a project that contains some local settings files client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3175,7 +3170,7 @@ async fn test_local_settings( // As client A, update a settings file. As Client B, see the changed settings. client_a - .fs + .fs() .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) .await; deterministic.run_until_parked(); @@ -3192,17 +3187,17 @@ async fn test_local_settings( // As client A, create and remove some settings files. As client B, see the changed settings. client_a - .fs + .fs() .remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) .await .unwrap(); client_a - .fs + .fs() .create_dir("/dir/b/.zed".as_ref()) .await .unwrap(); client_a - .fs + .fs() .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) .await; deterministic.run_until_parked(); @@ -3223,11 +3218,11 @@ async fn test_local_settings( // As client A, change and remove settings files while client B is disconnected. client_a - .fs + .fs() .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) .await; client_a - .fs + .fs() .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) .await .unwrap(); @@ -3261,7 +3256,7 @@ async fn test_buffer_conflict_after_save( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3323,7 +3318,7 @@ async fn test_buffer_reloading( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3351,7 +3346,7 @@ async fn test_buffer_reloading( let new_contents = Rope::from("d\ne\nf"); client_a - .fs + .fs() .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows) .await .unwrap(); @@ -3380,7 +3375,7 @@ async fn test_editing_while_guest_opens_buffer( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; @@ -3429,7 +3424,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/dir", json!({ "a.txt": "Some text\n" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; @@ -3527,7 +3522,7 @@ async fn test_leaving_worktree_while_opening_buffer( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/dir", json!({ "a.txt": "a-contents" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await; @@ -3570,7 +3565,7 @@ async fn test_canceling_buffer_opening( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -3626,7 +3621,7 @@ async fn test_leaving_project( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -3714,9 +3709,9 @@ async fn test_leaving_project( cx_b.spawn(|cx| { Project::remote( project_id, - client_b.client.clone(), - client_b.user_store.clone(), - client_b.language_registry.clone(), + client_b.app_state.client.clone(), + client_b.user_store().clone(), + client_b.language_registry().clone(), FakeFs::new(cx.background()), cx, ) @@ -3768,11 +3763,11 @@ async fn test_collaborating_with_diagnostics( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); // Share a project as client A client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -4040,11 +4035,11 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"]; client_a - .fs + .fs() .insert_tree( "/test", json!({ @@ -4181,10 +4176,10 @@ async fn test_collaborating_with_completion( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -4342,7 +4337,7 @@ async fn test_reloading_buffer_manually( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree("/a", json!({ "a.rs": "let one = 1;" })) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; @@ -4373,7 +4368,7 @@ async fn test_reloading_buffer_manually( buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;")); client_a - .fs + .fs() .save( "/a/a.rs".as_ref(), &Rope::from("let seven = 7;"), @@ -4444,14 +4439,14 @@ async fn test_formatting_buffer( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); // Here we insert a fake tree with a directory that exists on disk. This is needed // because later we'll invoke a command, which requires passing a working directory // that points to a valid location on disk. let directory = env::current_dir().unwrap(); client_a - .fs + .fs() .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" })) .await; let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await; @@ -4553,10 +4548,10 @@ async fn test_definition( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -4701,10 +4696,10 @@ async fn test_references( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -4797,7 +4792,7 @@ async fn test_project_search( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -4883,7 +4878,7 @@ async fn test_document_highlights( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/root-1", json!({ @@ -4902,7 +4897,7 @@ async fn test_document_highlights( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -4989,7 +4984,7 @@ async fn test_lsp_hover( let active_call_a = cx_a.read(ActiveCall::global); client_a - .fs + .fs() .insert_tree( "/root-1", json!({ @@ -5008,7 +5003,7 @@ async fn test_lsp_hover( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await; let project_id = active_call_a @@ -5107,10 +5102,10 @@ async fn test_project_symbols( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/code", json!({ @@ -5218,10 +5213,10 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/root", json!({ @@ -5278,6 +5273,7 @@ async fn test_collaborating_with_code_actions( deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; + // let client_b = server.create_client(cx_b, "user_b").await; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) @@ -5296,10 +5292,10 @@ async fn test_collaborating_with_code_actions( Some(tree_sitter_rust::language()), ); let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -5316,7 +5312,8 @@ async fn test_collaborating_with_code_actions( // Join the project as client B. let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let (_window_b, workspace_b) = + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -5521,10 +5518,10 @@ async fn test_collaborating_with_renames( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -5540,7 +5537,8 @@ async fn test_collaborating_with_renames( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let (_window_b, workspace_b) = + cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "one.rs"), None, true, cx) @@ -5706,10 +5704,10 @@ async fn test_language_server_statuses( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/dir", json!({ @@ -6166,7 +6164,7 @@ async fn test_contacts( // Test removing a contact client_b - .user_store + .user_store() .update(cx_b, |store, cx| { store.remove_contact(client_c.user_id().unwrap(), cx) }) @@ -6189,7 +6187,7 @@ async fn test_contacts( client: &TestClient, cx: &TestAppContext, ) -> Vec<(String, &'static str, &'static str)> { - client.user_store.read_with(cx, |store, _| { + client.user_store().read_with(cx, |store, _| { store .contacts() .iter() @@ -6232,14 +6230,14 @@ async fn test_contact_requests( // User A and User C request that user B become their contact. client_a - .user_store + .user_store() .update(cx_a, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) }) .await .unwrap(); client_c - .user_store + .user_store() .update(cx_c, |store, cx| { store.request_contact(client_b.user_id().unwrap(), cx) }) @@ -6293,7 +6291,7 @@ async fn test_contact_requests( // User B accepts the request from user A. client_b - .user_store + .user_store() .update(cx_b, |store, cx| { store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx) }) @@ -6337,7 +6335,7 @@ async fn test_contact_requests( // User B rejects the request from user C. client_b - .user_store + .user_store() .update(cx_b, |store, cx| { store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx) }) @@ -6419,7 +6417,7 @@ async fn test_basic_following( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -6980,7 +6978,7 @@ async fn test_join_call_after_screen_was_shared( .await .unwrap(); - client_b.user_store.update(cx_b, |user_store, _| { + client_b.user_store().update(cx_b, |user_store, _| { user_store.clear_cache(); }); @@ -7040,7 +7038,7 @@ async fn test_following_tab_order( cx_b.update(editor::init); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7163,7 +7161,7 @@ async fn test_peers_following_each_other( // Client A shares a project. client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7336,7 +7334,7 @@ async fn test_auto_unfollowing( // Client A shares a project. client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7500,7 +7498,7 @@ async fn test_peers_simultaneously_following_each_other( cx_a.update(editor::init); cx_b.update(editor::init); - client_a.fs.insert_tree("/a", json!({})).await; + client_a.fs().insert_tree("/a", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let workspace_a = client_a.build_workspace(&project_a, cx_a); let project_id = active_call_a @@ -7577,10 +7575,10 @@ async fn test_on_input_format_from_host_to_guest( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7706,10 +7704,10 @@ async fn test_on_input_format_from_guest_to_host( ..Default::default() })) .await; - client_a.language_registry.add(Arc::new(language)); + client_a.language_registry().add(Arc::new(language)); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -7862,11 +7860,11 @@ async fn test_mutual_editor_inlay_hint_cache_update( })) .await; let language = Arc::new(language); - client_a.language_registry.add(Arc::clone(&language)); - client_b.language_registry.add(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); client_a - .fs + .fs() .insert_tree( "/a", json!({ @@ -8169,11 +8167,11 @@ async fn test_inlay_hint_refresh_is_forwarded( })) .await; let language = Arc::new(language); - client_a.language_registry.add(Arc::clone(&language)); - client_b.language_registry.add(language); + client_a.language_registry().add(Arc::clone(&language)); + client_b.language_registry().add(language); client_a - .fs + .fs() .insert_tree( "/a", json!({ diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 8062a12b83..8202b53fdc 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -396,9 +396,9 @@ async fn apply_client_operation( ); let root_path = Path::new("/").join(&first_root_name); - client.fs.create_dir(&root_path).await.unwrap(); + client.fs().create_dir(&root_path).await.unwrap(); client - .fs + .fs() .create_file(&root_path.join("main.rs"), Default::default()) .await .unwrap(); @@ -422,8 +422,8 @@ async fn apply_client_operation( ); ensure_project_shared(&project, client, cx).await; - if !client.fs.paths(false).contains(&new_root_path) { - client.fs.create_dir(&new_root_path).await.unwrap(); + if !client.fs().paths(false).contains(&new_root_path) { + client.fs().create_dir(&new_root_path).await.unwrap(); } project .update(cx, |project, cx| { @@ -475,7 +475,7 @@ async fn apply_client_operation( Some(room.update(cx, |room, cx| { room.join_project( project_id, - client.language_registry.clone(), + client.language_registry().clone(), FakeFs::new(cx.background().clone()), cx, ) @@ -743,7 +743,7 @@ async fn apply_client_operation( content, } => { if !client - .fs + .fs() .directories(false) .contains(&path.parent().unwrap().to_owned()) { @@ -752,14 +752,14 @@ async fn apply_client_operation( if is_dir { log::info!("{}: creating dir at {:?}", client.username, path); - client.fs.create_dir(&path).await.unwrap(); + client.fs().create_dir(&path).await.unwrap(); } else { - let exists = client.fs.metadata(&path).await?.is_some(); + let exists = client.fs().metadata(&path).await?.is_some(); let verb = if exists { "updating" } else { "creating" }; log::info!("{}: {} file at {:?}", verb, client.username, path); client - .fs + .fs() .save(&path, &content.as_str().into(), fs::LineEnding::Unix) .await .unwrap(); @@ -771,12 +771,12 @@ async fn apply_client_operation( repo_path, contents, } => { - if !client.fs.directories(false).contains(&repo_path) { + if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } for (path, _) in contents.iter() { - if !client.fs.files().contains(&repo_path.join(path)) { + if !client.fs().files().contains(&repo_path.join(path)) { return Err(TestError::Inapplicable); } } @@ -793,16 +793,16 @@ async fn apply_client_operation( .iter() .map(|(path, contents)| (path.as_path(), contents.clone())) .collect::>(); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; } - client.fs.set_index_for_repo(&dot_git_dir, &contents); + client.fs().set_index_for_repo(&dot_git_dir, &contents); } GitOperation::WriteGitBranch { repo_path, new_branch, } => { - if !client.fs.directories(false).contains(&repo_path) { + if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } @@ -814,21 +814,21 @@ async fn apply_client_operation( ); let dot_git_dir = repo_path.join(".git"); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; } - client.fs.set_branch_name(&dot_git_dir, new_branch); + client.fs().set_branch_name(&dot_git_dir, new_branch); } GitOperation::WriteGitStatuses { repo_path, statuses, git_operation, } => { - if !client.fs.directories(false).contains(&repo_path) { + if !client.fs().directories(false).contains(&repo_path) { return Err(TestError::Inapplicable); } for (path, _) in statuses.iter() { - if !client.fs.files().contains(&repo_path.join(path)) { + if !client.fs().files().contains(&repo_path.join(path)) { return Err(TestError::Inapplicable); } } @@ -847,16 +847,16 @@ async fn apply_client_operation( .map(|(path, val)| (path.as_path(), val.clone())) .collect::>(); - if client.fs.metadata(&dot_git_dir).await?.is_none() { - client.fs.create_dir(&dot_git_dir).await?; + if client.fs().metadata(&dot_git_dir).await?.is_none() { + client.fs().create_dir(&dot_git_dir).await?; } if git_operation { client - .fs + .fs() .set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice()); } else { - client.fs.set_status_for_repo_via_working_copy_change( + client.fs().set_status_for_repo_via_working_copy_change( &dot_git_dir, statuses.as_slice(), ); @@ -1499,7 +1499,7 @@ impl TestPlan { // Invite a contact to the current call 0..=70 => { let available_contacts = - client.user_store.read_with(cx, |user_store, _| { + client.user_store().read_with(cx, |user_store, _| { user_store .contacts() .iter() @@ -1596,7 +1596,7 @@ impl TestPlan { .choose(&mut self.rng) .cloned() else { continue }; let project_root_name = root_name_for_project(&project, cx); - let mut paths = client.fs.paths(false); + let mut paths = client.fs().paths(false); paths.remove(0); let new_root_path = if paths.is_empty() || self.rng.gen() { Path::new("/").join(&self.next_root_dir_name(user_id)) @@ -1776,7 +1776,7 @@ impl TestPlan { let is_dir = self.rng.gen::(); let content; let mut path; - let dir_paths = client.fs.directories(false); + let dir_paths = client.fs().directories(false); if is_dir { content = String::new(); @@ -1786,7 +1786,7 @@ impl TestPlan { content = Alphanumeric.sample_string(&mut self.rng, 16); // Create a new file or overwrite an existing file - let file_paths = client.fs.files(); + let file_paths = client.fs().files(); if file_paths.is_empty() || self.rng.gen_bool(0.5) { path = dir_paths.choose(&mut self.rng).unwrap().clone(); path.push(gen_file_name(&mut self.rng)); @@ -1812,7 +1812,7 @@ impl TestPlan { client: &TestClient, ) -> Vec { let mut paths = client - .fs + .fs() .files() .into_iter() .filter(|path| path.starts_with(repo_path)) @@ -1829,7 +1829,7 @@ impl TestPlan { } let repo_path = client - .fs + .fs() .directories(false) .choose(&mut self.rng) .unwrap() @@ -1928,7 +1928,7 @@ async fn simulate_client( name: "the-fake-language-server", capabilities: lsp::LanguageServer::full_capabilities(), initializer: Some(Box::new({ - let fs = client.fs.clone(); + let fs = client.app_state.fs.clone(); move |fake_server: &mut FakeLanguageServer| { fake_server.handle_request::( |_, _| async move { @@ -1973,7 +1973,7 @@ async fn simulate_client( let background = cx.background(); let mut rng = background.rng(); let count = rng.gen_range::(1..3); - let files = fs.files(); + let files = fs.as_fake().files(); let files = (0..count) .map(|_| files.choose(&mut *rng).unwrap().clone()) .collect::>(); @@ -2023,7 +2023,7 @@ async fn simulate_client( ..Default::default() })) .await; - client.language_registry.add(Arc::new(language)); + client.app_state.languages.add(Arc::new(language)); while let Some(batch_id) = operation_rx.next().await { let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index edbb89e339..c42ed34de6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -3,9 +3,9 @@ mod contact_notification; mod face_pile; mod incoming_call_notification; mod notifications; +pub mod panel; mod project_shared_notification; mod sharing_status_indicator; -pub mod panel; use call::{ActiveCall, Room}; pub use collab_titlebar_item::CollabTitlebarItem; diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 53f7eee79a..c6940fbd14 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -6,7 +6,7 @@ use anyhow::Result; use call::ActiveCall; use client::{proto::PeerId, Channel, ChannelStore, Client, Contact, User, UserStore}; use contact_finder::build_contact_finder; -use context_menu::ContextMenu; +use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; use futures::StreamExt; @@ -18,6 +18,7 @@ use gpui::{ MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, }, geometry::{rect::RectF, vector::vec2f}, + impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -36,8 +37,15 @@ use workspace::{ Workspace, }; +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct RemoveChannel { + channel_id: u64, +} + actions!(collab_panel, [ToggleFocus]); +impl_actions!(collab_panel, [RemoveChannel]); + const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { @@ -49,6 +57,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::select_next); cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); + cx.add_action(CollabPanel::remove_channel); } #[derive(Debug, Default)] @@ -305,6 +314,8 @@ impl CollabPanel { let active_call = ActiveCall::global(cx); this.subscriptions .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx))); + this.subscriptions + .push(cx.observe(&this.channel_store, |this, _, cx| this.update_entries(cx))); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); @@ -1278,6 +1289,19 @@ impl CollabPanel { .on_click(MouseButton::Left, move |_, this, cx| { this.join_channel(channel_id, cx); }) + .on_click(MouseButton::Right, move |e, this, cx| { + this.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + e.position, + gpui::elements::AnchorCorner::BottomLeft, + vec![ContextMenuItem::action( + "Remove Channel", + RemoveChannel { channel_id }, + )], + cx, + ); + }); + }) .into_any() } @@ -1564,14 +1588,13 @@ impl CollabPanel { } } } else if let Some((_editing_state, channel_name)) = self.take_editing_state(cx) { - dbg!(&channel_name); let create_channel = self.channel_store.update(cx, |channel_store, cx| { channel_store.create_channel(&channel_name, None) }); cx.foreground() .spawn(async move { - dbg!(create_channel.await).ok(); + create_channel.await.ok(); }) .detach(); } @@ -1600,6 +1623,36 @@ impl CollabPanel { } } + fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { + let channel_id = action.channel_id; + let channel_store = self.channel_store.clone(); + if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { + let prompt_message = format!( + "Are you sure you want to remove the channel \"{}\"?", + channel.name + ); + let mut answer = + cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + let window_id = cx.window_id(); + cx.spawn(|_, mut cx| async move { + if answer.next().await == Some(0) { + if let Err(e) = channel_store + .update(&mut cx, |channels, cx| channels.remove_channel(channel_id)) + .await + { + cx.prompt( + window_id, + PromptLevel::Info, + &format!("Failed to remove channel: {}", e), + &["Ok"], + ); + } + } + }) + .detach(); + } + } + fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( diff --git a/crates/collab_ui/src/panel/channel_modal.rs b/crates/collab_ui/src/panel/channel_modal.rs index 562536d58c..fff1dc8624 100644 --- a/crates/collab_ui/src/panel/channel_modal.rs +++ b/crates/collab_ui/src/panel/channel_modal.rs @@ -1,5 +1,5 @@ use editor::Editor; -use gpui::{elements::*, AnyViewHandle, Entity, View, ViewContext, ViewHandle, AppContext}; +use gpui::{elements::*, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use menu::Cancel; use workspace::{item::ItemHandle, Modal}; @@ -62,12 +62,10 @@ impl View for ChannelModal { .constrained() .with_max_width(540.) .with_max_height(420.) - }) .on_click(gpui::platform::MouseButton::Left, |_, _, _| {}) // Capture click and down events - .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| { - v.dismiss(cx) - }).into_any_named("channel modal") + .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| v.dismiss(cx)) + .into_any_named("channel modal") } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8a4a72c268..f49a879dc7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -137,6 +137,7 @@ message Envelope { RespondToChannelInvite respond_to_channel_invite = 123; UpdateChannels update_channels = 124; JoinChannel join_channel = 125; + RemoveChannel remove_channel = 126; } } @@ -875,6 +876,11 @@ message JoinChannel { uint64 channel_id = 1; } +message RemoveChannel { + uint64 channel_id = 1; +} + + message CreateChannel { string name = 1; optional uint64 parent_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d71ddeed83..f6985d6906 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -231,6 +231,7 @@ messages!( (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), + (RemoveChannel, Foreground), (UpdateChannels, Foreground), (UpdateDiagnosticSummary, Foreground), (UpdateFollowers, Foreground), @@ -296,6 +297,7 @@ request_messages!( (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), (JoinChannel, JoinRoomResponse), + (RemoveChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 95077649a8..4fe8b5d0f4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3412,6 +3412,7 @@ impl Workspace { pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { let client = project.read(cx).client(); let user_store = project.read(cx).user_store(); + let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); let app_state = Arc::new(AppState { From b389dcc637d695d53a5ae883cb29fbcaf573505c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 1 Aug 2023 16:48:11 -0700 Subject: [PATCH 047/326] Add subchannel creation co-authored-by: max --- crates/collab/src/db.rs | 95 +++++++++++++++++++++++++++++++---- crates/collab/src/db/tests.rs | 28 +++++++++++ crates/collab/src/rpc.rs | 28 +++++++---- 3 files changed, 131 insertions(+), 20 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 066c93ec71..58607836cc 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3093,6 +3093,22 @@ impl Database { self.transaction(move |tx| async move { let tx = tx; + if let Some(parent) = parent { + let channels = self.get_channel_ancestors(parent, &*tx).await?; + channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channels.iter().copied())) + .filter( + channel_member::Column::UserId + .eq(creator_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| { + anyhow!("User does not have the permissions to create this channel") + })?; + } + let channel = channel::ActiveModel { name: ActiveValue::Set(name.to_string()), ..Default::default() @@ -3175,11 +3191,6 @@ impl Database { let channels_to_remove = descendants.keys().copied().collect::>(); - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIds { - UserId, - } - let members_to_notify: Vec = channel_member::Entity::find() .filter(channel_member::Column::ChannelId.is_in(channels_to_remove.iter().copied())) .select_only() @@ -3325,11 +3336,6 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelIds { - ChannelId, - } - let starting_channel_ids: Vec = channel_member::Entity::find() .filter( channel_member::Column::UserId @@ -3368,6 +3374,65 @@ impl Database { .await } + pub async fn get_channel_members(&self, id: ChannelId) -> Result> { + self.transaction(|tx| async move { + let tx = tx; + let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?; + let user_ids = channel_member::Entity::find() + .distinct() + .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + Ok(user_ids) + }) + .await + } + + async fn get_channel_ancestors( + &self, + id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let sql = format!( + r#" + WITH RECURSIVE channel_tree(child_id, parent_id) AS ( + SELECT CAST(NULL as INTEGER) as child_id, root_ids.column1 as parent_id + FROM (VALUES ({})) as root_ids + UNION + SELECT channel_parents.child_id, channel_parents.parent_id + FROM channel_parents, channel_tree + WHERE channel_parents.child_id = channel_tree.parent_id + ) + SELECT DISTINCT channel_tree.parent_id + FROM channel_tree + "#, + id + ); + + #[derive(FromQueryResult, Debug, PartialEq)] + pub struct ChannelParent { + pub parent_id: ChannelId, + } + + let stmt = Statement::from_string(self.pool.get_database_backend(), sql); + + let mut channel_ids_stream = channel_parent::Entity::find() + .from_raw_sql(stmt) + .into_model::() + .stream(&*tx) + .await?; + + let mut channel_ids = vec![]; + while let Some(channel_id) = channel_ids_stream.next().await { + channel_ids.push(channel_id?.parent_id); + } + + Ok(channel_ids) + } + async fn get_channel_descendants( &self, channel_ids: impl IntoIterator, @@ -3948,6 +4013,16 @@ pub struct WorktreeSettingsFile { pub content: String, } +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +enum QueryChannelIds { + ChannelId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +enum QueryUserIds { + UserId, +} + #[cfg(test)] pub use test::*; diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 3a47097f7d..2ffcef454b 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -899,7 +899,30 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .unwrap() .user_id; + let b_id = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + db.invite_channel_member(zed_id, b_id, a_id, true) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + let crdb_id = db .create_channel("crdb", Some(zed_id), "2", a_id) .await @@ -912,6 +935,11 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .create_channel("replace", Some(zed_id), "4", a_id) .await .unwrap(); + + let mut members = db.get_channel_members(replace_id).await.unwrap(); + members.sort(); + assert_eq!(members, &[a_id, b_id]); + let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap(); let cargo_id = db .create_channel("cargo", Some(rust_id), "6", a_id) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1465c66601..819a3dc4f6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2108,25 +2108,33 @@ async fn create_channel( live_kit.create_room(live_kit_room.clone()).await?; } + let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); let id = db - .create_channel( - &request.name, - request.parent_id.map(|id| ChannelId::from_proto(id)), - &live_kit_room, - session.user_id, - ) + .create_channel(&request.name, parent_id, &live_kit_room, session.user_id) .await?; + response.send(proto::CreateChannelResponse { + channel_id: id.to_proto(), + })?; + let mut update = proto::UpdateChannels::default(); update.channels.push(proto::Channel { id: id.to_proto(), name: request.name, parent_id: request.parent_id, }); - session.peer.send(session.connection_id, update)?; - response.send(proto::CreateChannelResponse { - channel_id: id.to_proto(), - })?; + + if let Some(parent_id) = parent_id { + let member_ids = db.get_channel_members(parent_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())?; + } + } + } else { + session.peer.send(session.connection_id, update)?; + } Ok(()) } From 6a404dfe317131508c30ecef5eaa761bd9294951 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 1 Aug 2023 18:20:25 -0700 Subject: [PATCH 048/326] Start work on adding sub-channels in the UI Co-authored-by: Mikayla --- crates/collab_ui/src/panel.rs | 315 +++++++++++++++++++--------------- 1 file changed, 179 insertions(+), 136 deletions(-) diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index c6940fbd14..bca0da6176 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -17,7 +17,10 @@ use gpui::{ Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, }, - geometry::{rect::RectF, vector::vec2f}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, @@ -42,9 +45,14 @@ struct RemoveChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct NewChannel { + channel_id: u64, +} + actions!(collab_panel, [ToggleFocus]); -impl_actions!(collab_panel, [RemoveChannel]); +impl_actions!(collab_panel, [RemoveChannel, NewChannel]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -58,11 +66,12 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); cx.add_action(CollabPanel::remove_channel); + cx.add_action(CollabPanel::new_subchannel); } #[derive(Debug, Default)] pub struct ChannelEditingState { - root_channel: bool, + parent_id: Option, } pub struct CollabPanel { @@ -74,7 +83,7 @@ pub struct CollabPanel { filter_editor: ViewHandle, channel_name_editor: ViewHandle, channel_editing_state: Option, - entries: Vec, + entries: Vec, selection: Option, user_store: ModelHandle, channel_store: ModelHandle, @@ -109,7 +118,7 @@ enum Section { } #[derive(Clone, Debug)] -enum ContactEntry { +enum ListEntry { Header(Section, usize), CallParticipant { user: Arc, @@ -125,10 +134,13 @@ enum ContactEntry { peer_id: PeerId, is_last: bool, }, - ChannelInvite(Arc), IncomingRequest(Arc), OutgoingRequest(Arc), + ChannelInvite(Arc), Channel(Arc), + ChannelEditor { + depth: usize, + }, Contact { contact: Arc, calling: bool, @@ -166,7 +178,7 @@ impl CollabPanel { this.selection = this .entries .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_, _))); + .position(|entry| !matches!(entry, ListEntry::Header(_, _))); } } }) @@ -184,6 +196,7 @@ impl CollabPanel { cx.subscribe(&channel_name_editor, |this, _, event, cx| { if let editor::Event::Blurred = event { this.take_editing_state(cx); + this.update_entries(cx); cx.notify(); } }) @@ -196,7 +209,7 @@ impl CollabPanel { let current_project_id = this.project.read(cx).remote_id(); match &this.entries[ix] { - ContactEntry::Header(section, depth) => { + ListEntry::Header(section, depth) => { let is_collapsed = this.collapsed_sections.contains(section); this.render_header( *section, @@ -207,7 +220,7 @@ impl CollabPanel { cx, ) } - ContactEntry::CallParticipant { user, is_pending } => { + ListEntry::CallParticipant { user, is_pending } => { Self::render_call_participant( user, *is_pending, @@ -215,7 +228,7 @@ impl CollabPanel { &theme.collab_panel, ) } - ContactEntry::ParticipantProject { + ListEntry::ParticipantProject { project_id, worktree_root_names, host_user_id, @@ -230,7 +243,7 @@ impl CollabPanel { &theme.collab_panel, cx, ), - ContactEntry::ParticipantScreen { peer_id, is_last } => { + ListEntry::ParticipantScreen { peer_id, is_last } => { Self::render_participant_screen( *peer_id, *is_last, @@ -239,17 +252,17 @@ impl CollabPanel { cx, ) } - ContactEntry::Channel(channel) => { + ListEntry::Channel(channel) => { Self::render_channel(&*channel, &theme.collab_panel, is_selected, cx) } - ContactEntry::ChannelInvite(channel) => Self::render_channel_invite( + ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), &theme.collab_panel, is_selected, cx, ), - ContactEntry::IncomingRequest(user) => Self::render_contact_request( + ListEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), &theme.collab_panel, @@ -257,7 +270,7 @@ impl CollabPanel { is_selected, cx, ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + ListEntry::OutgoingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), &theme.collab_panel, @@ -265,7 +278,7 @@ impl CollabPanel { is_selected, cx, ), - ContactEntry::Contact { contact, calling } => Self::render_contact( + ListEntry::Contact { contact, calling } => Self::render_contact( contact, *calling, &this.project, @@ -273,6 +286,9 @@ impl CollabPanel { is_selected, cx, ), + ListEntry::ChannelEditor { depth } => { + this.render_channel_editor(&theme.collab_panel, *depth, cx) + } } }); @@ -369,13 +385,6 @@ impl CollabPanel { ); } - fn is_editing_root_channel(&self) -> bool { - self.channel_editing_state - .as_ref() - .map(|state| state.root_channel) - .unwrap_or(false) - } - fn update_entries(&mut self, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); @@ -407,13 +416,13 @@ impl CollabPanel { )); if !matches.is_empty() { let user_id = user.id; - participant_entries.push(ContactEntry::CallParticipant { + participant_entries.push(ListEntry::CallParticipant { user, is_pending: false, }); let mut projects = room.local_participant().projects.iter().peekable(); while let Some(project) = projects.next() { - participant_entries.push(ContactEntry::ParticipantProject { + participant_entries.push(ListEntry::ParticipantProject { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, @@ -444,13 +453,13 @@ impl CollabPanel { for mat in matches { let user_id = mat.candidate_id as u64; let participant = &room.remote_participants()[&user_id]; - participant_entries.push(ContactEntry::CallParticipant { + participant_entries.push(ListEntry::CallParticipant { user: participant.user.clone(), is_pending: false, }); let mut projects = participant.projects.iter().peekable(); while let Some(project) = projects.next() { - participant_entries.push(ContactEntry::ParticipantProject { + participant_entries.push(ListEntry::ParticipantProject { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: participant.user.id, @@ -458,7 +467,7 @@ impl CollabPanel { }); } if !participant.video_tracks.is_empty() { - participant_entries.push(ContactEntry::ParticipantScreen { + participant_entries.push(ListEntry::ParticipantScreen { peer_id: participant.peer_id, is_last: true, }); @@ -486,22 +495,20 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + participant_entries.extend(matches.iter().map(|mat| ListEntry::CallParticipant { user: room.pending_participants()[mat.candidate_id].clone(), is_pending: true, })); if !participant_entries.is_empty() { - self.entries - .push(ContactEntry::Header(Section::ActiveCall, 0)); + self.entries.push(ListEntry::Header(Section::ActiveCall, 0)); if !self.collapsed_sections.contains(&Section::ActiveCall) { self.entries.extend(participant_entries); } } } - self.entries - .push(ContactEntry::Header(Section::Channels, 0)); + self.entries.push(ListEntry::Header(Section::Channels, 0)); let channels = channel_store.channels(); if !channels.is_empty() { @@ -525,15 +532,25 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - self.entries.extend( - matches - .iter() - .map(|mat| ContactEntry::Channel(channels[mat.candidate_id].clone())), - ); + if let Some(state) = &self.channel_editing_state { + if state.parent_id.is_none() { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); + } + } + for mat in matches { + let channel = &channels[mat.candidate_id]; + self.entries.push(ListEntry::Channel(channel.clone())); + if let Some(state) = &self.channel_editing_state { + if state.parent_id == Some(channel.id) { + self.entries.push(ListEntry::ChannelEditor { + depth: channel.depth + 1, + }); + } + } + } } - self.entries - .push(ContactEntry::Header(Section::Contacts, 0)); + self.entries.push(ListEntry::Header(Section::Contacts, 0)); let mut request_entries = Vec::new(); let channel_invites = channel_store.channel_invitations(); @@ -556,9 +573,9 @@ impl CollabPanel { executor.clone(), )); request_entries.extend( - matches.iter().map(|mat| { - ContactEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) - }), + matches + .iter() + .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())), ); } @@ -587,7 +604,7 @@ impl CollabPanel { request_entries.extend( matches .iter() - .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), ); } @@ -616,13 +633,12 @@ impl CollabPanel { request_entries.extend( matches .iter() - .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), ); } if !request_entries.is_empty() { - self.entries - .push(ContactEntry::Header(Section::Requests, 1)); + self.entries.push(ListEntry::Header(Section::Requests, 1)); if !self.collapsed_sections.contains(&Section::Requests) { self.entries.append(&mut request_entries); } @@ -668,12 +684,12 @@ impl CollabPanel { (offline_contacts, Section::Offline), ] { if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section, 1)); + self.entries.push(ListEntry::Header(section, 1)); if !self.collapsed_sections.contains(§ion) { let active_call = &ActiveCall::global(cx).read(cx); for mat in matches { let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact { + self.entries.push(ListEntry::Contact { contact: contact.clone(), calling: active_call.pending_invites().contains(&contact.user.id), }); @@ -1072,15 +1088,7 @@ impl CollabPanel { render_icon_button(&theme.collab_panel.add_contact_button, "icons/plus_16.svg") }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - if this.channel_editing_state.is_none() { - this.channel_editing_state = - Some(ChannelEditingState { root_channel: true }); - } - - cx.focus(this.channel_name_editor.as_any()); - cx.notify(); - }) + .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) .with_tooltip::( 0, "Add or join a channel".into(), @@ -1092,13 +1100,6 @@ impl CollabPanel { _ => None, }; - let addition = match section { - Section::Channels if self.is_editing_root_channel() => { - Some(ChildView::new(self.channel_name_editor.as_any(), cx)) - } - _ => None, - }; - let can_collapse = depth > 0; let icon_size = (&theme.collab_panel).section_icon_size; MouseEventHandler::::new(section as usize, cx, |state, _| { @@ -1112,44 +1113,40 @@ impl CollabPanel { &theme.collab_panel.header_row }; - Flex::column() - .with_child( - Flex::row() - .with_children(if can_collapse { - Some( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .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, - ), - ) + Flex::row() + .with_children(if can_collapse { + Some( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" } else { - None + "icons/chevron_down_8.svg" }) - .with_child( - Label::new(text, header_style.text.clone()) - .aligned() - .left() - .flex(1., true), - ) - .with_children(button.map(|button| button.aligned().right())) + .with_color(header_style.text.color) .constrained() - .with_height(theme.collab_panel.row_height) + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size) .contained() - .with_style(header_style.container), + .with_margin_right( + theme.collab_panel.contact_username.container.margin.left, + ), + ) + } else { + None + }) + .with_child( + Label::new(text, header_style.text.clone()) + .aligned() + .left() + .flex(1., true), ) - .with_children(addition) + .with_children(button.map(|button| button.aligned().right())) + .constrained() + .with_height(theme.collab_panel.row_height) + .contained() + .with_style(header_style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1258,6 +1255,15 @@ impl CollabPanel { event_handler.into_any() } + fn render_channel_editor( + &self, + theme: &theme::CollabPanel, + depth: usize, + cx: &AppContext, + ) -> AnyElement { + ChildView::new(&self.channel_name_editor, cx).into_any() + } + fn render_channel( channel: &Channel, theme: &theme::CollabPanel, @@ -1285,22 +1291,13 @@ impl CollabPanel { .with_height(theme.row_height) .contained() .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) + .with_margin_left(10. * channel.depth as f32) }) .on_click(MouseButton::Left, move |_, this, cx| { this.join_channel(channel_id, cx); }) .on_click(MouseButton::Right, move |e, this, cx| { - this.context_menu.update(cx, |context_menu, cx| { - context_menu.show( - e.position, - gpui::elements::AnchorCorner::BottomLeft, - vec![ContextMenuItem::action( - "Remove Channel", - RemoveChannel { channel_id }, - )], - cx, - ); - }); + this.deploy_channel_context_menu(e.position, channel_id, cx); }) .into_any() } @@ -1489,6 +1486,25 @@ impl CollabPanel { .into_any() } + fn deploy_channel_context_menu( + &mut self, + position: Vector2F, + channel_id: u64, + cx: &mut ViewContext, + ) { + self.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + position, + gpui::elements::AnchorCorner::BottomLeft, + vec![ + ContextMenuItem::action("New Channel", NewChannel { channel_id }), + ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), + ], + cx, + ); + }); + } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { let mut did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { @@ -1553,15 +1569,15 @@ impl CollabPanel { if let Some(selection) = self.selection { if let Some(entry) = self.entries.get(selection) { match entry { - ContactEntry::Header(section, _) => { + ListEntry::Header(section, _) => { self.toggle_expanded(*section, cx); } - ContactEntry::Contact { contact, calling } => { + ListEntry::Contact { contact, calling } => { if contact.online && !contact.busy && !calling { self.call(contact.user.id, Some(self.project.clone()), cx); } } - ContactEntry::ParticipantProject { + ListEntry::ParticipantProject { project_id, host_user_id, .. @@ -1577,7 +1593,7 @@ impl CollabPanel { .detach_and_log_err(cx); } } - ContactEntry::ParticipantScreen { peer_id, .. } => { + ListEntry::ParticipantScreen { peer_id, .. } => { if let Some(workspace) = self.workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { workspace.open_shared_screen(*peer_id, cx) @@ -1587,9 +1603,9 @@ impl CollabPanel { _ => {} } } - } else if let Some((_editing_state, channel_name)) = self.take_editing_state(cx) { + } else if let Some((editing_state, channel_name)) = self.take_editing_state(cx) { let create_channel = self.channel_store.update(cx, |channel_store, cx| { - channel_store.create_channel(&channel_name, None) + channel_store.create_channel(&channel_name, editing_state.parent_id) }); cx.foreground() @@ -1623,6 +1639,28 @@ impl CollabPanel { } } + fn new_root_channel(&mut self, cx: &mut ViewContext) { + if self.channel_editing_state.is_none() { + self.channel_editing_state = Some(ChannelEditingState { parent_id: None }); + self.update_entries(cx); + } + + cx.focus(self.channel_name_editor.as_any()); + cx.notify(); + } + + fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + if self.channel_editing_state.is_none() { + self.channel_editing_state = Some(ChannelEditingState { + parent_id: Some(action.channel_id), + }); + self.update_entries(cx); + } + + cx.focus(self.channel_name_editor.as_any()); + cx.notify(); + } + fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { let channel_id = action.channel_id; let channel_store = self.channel_store.clone(); @@ -1838,9 +1876,9 @@ impl Panel for CollabPanel { } } -impl ContactEntry { +impl ListEntry { fn is_selectable(&self) -> bool { - if let ContactEntry::Header(_, 0) = self { + if let ListEntry::Header(_, 0) = self { false } else { true @@ -1848,24 +1886,24 @@ impl ContactEntry { } } -impl PartialEq for ContactEntry { +impl PartialEq for ListEntry { fn eq(&self, other: &Self) -> bool { match self { - ContactEntry::Header(section_1, depth_1) => { - if let ContactEntry::Header(section_2, depth_2) = other { + ListEntry::Header(section_1, depth_1) => { + if let ListEntry::Header(section_2, depth_2) = other { return section_1 == section_2 && depth_1 == depth_2; } } - ContactEntry::CallParticipant { user: user_1, .. } => { - if let ContactEntry::CallParticipant { user: user_2, .. } = other { + ListEntry::CallParticipant { user: user_1, .. } => { + if let ListEntry::CallParticipant { user: user_2, .. } = other { return user_1.id == user_2.id; } } - ContactEntry::ParticipantProject { + ListEntry::ParticipantProject { project_id: project_id_1, .. } => { - if let ContactEntry::ParticipantProject { + if let ListEntry::ParticipantProject { project_id: project_id_2, .. } = other @@ -1873,46 +1911,51 @@ impl PartialEq for ContactEntry { return project_id_1 == project_id_2; } } - ContactEntry::ParticipantScreen { + ListEntry::ParticipantScreen { peer_id: peer_id_1, .. } => { - if let ContactEntry::ParticipantScreen { + if let ListEntry::ParticipantScreen { peer_id: peer_id_2, .. } = other { return peer_id_1 == peer_id_2; } } - ContactEntry::Channel(channel_1) => { - if let ContactEntry::Channel(channel_2) = other { + ListEntry::Channel(channel_1) => { + if let ListEntry::Channel(channel_2) = other { return channel_1.id == channel_2.id; } } - ContactEntry::ChannelInvite(channel_1) => { - if let ContactEntry::ChannelInvite(channel_2) = other { + ListEntry::ChannelInvite(channel_1) => { + if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; } } - ContactEntry::IncomingRequest(user_1) => { - if let ContactEntry::IncomingRequest(user_2) = other { + ListEntry::IncomingRequest(user_1) => { + if let ListEntry::IncomingRequest(user_2) = other { return user_1.id == user_2.id; } } - ContactEntry::OutgoingRequest(user_1) => { - if let ContactEntry::OutgoingRequest(user_2) = other { + ListEntry::OutgoingRequest(user_1) => { + if let ListEntry::OutgoingRequest(user_2) = other { return user_1.id == user_2.id; } } - ContactEntry::Contact { + ListEntry::Contact { contact: contact_1, .. } => { - if let ContactEntry::Contact { + if let ListEntry::Contact { contact: contact_2, .. } = other { return contact_1.user.id == contact_2.user.id; } } + ListEntry::ChannelEditor { depth } => { + if let ListEntry::ChannelEditor { depth: other_depth } = other { + return depth == other_depth; + } + } } false } From 7145f47454e9ad37525043a47a73389ce2919259 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 1 Aug 2023 18:42:14 -0700 Subject: [PATCH 049/326] Fix a few bugs in how channels are moved around --- crates/collab/src/rpc.rs | 2 +- crates/collab_ui/src/panel.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 819a3dc4f6..eaa3eb8261 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2364,7 +2364,7 @@ fn build_initial_channels_update( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - parent_id: None, + parent_id: channel.parent_id.map(|id| id.to_proto()), }); } diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index bca0da6176..1973ddd9f6 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -511,7 +511,7 @@ impl CollabPanel { self.entries.push(ListEntry::Header(Section::Channels, 0)); let channels = channel_store.channels(); - if !channels.is_empty() { + if !(channels.is_empty() && self.channel_editing_state.is_none()) { self.match_candidates.clear(); self.match_candidates .extend( @@ -1291,7 +1291,7 @@ impl CollabPanel { .with_height(theme.row_height) .contained() .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) - .with_margin_left(10. * channel.depth as f32) + .with_margin_left(20. * channel.depth as f32) }) .on_click(MouseButton::Left, move |_, this, cx| { this.join_channel(channel_id, cx); From 4ef59899d1234c5156bf1efe3f058adfd373383d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:05:29 +0200 Subject: [PATCH 050/326] WIP: Add ButtonSide element --- crates/search/Cargo.toml | 4 +- crates/search/src/project_search.rs | 172 ++++++++++++++++++++++++++-- styles/src/style_tree/search.ts | 6 +- 3 files changed, 165 insertions(+), 17 deletions(-) diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 3a59f9a5cd..64421f5431 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -30,11 +30,11 @@ serde_derive.workspace = true smallvec.workspace = true smol.workspace = true globset.workspace = true - +serde_json.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -serde_json.workspace = true + workspace = { path = "../workspace", features = ["test-support"] } unindent.workspace = true diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 44586b6102..4d86134a1f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -10,6 +10,10 @@ use editor::{ }; use futures::StreamExt; use globset::{Glob, GlobMatcher}; +use gpui::color::Color; +use gpui::geometry::rect::RectF; +use gpui::json::{self, ToJson}; +use gpui::SceneBuilder; use gpui::{ actions, elements::*, @@ -17,6 +21,7 @@ use gpui::{ Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; +use gpui::{scene::Path, Border, LayoutContext}; use menu::Confirm; use postage::stream::Stream; use project::{search::SearchQuery, Entry, Project}; @@ -958,6 +963,113 @@ impl Default for ProjectSearchBar { Self::new() } } +type CreatePath = fn(RectF, Color) -> Path; + +pub struct ButtonSide { + color: Color, + factory: CreatePath, +} + +impl ButtonSide { + fn new(color: Color, factory: CreatePath) -> Self { + Self { color, factory } + } + pub fn left(color: Color) -> Self { + Self::new(color, left_button_side) + } + pub fn right(color: Color) -> Self { + Self::new(color, right_button_side) + } +} + +fn left_button_side(bounds: RectF, color: Color) -> Path { + use gpui::geometry::PathBuilder; + let mut path = PathBuilder::new(); + path.reset(bounds.lower_right()); + path.line_to(bounds.upper_right()); + let mut middle_point = bounds.origin(); + let distance_to_line = (middle_point.y() - bounds.lower_left().y()) / 4.; + middle_point.set_y(middle_point.y() - distance_to_line); + path.curve_to(middle_point, bounds.origin()); + let mut target = bounds.lower_left(); + target.set_y(target.y() + distance_to_line); + path.line_to(target); + //path.curve_to(bounds.lower_right(), bounds.upper_right()); + path.curve_to(bounds.lower_right(), bounds.lower_left()); + path.build(color, None) +} + +fn right_button_side(bounds: RectF, color: Color) -> Path { + use gpui::geometry::PathBuilder; + let mut path = PathBuilder::new(); + path.reset(bounds.lower_left()); + path.line_to(bounds.origin()); + let mut middle_point = bounds.upper_right(); + let distance_to_line = (middle_point.y() - bounds.lower_right().y()) / 4.; + middle_point.set_y(middle_point.y() - distance_to_line); + path.curve_to(middle_point, bounds.upper_right()); + let mut target = bounds.lower_right(); + target.set_y(target.y() + distance_to_line); + path.line_to(target); + //path.curve_to(bounds.lower_right(), bounds.upper_right()); + path.curve_to(bounds.lower_left(), bounds.lower_right()); + path.build(color, None) +} + +impl Element for ButtonSide { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + _: &mut ProjectSearchBar, + _: &mut LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + scene: &mut SceneBuilder, + bounds: RectF, + _: RectF, + _: &mut Self::LayoutState, + _: &mut ProjectSearchBar, + _: &mut ViewContext, + ) -> Self::PaintState { + scene.push_path((self.factory)(bounds, self.color)); + } + + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &ProjectSearchBar, + _: &ViewContext, + ) -> Option { + None + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &ProjectSearchBar, + _: &ViewContext, + ) -> gpui::json::Value { + json::json!({ + "type": "ButtonSide", + "bounds": bounds.to_json(), + "color": self.color.to_json(), + }) + } +} impl ProjectSearchBar { pub fn new() -> Self { @@ -1276,14 +1388,32 @@ impl ProjectSearchBar { let option = SearchOptions::REGEX; MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme + let mut style = theme .search .option_button .in_state(is_active) - .style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) + .style_for(state) + .clone(); + style.container.border.right = false; + Flex::row() + .with_child( + Label::new(icon, style.text.clone()) + .contained() + .with_style(style.container), + ) + .with_child( + ButtonSide::right( + style + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .contained() + .constrained() + .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) + .aligned() + .bottom(), + ) }) .on_click(MouseButton::Left, move |_, this, cx| { this.toggle_search_option(option, cx); @@ -1347,14 +1477,32 @@ impl ProjectSearchBar { enum NormalSearchTag {} MouseEventHandler::::new(region_id, cx, |state, cx| { let theme = theme::current(cx); - let style = theme + let mut style = theme .search .option_button .in_state(is_active) - .style_for(state); - Label::new("Text", style.text.clone()) - .contained() - .with_style(style.container) + .style_for(state) + .clone(); + style.container.border.left = false; + Flex::row() + .with_child( + ButtonSide::left( + style + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .contained() + .constrained() + .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) + .aligned() + .bottom(), + ) + .with_child( + Label::new("Text", style.text.clone()) + .contained() + .with_style(style.container), + ) }) .on_click(MouseButton::Left, move |_, this, cx| { if let Some(search) = this.active_project_search.as_mut() { @@ -1592,7 +1740,9 @@ impl View for ProjectSearchBar { .with_child(normal_search) .with_children(semantic_index) .with_child(regex_button) - .flex(1., true) + .constrained() + .with_height(theme.workspace.toolbar.height) + .contained() .aligned() .right(), ), diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 94b52aa664..356cd77f97 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -44,11 +44,9 @@ export default function search(): any { base: { ...text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - corner_radius: 2, + border: border(theme.highest, "on"), - margin: { - right: 2, - }, + padding: { bottom: 6, left: 6, From 0253ff304371a6737747a1183bcfcc6f69f5b5c9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:52:02 +0200 Subject: [PATCH 051/326] Touch up rounding on the sides of mode switcher --- crates/search/src/project_search.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4d86134a1f..9550ca6e89 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1395,6 +1395,7 @@ impl ProjectSearchBar { .style_for(state) .clone(); style.container.border.right = false; + Flex::row() .with_child( Label::new(icon, style.text.clone()) @@ -1410,7 +1411,7 @@ impl ProjectSearchBar { ) .contained() .constrained() - .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) + .with_max_width(theme.titlebar.avatar_ribbon.width / 3.) .aligned() .bottom(), ) @@ -1494,7 +1495,7 @@ impl ProjectSearchBar { ) .contained() .constrained() - .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) + .with_max_width(theme.titlebar.avatar_ribbon.width / 3.) .aligned() .bottom(), ) From d39585b24040a457df95884782a743135ec85299 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:05:09 +0200 Subject: [PATCH 052/326] Fix query editor 'floating' when filters are enabled --- crates/search/src/project_search.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 9550ca6e89..49780e53ef 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1733,6 +1733,8 @@ impl View for ProjectSearchBar { .with_children(filters) .contained() .with_style(theme.search.container) + .aligned() + .top() .flex(2., true), ) .with_child( From 5f6535e92bccc248267dc2f227f6d87280b695ae Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 2 Aug 2023 15:06:46 +0100 Subject: [PATCH 053/326] TEMP --- crates/editor/src/element.rs | 1 + crates/vim/src/visual.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 98bf41cdae..bc6c2f0bb8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -865,6 +865,7 @@ impl EditorElement { layout.position_map.snapshot.line_len(cursor_row) as usize; } } + dbg!(selection.head, cursor_row, cursor_column); if layout.visible_display_row_range.contains(&cursor_row) { let cursor_row_layout = &layout.position_map.line_layouts diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 8338d32de4..1716e2d1a5 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -67,7 +67,9 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // ensure the current character is included in the selection. if !selection.reversed { + // TODO: maybe try clipping left for multi-buffers let next_point = movement::right(map, selection.end); + if !(next_point.column() == 0 && next_point == map.max_point()) { selection.end = movement::right(map, selection.end) } From e0eaf23c28a928428bda7243fb92413803e612fb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:33:35 +0200 Subject: [PATCH 054/326] Add borders to button sides --- crates/search/src/project_search.rs | 51 ++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 49780e53ef..95d335ea31 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -12,6 +12,7 @@ use futures::StreamExt; use globset::{Glob, GlobMatcher}; use gpui::color::Color; use gpui::geometry::rect::RectF; +use gpui::geometry::vector::IntoVector2F; use gpui::json::{self, ToJson}; use gpui::SceneBuilder; use gpui::{ @@ -964,24 +965,46 @@ impl Default for ProjectSearchBar { } } type CreatePath = fn(RectF, Color) -> Path; - +type AdjustBorder = fn(RectF, f32) -> RectF; pub struct ButtonSide { color: Color, factory: CreatePath, + border_adjustment: AdjustBorder, + border: Option<(f32, Color)>, } impl ButtonSide { - fn new(color: Color, factory: CreatePath) -> Self { - Self { color, factory } + fn new(color: Color, factory: CreatePath, border_adjustment: AdjustBorder) -> Self { + Self { + color, + factory, + border_adjustment, + border: None, + } + } + pub fn with_border(mut self, width: f32, color: Color) -> Self { + self.border = Some((width, color)); + self } pub fn left(color: Color) -> Self { - Self::new(color, left_button_side) + Self::new(color, left_button_side, left_button_border_adjust) } pub fn right(color: Color) -> Self { - Self::new(color, right_button_side) + Self::new(color, right_button_side, right_button_border_adjust) } } - +fn left_button_border_adjust(bounds: RectF, width: f32) -> RectF { + let width = width.into_vector_2f(); + let mut lower_right = bounds.clone().lower_right(); + lower_right.set_x(lower_right.x() + width.x()); + RectF::from_points(bounds.origin() + width, lower_right) +} +fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { + let width = width.into_vector_2f(); + let mut origin = bounds.clone().origin(); + origin.set_x(origin.x() - width.x()); + RectF::from_points(origin, bounds.lower_right() - width) +} fn left_button_side(bounds: RectF, color: Color) -> Path { use gpui::geometry::PathBuilder; let mut path = PathBuilder::new(); @@ -994,7 +1017,6 @@ fn left_button_side(bounds: RectF, color: Color) -> Path { let mut target = bounds.lower_left(); target.set_y(target.y() + distance_to_line); path.line_to(target); - //path.curve_to(bounds.lower_right(), bounds.upper_right()); path.curve_to(bounds.lower_right(), bounds.lower_left()); path.build(color, None) } @@ -1011,7 +1033,6 @@ fn right_button_side(bounds: RectF, color: Color) -> Path { let mut target = bounds.lower_right(); target.set_y(target.y() + distance_to_line); path.line_to(target); - //path.curve_to(bounds.lower_right(), bounds.upper_right()); path.curve_to(bounds.lower_left(), bounds.lower_right()); path.build(color, None) } @@ -1039,6 +1060,11 @@ impl Element for ButtonSide { _: &mut ProjectSearchBar, _: &mut ViewContext, ) -> Self::PaintState { + let mut bounds = bounds; + if let Some((border_width, border_color)) = self.border.as_ref() { + scene.push_path((self.factory)(bounds, border_color.clone())); + bounds = (self.border_adjustment)(bounds, *border_width); + }; scene.push_path((self.factory)(bounds, self.color)); } @@ -1409,9 +1435,10 @@ impl ProjectSearchBar { .background_color .unwrap_or_else(gpui::color::Color::transparent_black), ) + .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.titlebar.avatar_ribbon.width / 3.) + .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) .aligned() .bottom(), ) @@ -1493,9 +1520,10 @@ impl ProjectSearchBar { .background_color .unwrap_or_else(gpui::color::Color::transparent_black), ) + .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.titlebar.avatar_ribbon.width / 3.) + .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) .aligned() .bottom(), ) @@ -1747,7 +1775,8 @@ impl View for ProjectSearchBar { .with_height(theme.workspace.toolbar.height) .contained() .aligned() - .right(), + .right() + .flex(1., true), ), ) .contained() From caaa4b1618a9936253b687aadcbeaa9a71843b64 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 2 Aug 2023 11:23:55 -0400 Subject: [PATCH 055/326] add theme for search mode option buttons, and adjust padding Co-authored-by: Piotr --- crates/search/src/project_search.rs | 74 +++++++++++------------------ crates/theme/src/theme.rs | 2 + styles/src/style_tree/search.ts | 46 +++++++++++++++++- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 95d335ea31..62ad328e02 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -12,7 +12,6 @@ use futures::StreamExt; use globset::{Glob, GlobMatcher}; use gpui::color::Color; use gpui::geometry::rect::RectF; -use gpui::geometry::vector::IntoVector2F; use gpui::json::{self, ToJson}; use gpui::SceneBuilder; use gpui::{ @@ -965,46 +964,24 @@ impl Default for ProjectSearchBar { } } type CreatePath = fn(RectF, Color) -> Path; -type AdjustBorder = fn(RectF, f32) -> RectF; + pub struct ButtonSide { color: Color, factory: CreatePath, - border_adjustment: AdjustBorder, - border: Option<(f32, Color)>, } impl ButtonSide { - fn new(color: Color, factory: CreatePath, border_adjustment: AdjustBorder) -> Self { - Self { - color, - factory, - border_adjustment, - border: None, - } - } - pub fn with_border(mut self, width: f32, color: Color) -> Self { - self.border = Some((width, color)); - self + fn new(color: Color, factory: CreatePath) -> Self { + Self { color, factory } } pub fn left(color: Color) -> Self { - Self::new(color, left_button_side, left_button_border_adjust) + Self::new(color, left_button_side) } pub fn right(color: Color) -> Self { - Self::new(color, right_button_side, right_button_border_adjust) + Self::new(color, right_button_side) } } -fn left_button_border_adjust(bounds: RectF, width: f32) -> RectF { - let width = width.into_vector_2f(); - let mut lower_right = bounds.clone().lower_right(); - lower_right.set_x(lower_right.x() + width.x()); - RectF::from_points(bounds.origin() + width, lower_right) -} -fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { - let width = width.into_vector_2f(); - let mut origin = bounds.clone().origin(); - origin.set_x(origin.x() - width.x()); - RectF::from_points(origin, bounds.lower_right() - width) -} + fn left_button_side(bounds: RectF, color: Color) -> Path { use gpui::geometry::PathBuilder; let mut path = PathBuilder::new(); @@ -1017,6 +994,7 @@ fn left_button_side(bounds: RectF, color: Color) -> Path { let mut target = bounds.lower_left(); target.set_y(target.y() + distance_to_line); path.line_to(target); + //path.curve_to(bounds.lower_right(), bounds.upper_right()); path.curve_to(bounds.lower_right(), bounds.lower_left()); path.build(color, None) } @@ -1033,6 +1011,7 @@ fn right_button_side(bounds: RectF, color: Color) -> Path { let mut target = bounds.lower_right(); target.set_y(target.y() + distance_to_line); path.line_to(target); + //path.curve_to(bounds.lower_right(), bounds.upper_right()); path.curve_to(bounds.lower_left(), bounds.lower_right()); path.build(color, None) } @@ -1060,11 +1039,6 @@ impl Element for ButtonSide { _: &mut ProjectSearchBar, _: &mut ViewContext, ) -> Self::PaintState { - let mut bounds = bounds; - if let Some((border_width, border_color)) = self.border.as_ref() { - scene.push_path((self.factory)(bounds, border_color.clone())); - bounds = (self.border_adjustment)(bounds, *border_width); - }; scene.push_path((self.factory)(bounds, self.color)); } @@ -1416,12 +1390,14 @@ impl ProjectSearchBar { let theme = theme::current(cx); let mut style = theme .search - .option_button + .mode_button .in_state(is_active) .style_for(state) .clone(); style.container.border.right = false; - + style.container.padding.right -= theme.search.mode_filling_width; + style.container.corner_radius = 0.; + debug_assert!(style.container.padding.right >= 0.); Flex::row() .with_child( Label::new(icon, style.text.clone()) @@ -1435,10 +1411,9 @@ impl ProjectSearchBar { .background_color .unwrap_or_else(gpui::color::Color::transparent_black), ) - .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) + .with_max_width(theme.search.mode_filling_width) .aligned() .bottom(), ) @@ -1470,11 +1445,15 @@ impl ProjectSearchBar { MouseEventHandler::::new(region_id, cx, |state, cx| { let theme = theme::current(cx); - let style = theme + let mut style = theme .search - .option_button + .mode_button .in_state(is_active) - .style_for(state); + .style_for(state) + .clone(); + + style.container.corner_radius = 0.; + Label::new("Semantic", style.text.clone()) .contained() .with_style(style.container) @@ -1507,11 +1486,14 @@ impl ProjectSearchBar { let theme = theme::current(cx); let mut style = theme .search - .option_button + .mode_button .in_state(is_active) .style_for(state) .clone(); style.container.border.left = false; + style.container.padding.left -= theme.search.mode_filling_width; + debug_assert!(style.container.padding.left >= 0.); + style.container.corner_radius = 0.; Flex::row() .with_child( ButtonSide::left( @@ -1520,10 +1502,9 @@ impl ProjectSearchBar { .background_color .unwrap_or_else(gpui::color::Color::transparent_black), ) - .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.titlebar.avatar_ribbon.width / 2.) + .with_max_width(theme.search.mode_filling_width) .aligned() .bottom(), ) @@ -1768,15 +1749,16 @@ impl View for ProjectSearchBar { .with_child( Flex::column().with_child( Flex::row() + .align_children_center() .with_child(normal_search) .with_children(semantic_index) .with_child(regex_button) .constrained() .with_height(theme.workspace.toolbar.height) .contained() + .with_style(theme.search.container) .aligned() - .right() - .flex(1., true), + .right(), ), ) .contained() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b3e65d9fda..c3b3502770 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -387,6 +387,8 @@ pub struct Search { pub minor_results_status: TextStyle, pub dismiss_button: Interactive, pub editor_icon: IconStyle, + pub mode_button: Toggleable>, + pub mode_filling_width: f32, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 356cd77f97..370d12c6f0 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -178,6 +178,50 @@ export default function search(): any { margin: { right: 6 }, padding: { left: 4 } } - } + }, + mode_button: toggleable({ + base: interactive({ + base: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + + border: border(theme.highest, "on"), + + padding: { + bottom: 6, + left: 10, + right: 10, + top: 6, + }, + corner_radius: 2, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }), + state: { + active: { + default: { + ...text(theme.highest, "mono", "accent"), + }, + hovered: { + ...text(theme.highest, "mono", "accent", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "accent", "pressed"), + }, + }, + }, + }), + mode_filling_width: 4.0, } } From 5b30caa33301974e14aafd61b64e0786343ff476 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 2 Aug 2023 16:33:35 +0200 Subject: [PATCH 056/326] Add borders to button sides --- crates/search/src/project_search.rs | 47 +++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 62ad328e02..b248de412d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -12,6 +12,7 @@ use futures::StreamExt; use globset::{Glob, GlobMatcher}; use gpui::color::Color; use gpui::geometry::rect::RectF; +use gpui::geometry::vector::IntoVector2F; use gpui::json::{self, ToJson}; use gpui::SceneBuilder; use gpui::{ @@ -964,24 +965,46 @@ impl Default for ProjectSearchBar { } } type CreatePath = fn(RectF, Color) -> Path; - +type AdjustBorder = fn(RectF, f32) -> RectF; pub struct ButtonSide { color: Color, factory: CreatePath, + border_adjustment: AdjustBorder, + border: Option<(f32, Color)>, } impl ButtonSide { - fn new(color: Color, factory: CreatePath) -> Self { - Self { color, factory } + fn new(color: Color, factory: CreatePath, border_adjustment: AdjustBorder) -> Self { + Self { + color, + factory, + border_adjustment, + border: None, + } + } + pub fn with_border(mut self, width: f32, color: Color) -> Self { + self.border = Some((width, color)); + self } pub fn left(color: Color) -> Self { - Self::new(color, left_button_side) + Self::new(color, left_button_side, left_button_border_adjust) } pub fn right(color: Color) -> Self { - Self::new(color, right_button_side) + Self::new(color, right_button_side, right_button_border_adjust) } } - +fn left_button_border_adjust(bounds: RectF, width: f32) -> RectF { + let width = width.into_vector_2f(); + let mut lower_right = bounds.clone().lower_right(); + lower_right.set_x(lower_right.x() + width.x()); + RectF::from_points(bounds.origin() + width, lower_right) +} +fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { + let width = width.into_vector_2f(); + let mut origin = bounds.clone().origin(); + origin.set_x(origin.x() - width.x()); + RectF::from_points(origin, bounds.lower_right() - width) +} fn left_button_side(bounds: RectF, color: Color) -> Path { use gpui::geometry::PathBuilder; let mut path = PathBuilder::new(); @@ -994,7 +1017,6 @@ fn left_button_side(bounds: RectF, color: Color) -> Path { let mut target = bounds.lower_left(); target.set_y(target.y() + distance_to_line); path.line_to(target); - //path.curve_to(bounds.lower_right(), bounds.upper_right()); path.curve_to(bounds.lower_right(), bounds.lower_left()); path.build(color, None) } @@ -1011,7 +1033,6 @@ fn right_button_side(bounds: RectF, color: Color) -> Path { let mut target = bounds.lower_right(); target.set_y(target.y() + distance_to_line); path.line_to(target); - //path.curve_to(bounds.lower_right(), bounds.upper_right()); path.curve_to(bounds.lower_left(), bounds.lower_right()); path.build(color, None) } @@ -1039,6 +1060,11 @@ impl Element for ButtonSide { _: &mut ProjectSearchBar, _: &mut ViewContext, ) -> Self::PaintState { + let mut bounds = bounds; + if let Some((border_width, border_color)) = self.border.as_ref() { + scene.push_path((self.factory)(bounds, border_color.clone())); + bounds = (self.border_adjustment)(bounds, *border_width); + }; scene.push_path((self.factory)(bounds, self.color)); } @@ -1411,6 +1437,7 @@ impl ProjectSearchBar { .background_color .unwrap_or_else(gpui::color::Color::transparent_black), ) + .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() .with_max_width(theme.search.mode_filling_width) @@ -1502,6 +1529,7 @@ impl ProjectSearchBar { .background_color .unwrap_or_else(gpui::color::Color::transparent_black), ) + .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() .with_max_width(theme.search.mode_filling_width) @@ -1758,7 +1786,8 @@ impl View for ProjectSearchBar { .contained() .with_style(theme.search.container) .aligned() - .right(), + .right() + .flex(1., true), ), ) .contained() From 61a6892b8cdc95fcb1b4ef1a0f64436a06263492 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 1 Aug 2023 19:17:51 -0700 Subject: [PATCH 057/326] WIP: Broadcast room updates to channel members --- crates/collab/src/db.rs | 102 +++++++++++++++++++++++++++------------ crates/collab/src/rpc.rs | 28 +++++++++-- 2 files changed, 95 insertions(+), 35 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 58607836cc..1a89978c38 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1178,7 +1178,7 @@ impl Database { user_id: UserId, connection: ConnectionId, live_kit_room: &str, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { let room = room::ActiveModel { live_kit_room: ActiveValue::set(live_kit_room.into()), @@ -1217,7 +1217,7 @@ impl Database { calling_connection: ConnectionId, called_user_id: UserId, initial_project_id: Option, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { room_participant::ActiveModel { room_id: ActiveValue::set(room_id), @@ -1246,7 +1246,7 @@ impl Database { &self, room_id: RoomId, called_user_id: UserId, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { room_participant::Entity::delete_many() .filter( @@ -1266,7 +1266,7 @@ impl Database { &self, expected_room_id: Option, user_id: UserId, - ) -> Result>> { + ) -> Result>> { self.optional_room_transaction(|tx| async move { let mut filter = Condition::all() .add(room_participant::Column::UserId.eq(user_id)) @@ -1303,7 +1303,7 @@ impl Database { room_id: RoomId, calling_connection: ConnectionId, called_user_id: UserId, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { let participant = room_participant::Entity::find() .filter( @@ -1340,7 +1340,7 @@ impl Database { user_id: UserId, channel_id: Option, connection: ConnectionId, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { if let Some(channel_id) = channel_id { channel_member::Entity::find() @@ -1868,7 +1868,7 @@ impl Database { project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { follower::ActiveModel { @@ -1898,7 +1898,7 @@ impl Database { project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { follower::Entity::delete_many() @@ -1930,7 +1930,7 @@ impl Database { room_id: RoomId, connection: ConnectionId, location: proto::ParticipantLocation, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async { let tx = tx; let location_kind; @@ -2043,7 +2043,7 @@ impl Database { }) } - async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { + async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { let db_room = room::Entity::find_by_id(room_id) .one(tx) .await? @@ -2147,12 +2147,22 @@ impl Database { }); } - Ok(proto::Room { - id: db_room.id.to_proto(), - live_kit_room: db_room.live_kit_room, - participants: participants.into_values().collect(), - pending_participants, - followers, + let channel_users = + if let Some(channel) = db_room.find_related(channel::Entity).one(tx).await? { + self.get_channel_members_internal(channel.id, tx).await? + } else { + Vec::new() + }; + + Ok(ChannelRoom { + room: proto::Room { + id: db_room.id.to_proto(), + live_kit_room: db_room.live_kit_room, + participants: participants.into_values().collect(), + pending_participants, + followers, + }, + channel_participants: channel_users, }) } @@ -2183,7 +2193,7 @@ impl Database { room_id: RoomId, connection: ConnectionId, worktrees: &[proto::WorktreeMetadata], - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { let participant = room_participant::Entity::find() .filter( @@ -2254,7 +2264,7 @@ impl Database { &self, project_id: ProjectId, connection: ConnectionId, - ) -> Result)>> { + ) -> Result)>> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; @@ -2281,7 +2291,7 @@ impl Database { project_id: ProjectId, connection: ConnectionId, worktrees: &[proto::WorktreeMetadata], - ) -> Result)>> { + ) -> Result)>> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let project = project::Entity::find_by_id(project_id) @@ -2858,7 +2868,7 @@ impl Database { &self, project_id: ProjectId, connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let result = project_collaborator::Entity::delete_many() @@ -3377,20 +3387,29 @@ impl Database { pub async fn get_channel_members(&self, id: ChannelId) -> Result> { self.transaction(|tx| async move { let tx = tx; - let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?; - let user_ids = channel_member::Entity::find() - .distinct() - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) - .select_only() - .column(channel_member::Column::UserId) - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; + let user_ids = self.get_channel_members_internal(id, &*tx).await?; Ok(user_ids) }) .await } + pub async fn get_channel_members_internal( + &self, + id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let ancestor_ids = self.get_channel_ancestors(id, tx).await?; + let user_ids = channel_member::Entity::find() + .distinct() + .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + Ok(user_ids) + } + async fn get_channel_ancestors( &self, id: ChannelId, @@ -3913,8 +3932,27 @@ id_type!(ServerId); id_type!(SignupId); id_type!(UserId); -pub struct RejoinedRoom { +pub struct ChannelRoom { pub room: proto::Room, + pub channel_participants: Vec, +} + +impl Deref for ChannelRoom { + type Target = proto::Room; + + fn deref(&self) -> &Self::Target { + &self.room + } +} + +impl DerefMut for ChannelRoom { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.room + } +} + +pub struct RejoinedRoom { + pub room: ChannelRoom, pub rejoined_projects: Vec, pub reshared_projects: Vec, } @@ -3951,14 +3989,14 @@ pub struct RejoinedWorktree { } pub struct LeftRoom { - pub room: proto::Room, + pub room: ChannelRoom, pub left_projects: HashMap, pub canceled_calls_to_user_ids: Vec, pub deleted: bool, } pub struct RefreshedRoom { - pub room: proto::Room, + pub room: ChannelRoom, pub stale_participant_user_ids: Vec, pub canceled_calls_to_user_ids: Vec, } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index eaa3eb8261..4d30d17485 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,7 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{self, ChannelId, ChannelRoom, Database, ProjectId, RoomId, ServerId, User, UserId}, executor::Executor, AppState, Result, }; @@ -2426,7 +2426,10 @@ fn contact_for_user( } } -fn room_updated(room: &proto::Room, peer: &Peer) { +fn room_updated(room: &ChannelRoom, peer: &Peer, pool: &ConnectionPool) { + let channel_ids = &room.channel_participants; + let room = &room.room; + broadcast( None, room.participants @@ -2441,6 +2444,21 @@ fn room_updated(room: &proto::Room, peer: &Peer) { ) }, ); + + broadcast( + None, + channel_ids + .iter() + .flat_map(|user_id| pool.user_connection_ids(*user_id)), + |peer_id| { + peer.send( + peer_id.into(), + proto::RoomUpdated { + room: Some(room.clone()), + }, + ) + }, + ); } async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> { @@ -2491,7 +2509,11 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { project_left(project, session); } - room_updated(&left_room.room, &session.peer); + { + let connection_pool = session.connection_pool().await; + room_updated(&left_room.room, &session.peer, &connection_pool); + } + room_id = RoomId::from_proto(left_room.room.id); canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids); live_kit_room = mem::take(&mut left_room.room.live_kit_room); From 7b43b0d4f117b3430b3302e784e4f0050854e141 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 2 Aug 2023 12:29:19 -0400 Subject: [PATCH 058/326] refactored search mode to ensure state is consistent Co-authored-by: Piotr --- crates/search/src/project_search.rs | 289 +++++++++++++++++++++++++--- 1 file changed, 257 insertions(+), 32 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b248de412d..b25defe4fe 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -51,7 +51,10 @@ actions!( ToggleFocus, NextField, ToggleSemanticSearch, - CycleMode + CycleMode, + ActivateTextMode, + ActivateSemanticMode, + ActivateRegexMode ] ); @@ -134,6 +137,68 @@ enum SearchMode { Regex, } +#[derive(Copy, Clone, Debug, PartialEq)] +enum Side { + Left, + Right, +} + +impl SearchMode { + fn label(&self) -> &'static str { + match self { + SearchMode::Text => "Text", + SearchMode::Semantic => "Semantic", + SearchMode::Regex => "Regex", + } + } + + fn region_id(&self) -> usize { + match self { + SearchMode::Text => 3, + SearchMode::Semantic => 4, + SearchMode::Regex => 5, + } + } + + fn tooltip_text(&self) -> &'static str { + match self { + SearchMode::Text => "Activate Text Search", + SearchMode::Semantic => "Activate Semantic Search", + SearchMode::Regex => "Activate Regex Search", + } + } + + fn activate_action(&self) -> Box { + match self { + SearchMode::Text => Box::new(ActivateTextMode), + SearchMode::Semantic => Box::new(ActivateSemanticMode), + SearchMode::Regex => Box::new(ActivateRegexMode), + } + } + + fn border_left(&self) -> bool { + match self { + SearchMode::Text => false, + _ => true, + } + } + + fn border_right(&self) -> bool { + match self { + SearchMode::Regex => false, + _ => true, + } + } + + fn button_side(&self) -> Option { + match self { + SearchMode::Text => Some(Side::Left), + SearchMode::Semantic => None, + SearchMode::Regex => Some(Side::Right), + } + } +} + pub struct ProjectSearchBar { active_project_search: Option>, subscription: Option, @@ -560,6 +625,68 @@ impl Item for ProjectSearchView { } impl ProjectSearchView { + fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + self.current_mode = mode; + + match mode { + SearchMode::Semantic => { + if let Some(semantic_index) = SemanticIndex::global(cx) { + // Semantic search uses no options + self.search_options = SearchOptions::none(); + + let project = self.model.read(cx).project.clone(); + let index_task = semantic_index.update(cx, |semantic_index, cx| { + semantic_index.index_project(project, cx) + }); + + cx.spawn(|search_view, mut cx| async move { + let (files_to_index, mut files_remaining_rx) = index_task.await?; + + search_view.update(&mut cx, |search_view, cx| { + cx.notify(); + search_view.semantic = Some(SemanticSearchState { + file_count: files_to_index, + outstanding_file_count: files_to_index, + _progress_task: cx.spawn(|search_view, mut cx| async move { + while let Some(count) = files_remaining_rx.recv().await { + search_view + .update(&mut cx, |search_view, cx| { + if let Some(semantic_search_state) = + &mut search_view.semantic + { + semantic_search_state.outstanding_file_count = + count; + cx.notify(); + if count == 0 { + return; + } + } + }) + .ok(); + } + }), + }); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + SearchMode::Regex => { + if !self.is_option_enabled(SearchOptions::REGEX) { + self.toggle_search_option(SearchOptions::REGEX, cx); + } + self.semantic = None; + } + SearchMode::Text => { + if self.is_option_enabled(SearchOptions::REGEX) { + self.toggle_search_option(SearchOptions::REGEX, cx); + } + self.semantic = None; + } + } + cx.notify(); + } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { let project; let excerpts; @@ -739,24 +866,29 @@ impl ProjectSearchView { } fn search(&mut self, cx: &mut ViewContext) { - if let Some(semantic) = &mut self.semantic { - if semantic.outstanding_file_count > 0 { - return; - } + let mode = self.current_mode; + match mode { + SearchMode::Semantic => { + if let Some(semantic) = &mut self.semantic { + if semantic.outstanding_file_count > 0 { + return; + } - let query = self.query_editor.read(cx).text(cx); - if let Some((included_files, exclude_files)) = - self.get_included_and_excluded_globsets(cx) - { - self.model.update(cx, |model, cx| { - model.semantic_search(query, included_files, exclude_files, cx) - }); + let query = self.query_editor.read(cx).text(cx); + if let Some((included_files, exclude_files)) = + self.get_included_and_excluded_globsets(cx) + { + self.model.update(cx, |model, cx| { + model.semantic_search(query, included_files, exclude_files, cx) + }); + } + } + } + _ => { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } } - return; - } - - if let Some(query) = self.build_search_query(cx) { - self.model.update(cx, |model, cx| model.search(query, cx)); } } @@ -791,7 +923,10 @@ impl ProjectSearchView { Some((included_files, excluded_files)) } - + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(option); + self.semantic = None; + } fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { let text = self.query_editor.read(cx).text(cx); let included_files = @@ -957,6 +1092,9 @@ impl ProjectSearchView { cx.propagate_action(); } + fn is_option_enabled(&self, option: SearchOptions) -> bool { + self.search_options.contains(option) + } } impl Default for ProjectSearchBar { @@ -1117,12 +1255,12 @@ impl ProjectSearchBar { SearchMode::Regex }; - this.current_mode = match mode { + let new_mode = match mode { &SearchMode::Text => next_text_state, &SearchMode::Semantic => SearchMode::Regex, SearchMode::Regex => SearchMode::Text, }; - cx.notify(); + this.activate_search_mode(new_mode, cx); }) } } @@ -1235,12 +1373,7 @@ impl ProjectSearchBar { fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - search_view.search_options.toggle(option); - if option.contains(SearchOptions::REGEX) { - search_view.current_mode = SearchMode::Regex; - } - search_view.semantic = None; - search_view.search(cx); + search_view.toggle_search_option(option, cx); }); cx.notify(); true @@ -1248,6 +1381,7 @@ impl ProjectSearchBar { false } } + fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { @@ -1403,6 +1537,98 @@ impl ProjectSearchBar { .into_any() } + fn render_search_mode_button( + &self, + mode: SearchMode, + cx: &mut ViewContext, + ) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + + enum SearchModeButton {} + MouseEventHandler::::new(mode.region_id(), cx, |state, cx| { + let theme = theme::current(cx); + let mut style = theme + .search + .mode_button + .in_state(is_active) + .style_for(state) + .clone(); + + let label = Label::new(mode.label(), style.text.clone()) + .contained() + .with_style(style.container); + + if let Some(button_side) = mode.button_side() { + style.container.border.left = mode.border_left(); + style.container.border.right = mode.border_right(); + + if button_side == Side::Left { + Flex::row() + .with_child( + ButtonSide::left( + style + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .with_child(label) + .into_any() + } else { + Flex::row() + .with_child(label) + .with_child( + ButtonSide::right( + style + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .into_any() + } + } else { + label.into_any() + } + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.activate_search_mode(mode, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + mode.region_id(), + mode.tooltip_text().to_owned(), + Some(mode.activate_action()), + tooltip_style, + cx, + ) + .into_any() + } + + fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { + // Update Current Mode + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.activate_search_mode(mode, cx); + }); + cx.notify(); + } + } + fn render_regex_button( &self, icon: &'static str, @@ -1561,6 +1787,7 @@ impl ProjectSearchBar { ) .into_any() } + fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { if let Some(search) = self.active_project_search.as_ref() { search.read(cx).search_options.contains(option) @@ -1609,7 +1836,6 @@ impl View for ProjectSearchBar { .aligned() .right() .flex(1.0, true); - let regex_button = self.render_regex_button("Regex", search.current_mode.clone(), cx); let row_spacing = theme.workspace.toolbar.container.padding.bottom; let search = _search.read(cx); let filter_button = { @@ -1726,9 +1952,8 @@ impl View for ProjectSearchBar { ) }); - let semantic_index = - SemanticIndex::enabled(cx).then(|| self.render_semantic_search_button(cx)); - let normal_search = self.render_text_search_button(cx); + let semantic_index = SemanticIndex::enabled(cx) + .then(|| self.render_search_mode_button(SearchMode::Semantic, cx)); Flex::row() .with_child( Flex::column() @@ -1778,9 +2003,9 @@ impl View for ProjectSearchBar { Flex::column().with_child( Flex::row() .align_children_center() - .with_child(normal_search) + .with_child(self.render_search_mode_button(SearchMode::Text, cx)) .with_children(semantic_index) - .with_child(regex_button) + .with_child(self.render_search_mode_button(SearchMode::Regex, cx)) .constrained() .with_height(theme.workspace.toolbar.height) .contained() From acf78f5fb4c2ba5a53dc508d86950fa49d98dc08 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 2 Aug 2023 14:18:28 -0400 Subject: [PATCH 059/326] add kill_search function to stop searching on mode change --- crates/search/src/project_search.rs | 237 ++-------------------------- 1 file changed, 13 insertions(+), 224 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b25defe4fe..ee12038e1c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,5 @@ use crate::{ - SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, - ToggleWholeWord, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use anyhow::Result; use collections::HashMap; @@ -22,7 +21,7 @@ use gpui::{ Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; -use gpui::{scene::Path, Border, LayoutContext}; +use gpui::{scene::Path, LayoutContext}; use menu::Confirm; use postage::stream::Stream; use project::{search::SearchQuery, Entry, Project}; @@ -50,7 +49,6 @@ actions!( SearchInNew, ToggleFocus, NextField, - ToggleSemanticSearch, CycleMode, ActivateTextMode, ActivateSemanticMode, @@ -74,7 +72,6 @@ pub fn init(cx: &mut AppContext) { cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); - add_toggle_option_action::(SearchOptions::REGEX, cx); } fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { @@ -234,6 +231,12 @@ impl ProjectSearch { }) } + fn kill_search(&mut self) { + self.active_query = None; + self.match_ranges.clear(); + self.pending_search = None; + } + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { let search = self .project @@ -626,6 +629,7 @@ impl Item for ProjectSearchView { impl ProjectSearchView { fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + self.model.update(cx, |model, _| model.kill_search()); self.current_mode = mode; match mode { @@ -674,13 +678,13 @@ impl ProjectSearchView { } SearchMode::Regex => { if !self.is_option_enabled(SearchOptions::REGEX) { - self.toggle_search_option(SearchOptions::REGEX, cx); + self.toggle_search_option(SearchOptions::REGEX); } self.semantic = None; } SearchMode::Text => { if self.is_option_enabled(SearchOptions::REGEX) { - self.toggle_search_option(SearchOptions::REGEX, cx); + self.toggle_search_option(SearchOptions::REGEX); } self.semantic = None; } @@ -923,7 +927,7 @@ impl ProjectSearchView { Some((included_files, excluded_files)) } - fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) { + fn toggle_search_option(&mut self, option: SearchOptions) { self.search_options.toggle(option); self.semantic = None; } @@ -1373,7 +1377,7 @@ impl ProjectSearchBar { fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - search_view.toggle_search_option(option, cx); + search_view.toggle_search_option(option); }); cx.notify(); true @@ -1403,62 +1407,6 @@ impl ProjectSearchBar { } } - fn toggle_semantic_search(&mut self, cx: &mut ViewContext) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - if search_view.semantic.is_some() { - search_view.semantic = None; - } else if let Some(semantic_index) = SemanticIndex::global(cx) { - search_view.current_mode = SearchMode::Semantic; - // TODO: confirm that it's ok to send this project - search_view.search_options = SearchOptions::none(); - - let project = search_view.model.read(cx).project.clone(); - let index_task = semantic_index.update(cx, |semantic_index, cx| { - semantic_index.index_project(project, cx) - }); - - cx.spawn(|search_view, mut cx| async move { - let (files_to_index, mut files_remaining_rx) = index_task.await?; - - search_view.update(&mut cx, |search_view, cx| { - cx.notify(); - search_view.semantic = Some(SemanticSearchState { - file_count: files_to_index, - outstanding_file_count: files_to_index, - _progress_task: cx.spawn(|search_view, mut cx| async move { - while let Some(count) = files_remaining_rx.recv().await { - search_view - .update(&mut cx, |search_view, cx| { - if let Some(semantic_search_state) = - &mut search_view.semantic - { - semantic_search_state.outstanding_file_count = - count; - cx.notify(); - if count == 0 { - return; - } - } - }) - .ok(); - } - }), - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - cx.notify(); - }); - cx.notify(); - true - } else { - false - } - } - fn render_nav_button( &self, icon: &'static str, @@ -1629,165 +1577,6 @@ impl ProjectSearchBar { } } - fn render_regex_button( - &self, - icon: &'static str, - current_mode: SearchMode, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = current_mode == SearchMode::Regex; //self.is_option_enabled(option, cx); - let option = SearchOptions::REGEX; - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { - let theme = theme::current(cx); - let mut style = theme - .search - .mode_button - .in_state(is_active) - .style_for(state) - .clone(); - style.container.border.right = false; - style.container.padding.right -= theme.search.mode_filling_width; - style.container.corner_radius = 0.; - debug_assert!(style.container.padding.right >= 0.); - Flex::row() - .with_child( - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container), - ) - .with_child( - ButtonSide::right( - style - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(theme.search.mode_filling_width) - .aligned() - .bottom(), - ) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_search_option(option, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - option.bits as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, - ) - .into_any() - } - - fn render_semantic_search_button(&self, cx: &mut ViewContext) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == SearchMode::Semantic - } else { - false - }; - - let region_id = 3; - - MouseEventHandler::::new(region_id, cx, |state, cx| { - let theme = theme::current(cx); - let mut style = theme - .search - .mode_button - .in_state(is_active) - .style_for(state) - .clone(); - - style.container.corner_radius = 0.; - - Label::new("Semantic", style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_semantic_search(cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - region_id, - format!("Toggle Semantic Search"), - Some(Box::new(ToggleSemanticSearch)), - tooltip_style, - cx, - ) - .into_any() - } - fn render_text_search_button(&self, cx: &mut ViewContext) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == SearchMode::Text - } else { - false - }; - - let region_id = 4; - enum NormalSearchTag {} - MouseEventHandler::::new(region_id, cx, |state, cx| { - let theme = theme::current(cx); - let mut style = theme - .search - .mode_button - .in_state(is_active) - .style_for(state) - .clone(); - style.container.border.left = false; - style.container.padding.left -= theme.search.mode_filling_width; - debug_assert!(style.container.padding.left >= 0.); - style.container.corner_radius = 0.; - Flex::row() - .with_child( - ButtonSide::left( - style - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(theme.search.mode_filling_width) - .aligned() - .bottom(), - ) - .with_child( - Label::new("Text", style.text.clone()) - .contained() - .with_style(style.container), - ) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |this, cx| { - this.semantic = None; - this.current_mode = SearchMode::Text; - cx.notify(); - }); - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - region_id, - format!("Toggle Normal Search"), - None, - tooltip_style, - cx, - ) - .into_any() - } - fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { if let Some(search) = self.active_project_search.as_ref() { search.read(cx).search_options.contains(option) From 71bbd5f2f6d86a4319d66699f763324d2693a106 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 2 Aug 2023 15:08:54 -0400 Subject: [PATCH 060/326] update keymaps for cycle search mode and toggle filters --- assets/keymaps/default.json | 9 ++++++--- crates/search/src/project_search.rs | 31 +++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a2be44cbce..590e0d6234 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -229,13 +229,15 @@ { "context": "ProjectSearchBar", "bindings": { - "escape": "project_search::ToggleFocus" + "escape": "project_search::ToggleFocus", + "alt-tab": "project_search::CycleMode", } }, { "context": "ProjectSearchView", "bindings": { - "escape": "project_search::ToggleFocus" + "escape": "project_search::ToggleFocus", + "alt-tab": "project_search::CycleMode" } }, { @@ -247,7 +249,8 @@ "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex" + "alt-cmd-r": "search::ToggleRegex", + "alt-cmd-f": "project_search::ToggleFilters" } }, // Bindings from VS Code diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ee12038e1c..d3cefa1af1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -50,6 +50,7 @@ actions!( ToggleFocus, NextField, CycleMode, + ToggleFilters, ActivateTextMode, ActivateSemanticMode, ActivateRegexMode @@ -72,6 +73,18 @@ pub fn init(cx: &mut AppContext) { cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_filters_action::(cx); +} + +fn add_toggle_filters_action(cx: &mut AppContext) { + cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) { + return; + } + } + cx.propagate_action(); + }); } fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { @@ -380,7 +393,7 @@ impl View for ProjectSearchView { let dots_count = semantic.outstanding_file_count % 3 + 1; let dots: String = std::iter::repeat('.').take(dots_count).collect(); format!( - "Indexing. {} of {}{dots}", + "Indexing: {} of {}{dots}", semantic.file_count - semantic.outstanding_file_count, semantic.file_count ) @@ -388,13 +401,13 @@ impl View for ProjectSearchView { "Indexing complete".to_string() } } else { - "This is an invalid state".to_string() + "Indexing: ...".to_string() }; let minor_text = match current_mode { SearchMode::Semantic => [ semantic_status, - "ex. list all available languages".to_owned(), + "ex. 'list all available languages'".to_owned(), ], _ => [ "Include/exclude specific paths with the filter option.".to_owned(), @@ -1396,8 +1409,6 @@ impl ProjectSearchBar { search_view .excluded_files_editor .update(cx, |_, cx| cx.notify()); - search_view.semantic = None; - search_view.search(cx); cx.notify(); }); cx.notify(); @@ -1518,6 +1529,7 @@ impl ProjectSearchBar { if button_side == Side::Left { Flex::row() + .align_children_center() .with_child( ButtonSide::left( style @@ -1534,6 +1546,7 @@ impl ProjectSearchBar { .into_any() } else { Flex::row() + .align_children_center() .with_child(label) .with_child( ButtonSide::right( @@ -1646,7 +1659,13 @@ impl View for ProjectSearchBar { this.toggle_filters(cx); }) .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(0, "Toggle filters".into(), None, tooltip_style, cx) + .with_tooltip::( + 0, + "Toggle filters".into(), + Some(Box::new(ToggleFilters)), + tooltip_style, + cx, + ) .into_any() }; let search = _search.read(cx); From a9de73739a1bdbd4e2bcbd0fd572d5ecc0f47de0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 12:15:06 -0700 Subject: [PATCH 061/326] WIP --- crates/client/src/channel_store.rs | 27 ++-- crates/collab/src/db.rs | 175 ++++++++++++++++------- crates/collab/src/db/room.rs | 2 +- crates/collab/src/db/tests.rs | 10 +- crates/collab/src/rpc.rs | 134 +++++++++++++---- crates/collab/src/tests/channel_tests.rs | 29 +++- crates/rpc/proto/zed.proto | 10 +- crates/rpc/src/proto.rs | 2 +- 8 files changed, 289 insertions(+), 100 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 99501bbd2a..5218c56891 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -1,13 +1,17 @@ use crate::{Client, Subscription, User, UserStore}; use anyhow::Result; +use collections::HashMap; use futures::Future; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; +type ChannelId = u64; + pub struct ChannelStore { channels: Vec>, channel_invitations: Vec>, + channel_participants: HashMap>, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -15,9 +19,9 @@ pub struct ChannelStore { #[derive(Clone, Debug, PartialEq)] pub struct Channel { - pub id: u64, + pub id: ChannelId, pub name: String, - pub parent_id: Option, + pub parent_id: Option, pub depth: usize, } @@ -37,6 +41,7 @@ impl ChannelStore { Self { channels: vec![], channel_invitations: vec![], + channel_participants: Default::default(), client, user_store, _rpc_subscription: rpc_subscription, @@ -51,15 +56,15 @@ impl ChannelStore { &self.channel_invitations } - pub fn channel_for_id(&self, channel_id: u64) -> Option> { + pub fn channel_for_id(&self, channel_id: ChannelId) -> Option> { self.channels.iter().find(|c| c.id == channel_id).cloned() } pub fn create_channel( &self, name: &str, - parent_id: Option, - ) -> impl Future> { + parent_id: Option, + ) -> impl Future> { let client = self.client.clone(); let name = name.to_owned(); async move { @@ -72,7 +77,7 @@ impl ChannelStore { pub fn invite_member( &self, - channel_id: u64, + channel_id: ChannelId, user_id: u64, admin: bool, ) -> impl Future> { @@ -91,7 +96,7 @@ impl ChannelStore { pub fn respond_to_channel_invite( &mut self, - channel_id: u64, + channel_id: ChannelId, accept: bool, ) -> impl Future> { let client = self.client.clone(); @@ -107,7 +112,7 @@ impl ChannelStore { false } - pub fn remove_channel(&self, channel_id: u64) -> impl Future> { + pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { let client = self.client.clone(); async move { client.request(proto::RemoveChannel { channel_id }).await?; @@ -117,7 +122,7 @@ impl ChannelStore { pub fn remove_member( &self, - channel_id: u64, + channel_id: ChannelId, user_id: u64, cx: &mut ModelContext, ) -> Task> { @@ -126,13 +131,13 @@ impl ChannelStore { pub fn channel_members( &self, - channel_id: u64, + channel_id: ChannelId, cx: &mut ModelContext, ) -> Task>>> { todo!() } - pub fn add_guest_channel(&self, channel_id: u64) -> Task> { + pub fn add_guest_channel(&self, channel_id: ChannelId) -> Task> { todo!() } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 1a89978c38..ad87266e7d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -212,7 +212,13 @@ impl Database { .map(|participant| participant.user_id), ); - let room = self.get_room(room_id, &tx).await?; + 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? + } else { + Vec::new() + }; + // Delete the room if it becomes empty. if room.participants.is_empty() { project::Entity::delete_many() @@ -224,6 +230,8 @@ impl Database { Ok(RefreshedRoom { room, + channel_id, + channel_members, stale_participant_user_ids, canceled_calls_to_user_ids, }) @@ -1178,7 +1186,7 @@ impl Database { user_id: UserId, connection: ConnectionId, live_kit_room: &str, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { let room = room::ActiveModel { live_kit_room: ActiveValue::set(live_kit_room.into()), @@ -1217,7 +1225,7 @@ impl Database { calling_connection: ConnectionId, called_user_id: UserId, initial_project_id: Option, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { room_participant::ActiveModel { room_id: ActiveValue::set(room_id), @@ -1246,7 +1254,7 @@ impl Database { &self, room_id: RoomId, called_user_id: UserId, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { room_participant::Entity::delete_many() .filter( @@ -1266,7 +1274,7 @@ impl Database { &self, expected_room_id: Option, user_id: UserId, - ) -> Result>> { + ) -> Result>> { self.optional_room_transaction(|tx| async move { let mut filter = Condition::all() .add(room_participant::Column::UserId.eq(user_id)) @@ -1303,7 +1311,7 @@ impl Database { room_id: RoomId, calling_connection: ConnectionId, called_user_id: UserId, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { let participant = room_participant::Entity::find() .filter( @@ -1340,7 +1348,7 @@ impl Database { user_id: UserId, channel_id: Option, connection: ConnectionId, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { if let Some(channel_id) = channel_id { channel_member::Entity::find() @@ -1396,7 +1404,16 @@ impl Database { } let room = self.get_room(room_id, &tx).await?; - Ok(room) + let channel_members = if let Some(channel_id) = channel_id { + self.get_channel_members_internal(channel_id, &tx).await? + } else { + Vec::new() + }; + Ok(JoinRoom { + room, + channel_id, + channel_members, + }) }) .await } @@ -1690,9 +1707,18 @@ impl Database { }); } - let room = self.get_room(room_id, &tx).await?; + 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? + } else { + Vec::new() + }; + Ok(RejoinedRoom { room, + channel_id, + channel_members, rejoined_projects, reshared_projects, }) @@ -1833,7 +1859,7 @@ impl Database { .exec(&*tx) .await?; - let room = self.get_room(room_id, &tx).await?; + let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; let deleted = if room.participants.is_empty() { let result = room::Entity::delete_by_id(room_id) .filter(room::Column::ChannelId.is_null()) @@ -1844,8 +1870,15 @@ impl Database { false }; + let channel_members = if let Some(channel_id) = channel_id { + self.get_channel_members_internal(channel_id, &tx).await? + } else { + Vec::new() + }; let left_room = LeftRoom { room, + channel_id, + channel_members, left_projects, canceled_calls_to_user_ids, deleted, @@ -1868,7 +1901,7 @@ impl Database { project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { follower::ActiveModel { @@ -1898,7 +1931,7 @@ impl Database { project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { follower::Entity::delete_many() @@ -1930,7 +1963,7 @@ impl Database { room_id: RoomId, connection: ConnectionId, location: proto::ParticipantLocation, - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async { let tx = tx; let location_kind; @@ -2042,8 +2075,16 @@ impl Database { }), }) } + async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { + let (_, room) = self.get_channel_room(room_id, tx).await?; + Ok(room) + } - async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { + async fn get_channel_room( + &self, + room_id: RoomId, + tx: &DatabaseTransaction, + ) -> Result<(Option, proto::Room)> { let db_room = room::Entity::find_by_id(room_id) .one(tx) .await? @@ -2147,6 +2188,28 @@ impl Database { }); } + Ok(( + db_room.channel_id, + proto::Room { + id: db_room.id.to_proto(), + live_kit_room: db_room.live_kit_room, + participants: participants.into_values().collect(), + pending_participants, + followers, + }, + )) + } + + async fn get_channel_members_for_room( + &self, + room_id: RoomId, + tx: &DatabaseTransaction, + ) -> Result> { + let db_room = room::Model { + id: room_id, + ..Default::default() + }; + let channel_users = if let Some(channel) = db_room.find_related(channel::Entity).one(tx).await? { self.get_channel_members_internal(channel.id, tx).await? @@ -2154,16 +2217,7 @@ impl Database { Vec::new() }; - Ok(ChannelRoom { - room: proto::Room { - id: db_room.id.to_proto(), - live_kit_room: db_room.live_kit_room, - participants: participants.into_values().collect(), - pending_participants, - followers, - }, - channel_participants: channel_users, - }) + Ok(channel_users) } // projects @@ -2193,7 +2247,7 @@ impl Database { room_id: RoomId, connection: ConnectionId, worktrees: &[proto::WorktreeMetadata], - ) -> Result> { + ) -> Result> { self.room_transaction(room_id, |tx| async move { let participant = room_participant::Entity::find() .filter( @@ -2264,7 +2318,7 @@ impl Database { &self, project_id: ProjectId, connection: ConnectionId, - ) -> Result)>> { + ) -> Result)>> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; @@ -2291,7 +2345,7 @@ impl Database { project_id: ProjectId, connection: ConnectionId, worktrees: &[proto::WorktreeMetadata], - ) -> Result)>> { + ) -> Result)>> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let project = project::Entity::find_by_id(project_id) @@ -2868,7 +2922,7 @@ impl Database { &self, project_id: ProjectId, connection: ConnectionId, - ) -> Result> { + ) -> Result> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { let result = project_collaborator::Entity::delete_many() @@ -3342,7 +3396,10 @@ impl Database { .await } - pub async fn get_channels(&self, user_id: UserId) -> Result> { + pub async fn get_channels( + &self, + user_id: UserId, + ) -> Result<(Vec, HashMap>)> { self.transaction(|tx| async move { let tx = tx; @@ -3379,7 +3436,31 @@ impl Database { drop(rows); - Ok(channels) + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIdsAndChannelIds { + ChannelId, + UserId, + } + + let mut participants = room_participant::Entity::find() + .inner_join(room::Entity) + .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) + .select_only() + .column(room::Column::ChannelId) + .column(room_participant::Column::UserId) + .into_values::<_, QueryUserIdsAndChannelIds>() + .stream(&*tx) + .await?; + + let mut participant_map: HashMap> = HashMap::default(); + while let Some(row) = participants.next().await { + let row: (ChannelId, UserId) = row?; + participant_map.entry(row.0).or_default().push(row.1) + } + + drop(participants); + + Ok((channels, participant_map)) }) .await } @@ -3523,7 +3604,7 @@ impl Database { .await } - pub async fn get_channel_room(&self, channel_id: ChannelId) -> Result { + pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { self.transaction(|tx| async move { let tx = tx; let room = channel::Model { @@ -3932,29 +4013,19 @@ id_type!(ServerId); id_type!(SignupId); id_type!(UserId); -pub struct ChannelRoom { +#[derive(Clone)] +pub struct JoinRoom { pub room: proto::Room, - pub channel_participants: Vec, -} - -impl Deref for ChannelRoom { - type Target = proto::Room; - - fn deref(&self) -> &Self::Target { - &self.room - } -} - -impl DerefMut for ChannelRoom { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.room - } + pub channel_id: Option, + pub channel_members: Vec, } pub struct RejoinedRoom { - pub room: ChannelRoom, + pub room: proto::Room, pub rejoined_projects: Vec, pub reshared_projects: Vec, + pub channel_id: Option, + pub channel_members: Vec, } pub struct ResharedProject { @@ -3989,14 +4060,18 @@ pub struct RejoinedWorktree { } pub struct LeftRoom { - pub room: ChannelRoom, + pub room: proto::Room, + pub channel_id: Option, + pub channel_members: Vec, pub left_projects: HashMap, pub canceled_calls_to_user_ids: Vec, pub deleted: bool, } pub struct RefreshedRoom { - pub room: ChannelRoom, + pub room: proto::Room, + pub channel_id: Option, + pub channel_members: Vec, pub stale_participant_user_ids: Vec, pub canceled_calls_to_user_ids: Vec, } diff --git a/crates/collab/src/db/room.rs b/crates/collab/src/db/room.rs index 88514ef4f1..c1624f0f2a 100644 --- a/crates/collab/src/db/room.rs +++ b/crates/collab/src/db/room.rs @@ -1,7 +1,7 @@ use super::{ChannelId, RoomId}; use sea_orm::entity::prelude::*; -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "rooms")] pub struct Model { #[sea_orm(primary_key)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 2ffcef454b..a6249bb548 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -951,7 +951,7 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .await .unwrap(); - let channels = db.get_channels(a_id).await.unwrap(); + let (channels, _) = db.get_channels(a_id).await.unwrap(); assert_eq!( channels, @@ -1047,10 +1047,10 @@ test_both_dbs!( .create_root_channel("channel_1", "1", user_1) .await .unwrap(); - let room_1 = db.get_channel_room(channel_1).await.unwrap(); + let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); // can join a room with membership to its channel - let room = db + let joined_room = db .join_room( room_1, user_1, @@ -1059,9 +1059,9 @@ test_both_dbs!( ) .await .unwrap(); - assert_eq!(room.participants.len(), 1); + assert_eq!(joined_room.room.participants.len(), 1); - drop(room); + drop(joined_room); // cannot join a room without membership to its channel assert!(db .join_room( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4d30d17485..59a997377e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,7 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, ChannelRoom, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{self, ChannelId, Database, ProjectId, RoomId, ServerId, User, UserId}, executor::Executor, AppState, Result, }; @@ -296,6 +296,15 @@ impl Server { "refreshed room" ); room_updated(&refreshed_room.room, &peer); + if let Some(channel_id) = refreshed_room.channel_id { + channel_updated( + channel_id, + &refreshed_room.room, + &refreshed_room.channel_members, + &peer, + &*pool.lock(), + ); + } contacts_to_update .extend(refreshed_room.stale_participant_user_ids.iter().copied()); contacts_to_update @@ -517,7 +526,7 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, invite_code, channels, channel_invites) = future::try_join4( + let (contacts, invite_code, (channels, channel_participants), channel_invites) = future::try_join4( this.app_state.db.get_contacts(user_id), this.app_state.db.get_invite_code_for_user(user_id), this.app_state.db.get_channels(user_id), @@ -528,7 +537,7 @@ impl Server { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; - this.peer.send(connection_id, build_initial_channels_update(channels, channel_invites))?; + this.peer.send(connection_id, build_initial_channels_update(channels, channel_participants, channel_invites))?; if let Some((code, count)) = invite_code { this.peer.send(connection_id, proto::UpdateInviteInfo { @@ -921,8 +930,8 @@ async fn join_room( .await .join_room(room_id, session.user_id, None, session.connection_id) .await?; - room_updated(&room, &session.peer); - room.clone() + room_updated(&room.room, &session.peer); + room.room.clone() }; for connection_id in session @@ -971,6 +980,9 @@ async fn rejoin_room( response: Response, session: Session, ) -> Result<()> { + let room; + let channel_id; + let channel_members; { let mut rejoined_room = session .db() @@ -1132,6 +1144,21 @@ async fn rejoin_room( )?; } } + + room = mem::take(&mut rejoined_room.room); + channel_id = rejoined_room.channel_id; + channel_members = mem::take(&mut rejoined_room.channel_members); + } + + //TODO: move this into the room guard + if let Some(channel_id) = channel_id { + channel_updated( + channel_id, + &room, + &channel_members, + &session.peer, + &*session.connection_pool().await, + ); } update_user_contacts(session.user_id, &session).await?; @@ -2202,9 +2229,9 @@ async fn invite_channel_member( } async fn remove_channel_member( - request: proto::RemoveChannelMember, - response: Response, - session: Session, + _request: proto::RemoveChannelMember, + _response: Response, + _session: Session, ) -> Result<()> { Ok(()) } @@ -2247,11 +2274,11 @@ async fn join_channel( ) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); - { + let joined_room = { let db = session.db().await; - let room_id = db.get_channel_room(channel_id).await?; + let room_id = db.room_id_for_channel(channel_id).await?; - let room = db + let joined_room = db .join_room( room_id, session.user_id, @@ -2262,7 +2289,10 @@ async fn join_channel( let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { let token = live_kit - .room_token(&room.live_kit_room, &session.user_id.to_string()) + .room_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) .trace_err()?; Some(LiveKitConnectionInfo { @@ -2272,12 +2302,25 @@ async fn join_channel( }); response.send(proto::JoinRoomResponse { - room: Some(room.clone()), + room: Some(joined_room.room.clone()), live_kit_connection_info, })?; - room_updated(&room, &session.peer); - } + room_updated(&joined_room.room, &session.peer); + + joined_room.clone() + }; + + // TODO - do this while still holding the room guard, + // currently there's a possible race condition if someone joins the channel + // after we've dropped the lock but before we finish sending these updates + channel_updated( + channel_id, + &joined_room.room, + &joined_room.channel_members, + &session.peer, + &*session.connection_pool().await, + ); update_user_contacts(session.user_id, &session).await?; @@ -2356,6 +2399,7 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { fn build_initial_channels_update( channels: Vec, + channel_participants: HashMap>, channel_invites: Vec, ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); @@ -2426,10 +2470,7 @@ fn contact_for_user( } } -fn room_updated(room: &ChannelRoom, peer: &Peer, pool: &ConnectionPool) { - let channel_ids = &room.channel_participants; - let room = &room.room; - +fn room_updated(room: &proto::Room, peer: &Peer) { broadcast( None, room.participants @@ -2444,17 +2485,41 @@ fn room_updated(room: &ChannelRoom, peer: &Peer, pool: &ConnectionPool) { ) }, ); +} + +fn channel_updated( + channel_id: ChannelId, + room: &proto::Room, + channel_members: &[UserId], + peer: &Peer, + pool: &ConnectionPool, +) { + let participants = room + .participants + .iter() + .map(|p| p.user_id) + .collect::>(); broadcast( None, - channel_ids + channel_members .iter() + .filter(|user_id| { + !room + .participants + .iter() + .any(|p| p.user_id == user_id.to_proto()) + }) .flat_map(|user_id| pool.user_connection_ids(*user_id)), |peer_id| { peer.send( peer_id.into(), - proto::RoomUpdated { - room: Some(room.clone()), + proto::UpdateChannels { + channel_participants: vec![proto::ChannelParticipants { + channel_id: channel_id.to_proto(), + participant_user_ids: participants.clone(), + }], + ..Default::default() }, ) }, @@ -2502,6 +2567,10 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { let canceled_calls_to_user_ids; let live_kit_room; let delete_live_kit_room; + let room; + let channel_members; + let channel_id; + if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? { contacts_to_update.insert(session.user_id); @@ -2509,19 +2578,30 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { project_left(project, session); } - { - let connection_pool = session.connection_pool().await; - room_updated(&left_room.room, &session.peer, &connection_pool); - } - room_id = RoomId::from_proto(left_room.room.id); canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids); live_kit_room = mem::take(&mut left_room.room.live_kit_room); delete_live_kit_room = left_room.deleted; + room = mem::take(&mut left_room.room); + channel_members = mem::take(&mut left_room.channel_members); + channel_id = left_room.channel_id; + + room_updated(&room, &session.peer); } else { return Ok(()); } + // TODO - do this while holding the room guard. + if let Some(channel_id) = channel_id { + channel_updated( + channel_id, + &room, + &channel_members, + &session.peer, + &*session.connection_pool().await, + ); + } + { let pool = session.connection_pool().await; for canceled_user_id in canceled_calls_to_user_ids { diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 14363b74cf..957e085693 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -66,7 +66,7 @@ async fn test_basic_channels( ) }); - // Client B now sees that they are in channel A. + // Client B now sees that they are a member channel A. client_b .channel_store() .update(cx_b, |channels, _| { @@ -110,14 +110,20 @@ async fn test_channel_room( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_b, "user_c").await; let zed_id = server - .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel( + "zed", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) .await; let active_call_a = cx_a.read(ActiveCall::global); @@ -128,11 +134,26 @@ async fn test_channel_room( .await .unwrap(); + // TODO Test that B and C sees A in the channel room + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + depth: 0, + })] + ) + }); + active_call_b .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); + // TODO Test that C sees A and B in the channel room + deterministic.run_until_parked(); let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); @@ -162,12 +183,14 @@ async fn test_channel_room( .await .unwrap(); + // TODO Make sure that C sees A leave + active_call_b .update(cx_b, |active_call, cx| active_call.hang_up(cx)) .await .unwrap(); - // Make sure room exists? + // TODO Make sure that C sees B leave active_call_a .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f49a879dc7..c4fb5aa653 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -136,8 +136,8 @@ message Envelope { RemoveChannelMember remove_channel_member = 122; RespondToChannelInvite respond_to_channel_invite = 123; UpdateChannels update_channels = 124; - JoinChannel join_channel = 125; - RemoveChannel remove_channel = 126; + JoinChannel join_channel = 126; + RemoveChannel remove_channel = 127; } } @@ -870,6 +870,12 @@ message UpdateChannels { repeated uint64 remove_channels = 2; repeated Channel channel_invitations = 3; repeated uint64 remove_channel_invitations = 4; + repeated ChannelParticipants channel_participants = 5; +} + +message ChannelParticipants { + uint64 channel_id = 1; + repeated uint64 participant_user_ids = 2; } message JoinChannel { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f6985d6906..07d54ce4db 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -244,7 +244,7 @@ messages!( (UpdateWorktreeSettings, Foreground), (UpdateDiffBase, Foreground), (GetPrivateUserInfo, Foreground), - (GetPrivateUserInfoResponse, Foreground), + (GetPrivateUserInfoResponse, Foreground) ); request_messages!( From b4f6d6eadc8a051f841a1e1801d72c23750d1253 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 2 Aug 2023 17:14:15 -0400 Subject: [PATCH 062/326] update search text for no results --- crates/search/src/project_search.rs | 36 +++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 32a301d4f3..03e9f47d8d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -113,6 +113,7 @@ struct ProjectSearch { active_query: Option, search_id: usize, search_history: SearchHistory, + no_results: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -235,6 +236,7 @@ impl ProjectSearch { active_query: None, search_id: 0, search_history: SearchHistory::default(), + no_results: None, } } @@ -249,6 +251,7 @@ impl ProjectSearch { active_query: self.active_query.clone(), search_id: self.search_id, search_history: self.search_history.clone(), + no_results: self.no_results.clone(), }) } @@ -256,6 +259,7 @@ impl ProjectSearch { self.active_query = None; self.match_ranges.clear(); self.pending_search = None; + self.no_results = None; } fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { @@ -272,6 +276,7 @@ impl ProjectSearch { let mut matches = matches.into_iter().collect::>(); let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { this.match_ranges.clear(); + this.no_results = Some(true); matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); @@ -285,6 +290,7 @@ impl ProjectSearch { while let Ok(Some(match_range)) = match_ranges.try_next() { this.match_ranges.push(match_range); } + this.no_results = Some(false); cx.notify(); }); } @@ -315,6 +321,7 @@ impl ProjectSearch { self.search_id += 1; self.match_ranges.clear(); self.search_history.add(query.as_str().to_string()); + self.no_results = Some(true); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; @@ -337,6 +344,7 @@ impl ProjectSearch { while let Ok(Some(match_range)) = match_ranges.try_next() { this.match_ranges.push(match_range); } + this.no_results = Some(false); cx.notify(); }); } @@ -382,6 +390,8 @@ impl View for ProjectSearchView { let current_mode = self.current_mode; let major_text = if model.pending_search.is_some() { Cow::Borrowed("Searching...") + } else if model.no_results.is_some_and(|v| v) { + Cow::Borrowed("No Results...") } else { match current_mode { SearchMode::Text => Cow::Borrowed("Text search all files and folders"), @@ -408,15 +418,23 @@ impl View for ProjectSearchView { "Indexing: ...".to_string() }; - let minor_text = match current_mode { - SearchMode::Semantic => [ - semantic_status, - "ex. 'list all available languages'".to_owned(), - ], - _ => [ - "Include/exclude specific paths with the filter option.".to_owned(), - "Matching exact word and/or casing is available too.".to_owned(), - ], + let minor_text = if let Some(no_results) = model.no_results { + if no_results { + vec!["No results found in this project for the provided query".to_owned()] + } else { + vec![] + } + } else { + match current_mode { + SearchMode::Semantic => vec![ + semantic_status, + "ex. 'list all available languages'".to_owned(), + ], + _ => vec![ + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ], + } }; let previous_query_keystrokes = From fca8cdcb8e10a922005a9bd96b625fab55709e40 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 15:09:37 -0700 Subject: [PATCH 063/326] Start work on rendering channel participants in collab panel Co-authored-by: mikayla --- crates/client/src/channel_store.rs | 56 +++++++++++++- crates/collab/src/db.rs | 20 ----- crates/collab/src/rpc.rs | 15 ++-- crates/collab/src/tests.rs | 3 + crates/collab/src/tests/channel_tests.rs | 94 ++++++++++++++++++++++-- crates/collab_ui/src/face_pile.rs | 34 ++++----- crates/collab_ui/src/panel.rs | 28 +++++-- crates/theme/src/theme.rs | 1 + styles/src/style_tree/collab_panel.ts | 1 + 9 files changed, 192 insertions(+), 60 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 5218c56891..558570475e 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -7,11 +7,12 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; type ChannelId = u64; +type UserId = u64; pub struct ChannelStore { channels: Vec>, channel_invitations: Vec>, - channel_participants: HashMap>, + channel_participants: HashMap>>, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -60,6 +61,12 @@ impl ChannelStore { self.channels.iter().find(|c| c.id == channel_id).cloned() } + pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { + self.channel_participants + .get(&channel_id) + .map_or(&[], |v| v.as_slice()) + } + pub fn create_channel( &self, name: &str, @@ -78,7 +85,7 @@ impl ChannelStore { pub fn invite_member( &self, channel_id: ChannelId, - user_id: u64, + user_id: UserId, admin: bool, ) -> impl Future> { let client = self.client.clone(); @@ -162,6 +169,8 @@ impl ChannelStore { .retain(|channel| !payload.remove_channels.contains(&channel.id)); self.channel_invitations .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); + self.channel_participants + .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); for channel in payload.channel_invitations { if let Some(existing_channel) = self @@ -215,6 +224,49 @@ impl ChannelStore { ); } } + + let mut all_user_ids = Vec::new(); + let channel_participants = payload.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) { + all_user_ids.insert(ix, *user_id); + } + } + } + + // TODO: Race condition if an update channels messages comes in while resolving avatars + let users = self + .user_store + .update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx)); + cx.spawn(|this, mut cx| async move { + let users = users.await?; + + this.update(&mut cx, |this, cx| { + for entry in &channel_participants { + let mut participants: Vec<_> = entry + .participant_user_ids + .iter() + .filter_map(|user_id| { + users + .binary_search_by_key(&user_id, |user| &user.id) + .ok() + .map(|ix| users[ix].clone()) + }) + .collect(); + + participants.sort_by_key(|u| u.id); + + this.channel_participants + .insert(entry.channel_id, participants); + } + + cx.notify(); + }); + anyhow::Ok(()) + }) + .detach(); + cx.notify(); } } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ad87266e7d..85f5d5f0b8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2200,26 +2200,6 @@ impl Database { )) } - async fn get_channel_members_for_room( - &self, - room_id: RoomId, - tx: &DatabaseTransaction, - ) -> Result> { - let db_room = room::Model { - id: room_id, - ..Default::default() - }; - - let channel_users = - if let Some(channel) = db_room.find_related(channel::Entity).one(tx).await? { - self.get_channel_members_internal(channel.id, tx).await? - } else { - Vec::new() - }; - - Ok(channel_users) - } - // projects pub async fn project_count_excluding_admins(&self) -> Result { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 59a997377e..526f12d812 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2412,6 +2412,15 @@ fn build_initial_channels_update( }); } + for (channel_id, participants) in channel_participants { + update + .channel_participants + .push(proto::ChannelParticipants { + channel_id: channel_id.to_proto(), + participant_user_ids: participants.into_iter().map(|id| id.to_proto()).collect(), + }); + } + for channel in channel_invites { update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), @@ -2504,12 +2513,6 @@ fn channel_updated( None, channel_members .iter() - .filter(|user_id| { - !room - .participants - .iter() - .any(|p| p.user_id == user_id.to_proto()) - }) .flat_map(|user_id| pool.user_connection_ids(*user_id)), |peer_id| { peer.send( diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e0346dbe7f..26ca5a008e 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -103,6 +103,9 @@ impl TestServer { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { cx.update(|cx| { + if cx.has_global::() { + panic!("Same cx used to create two test clients") + } cx.set_global(SettingsStore::test(cx)); }); diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 957e085693..c41ac84d1d 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,5 +1,5 @@ use call::ActiveCall; -use client::Channel; +use client::{Channel, User}; use gpui::{executor::Deterministic, TestAppContext}; use std::sync::Arc; @@ -26,6 +26,7 @@ async fn test_basic_channels( .await .unwrap(); + deterministic.run_until_parked(); client_a.channel_store().read_with(cx_a, |channels, _| { assert_eq!( channels.channels(), @@ -105,6 +106,13 @@ async fn test_basic_channels( .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); } +fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u64]) { + assert_eq!( + participants.iter().map(|p| p.id).collect::>(), + expected_partitipants + ); +} + #[gpui::test] async fn test_channel_room( deterministic: Arc, @@ -116,7 +124,7 @@ async fn test_channel_room( let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_b, "user_c").await; + let client_c = server.create_client(cx_c, "user_c").await; let zed_id = server .make_channel( @@ -134,8 +142,21 @@ async fn test_channel_room( .await .unwrap(); - // TODO Test that B and C sees A in the channel room + // Give everyone a chance to observe user A joining + deterministic.run_until_parked(); + + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + }); + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); assert_eq!( channels.channels(), &[Arc::new(Channel { @@ -147,15 +168,41 @@ async fn test_channel_room( ) }); + client_c.channel_store().read_with(cx_c, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + }); + active_call_b .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); - // TODO Test that C sees A and B in the channel room - deterministic.run_until_parked(); + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }); + + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }); + + client_c.channel_store().read_with(cx_c, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }); + let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone()); room_a.read_with(cx_a, |room, _| assert!(room.is_connected())); assert_eq!( @@ -183,14 +230,47 @@ async fn test_channel_room( .await .unwrap(); - // TODO Make sure that C sees A leave + deterministic.run_until_parked(); + + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_b.user_id().unwrap()], + ); + }); + + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_b.user_id().unwrap()], + ); + }); + + client_c.channel_store().read_with(cx_c, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_b.user_id().unwrap()], + ); + }); active_call_b .update(cx_b, |active_call, cx| active_call.hang_up(cx)) .await .unwrap(); - // TODO Make sure that C sees B leave + deterministic.run_until_parked(); + + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + }); + + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + }); + + client_c.channel_store().read_with(cx_c, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + }); active_call_a .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 1bbceee9af..7e95a7677c 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -7,34 +7,34 @@ use gpui::{ }, json::ToJson, serde_json::{self, json}, - AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext, + AnyElement, Axis, Element, LayoutContext, SceneBuilder, View, ViewContext, }; use crate::CollabTitlebarItem; -pub(crate) struct FacePile { +pub(crate) struct FacePile { overlap: f32, - faces: Vec>, + faces: Vec>, } -impl FacePile { - pub fn new(overlap: f32) -> FacePile { - FacePile { +impl FacePile { + pub fn new(overlap: f32) -> Self { + Self { overlap, faces: Vec::new(), } } } -impl Element for FacePile { +impl Element for FacePile { type LayoutState = (); type PaintState = (); fn layout( &mut self, constraint: gpui::SizeConstraint, - view: &mut CollabTitlebarItem, - cx: &mut LayoutContext, + view: &mut V, + cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); @@ -53,8 +53,8 @@ impl Element for FacePile { bounds: RectF, visible_bounds: RectF, _layout: &mut Self::LayoutState, - view: &mut CollabTitlebarItem, - cx: &mut ViewContext, + view: &mut V, + cx: &mut ViewContext, ) -> Self::PaintState { let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); @@ -80,8 +80,8 @@ impl Element for FacePile { _: RectF, _: &Self::LayoutState, _: &Self::PaintState, - _: &CollabTitlebarItem, - _: &ViewContext, + _: &V, + _: &ViewContext, ) -> Option { None } @@ -91,8 +91,8 @@ impl Element for FacePile { bounds: RectF, _: &Self::LayoutState, _: &Self::PaintState, - _: &CollabTitlebarItem, - _: &ViewContext, + _: &V, + _: &ViewContext, ) -> serde_json::Value { json!({ "type": "FacePile", @@ -101,8 +101,8 @@ impl Element for FacePile { } } -impl Extend> for FacePile { - fn extend>>(&mut self, children: T) { +impl Extend> for FacePile { + fn extend>>(&mut self, children: T) { self.faces.extend(children); } } diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 1973ddd9f6..406daae0f2 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -40,6 +40,8 @@ use workspace::{ Workspace, }; +use crate::face_pile::FacePile; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { channel_id: u64, @@ -253,7 +255,7 @@ impl CollabPanel { ) } ListEntry::Channel(channel) => { - Self::render_channel(&*channel, &theme.collab_panel, is_selected, cx) + this.render_channel(&*channel, &theme.collab_panel, is_selected, cx) } ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), @@ -1265,20 +1267,16 @@ impl CollabPanel { } fn render_channel( + &self, channel: &Channel, theme: &theme::CollabPanel, is_selected: bool, cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; - MouseEventHandler::::new(channel.id as usize, cx, |state, _cx| { + MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() - .with_child({ - Svg::new("icons/file_icons/hash.svg") - // .with_style(theme.contact_avatar) - .aligned() - .left() - }) + .with_child({ Svg::new("icons/file_icons/hash.svg").aligned().left() }) .with_child( Label::new(channel.name.clone(), theme.contact_username.text.clone()) .contained() @@ -1287,6 +1285,20 @@ impl CollabPanel { .left() .flex(1., true), ) + .with_child( + FacePile::new(theme.face_overlap).with_children( + self.channel_store + .read(cx) + .channel_participants(channel_id) + .iter() + .filter_map(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(theme.contact_avatar), + ) + }), + ), + ) .constrained() .with_height(theme.row_height) .contained() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3de878118e..96eac81a50 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -241,6 +241,7 @@ pub struct CollabPanel { pub disabled_button: IconButton, pub section_icon_size: f32, pub calling_indicator: ContainedText, + pub face_overlap: f32, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 8e817add3f..49a343e6c9 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -275,5 +275,6 @@ export default function contacts_panel(): any { }, }, }), + face_overlap: 8 } } From 4d551104522ddfcc1ed4c597ed56ea1f7d3beb13 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 15:45:19 -0700 Subject: [PATCH 064/326] Restore seeding of random GH users in seed-db Co-authored-by: Mikayla --- crates/collab/src/bin/seed.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/crates/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index 9384e826c0..cb1594e941 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -64,9 +64,9 @@ async fn main() { .expect("failed to fetch user") .is_none() { - if let Some(email) = &github_user.email { + if admin { db.create_user( - email, + &format!("{}@zed.dev", github_user.login), admin, db::NewUserParams { github_login: github_user.login, @@ -76,15 +76,11 @@ async fn main() { ) .await .expect("failed to insert user"); - } else if admin { - db.create_user( - &format!("{}@zed.dev", github_user.login), - admin, - db::NewUserParams { - github_login: github_user.login, - github_user_id: github_user.id, - invite_count: 5, - }, + } else { + db.get_or_create_user_by_github_account( + &github_user.login, + Some(github_user.id), + github_user.email.as_deref(), ) .await .expect("failed to insert user"); From 0ae1f29be82c5b5a81cb9a7c62a42b6985a46dce Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 15:52:56 -0700 Subject: [PATCH 065/326] wip --- crates/client/src/channel_store.rs | 12 ++++-------- crates/collab/src/db/tests.rs | 2 +- crates/collab_ui/src/face_pile.rs | 2 -- crates/collab_ui/src/panel.rs | 17 ++++++----------- crates/collab_ui/src/panel/channel_modal.rs | 8 ++++---- script/zed-with-local-servers | 5 ++++- styles/.eslintrc.js | 1 + styles/src/style_tree/collab_panel.ts | 1 + styles/tsconfig.json | 4 +++- 9 files changed, 24 insertions(+), 28 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 558570475e..534bd0b05a 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -115,10 +115,6 @@ impl ChannelStore { } } - pub fn is_channel_invite_pending(&self, channel: &Arc) -> bool { - false - } - pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { let client = self.client.clone(); async move { @@ -127,6 +123,10 @@ impl ChannelStore { } } + pub fn is_channel_invite_pending(&self, _: &Arc) -> bool { + false + } + pub fn remove_member( &self, channel_id: ChannelId, @@ -144,10 +144,6 @@ impl ChannelStore { todo!() } - pub fn add_guest_channel(&self, channel_id: ChannelId) -> Task> { - todo!() - } - async fn handle_update_channels( this: ModelHandle, message: TypedEnvelope, diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index a6249bb548..a1d1a23dc9 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1080,7 +1080,7 @@ test_both_dbs!( test_channel_invites_sqlite, db, { - let owner_id = db.create_server("test").await.unwrap().0 as u32; + db.create_server("test").await.unwrap(); let user_1 = db .create_user( diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 7e95a7677c..30fcb97506 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -10,8 +10,6 @@ use gpui::{ AnyElement, Axis, Element, LayoutContext, SceneBuilder, View, ViewContext, }; -use crate::CollabTitlebarItem; - pub(crate) struct FacePile { overlap: f32, faces: Vec>, diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 406daae0f2..667e8d3a5c 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -1259,8 +1259,8 @@ impl CollabPanel { fn render_channel_editor( &self, - theme: &theme::CollabPanel, - depth: usize, + _theme: &theme::CollabPanel, + _depth: usize, cx: &AppContext, ) -> AnyElement { ChildView::new(&self.channel_name_editor, cx).into_any() @@ -1276,7 +1276,7 @@ impl CollabPanel { let channel_id = channel.id; MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() - .with_child({ Svg::new("icons/file_icons/hash.svg").aligned().left() }) + .with_child(Svg::new("icons/file_icons/hash.svg").aligned().left()) .with_child( Label::new(channel.name.clone(), theme.contact_username.text.clone()) .contained() @@ -1329,12 +1329,7 @@ impl CollabPanel { let button_spacing = theme.contact_button_spacing; Flex::row() - .with_child({ - Svg::new("icons/file_icons/hash.svg") - // .with_style(theme.contact_avatar) - .aligned() - .left() - }) + .with_child(Svg::new("icons/file_icons/hash.svg").aligned().left()) .with_child( Label::new(channel.name.clone(), theme.contact_username.text.clone()) .contained() @@ -1616,7 +1611,7 @@ impl CollabPanel { } } } else if let Some((editing_state, channel_name)) = self.take_editing_state(cx) { - let create_channel = self.channel_store.update(cx, |channel_store, cx| { + let create_channel = self.channel_store.update(cx, |channel_store, _| { channel_store.create_channel(&channel_name, editing_state.parent_id) }); @@ -1687,7 +1682,7 @@ impl CollabPanel { cx.spawn(|_, mut cx| async move { if answer.next().await == Some(0) { if let Err(e) = channel_store - .update(&mut cx, |channels, cx| channels.remove_channel(channel_id)) + .update(&mut cx, |channels, _| channels.remove_channel(channel_id)) .await { cx.prompt( diff --git a/crates/collab_ui/src/panel/channel_modal.rs b/crates/collab_ui/src/panel/channel_modal.rs index fff1dc8624..aa1b3e5a13 100644 --- a/crates/collab_ui/src/panel/channel_modal.rs +++ b/crates/collab_ui/src/panel/channel_modal.rs @@ -9,7 +9,7 @@ pub fn init(cx: &mut AppContext) { pub struct ChannelModal { has_focus: bool, - input_editor: ViewHandle, + filter_editor: ViewHandle, } pub enum Event { @@ -30,7 +30,7 @@ impl ChannelModal { ChannelModal { has_focus: false, - input_editor, + filter_editor: input_editor, } } @@ -55,7 +55,7 @@ impl View for ChannelModal { enum ChannelModal {} MouseEventHandler::::new(0, cx, |_, cx| { Flex::column() - .with_child(ChildView::new(self.input_editor.as_any(), cx)) + .with_child(ChildView::new(self.filter_editor.as_any(), cx)) .with_child(Label::new("ADD OR BROWSE CHANNELS HERE", style)) .contained() .with_style(modal_container) @@ -71,7 +71,7 @@ impl View for ChannelModal { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { self.has_focus = true; if cx.is_self_focused() { - cx.focus(&self.input_editor); + cx.focus(&self.filter_editor); } } diff --git a/script/zed-with-local-servers b/script/zed-with-local-servers index c47b0e3de0..e1b224de60 100755 --- a/script/zed-with-local-servers +++ b/script/zed-with-local-servers @@ -1,3 +1,6 @@ #!/bin/bash -ZED_ADMIN_API_TOKEN=secret ZED_IMPERSONATE=as-cii ZED_SERVER_URL=http://localhost:8080 cargo run $@ +: "${ZED_IMPERSONATE:=as-cii}" +export ZED_IMPERSONATE + +ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@ diff --git a/styles/.eslintrc.js b/styles/.eslintrc.js index 485ff73d10..82e9636189 100644 --- a/styles/.eslintrc.js +++ b/styles/.eslintrc.js @@ -28,6 +28,7 @@ module.exports = { }, rules: { "linebreak-style": ["error", "unix"], + "@typescript-eslint/no-explicit-any": "off", semi: ["error", "never"], }, } diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 49a343e6c9..3390dd51f8 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -8,6 +8,7 @@ import { import { interactive, toggleable } from "../element" import { useTheme } from "../theme" + export default function contacts_panel(): any { const theme = useTheme() diff --git a/styles/tsconfig.json b/styles/tsconfig.json index a1913027b7..281bd74b21 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -24,5 +24,7 @@ "useUnknownInCatchVariables": false, "baseUrl": "." }, - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] } From 30e1bfc872bf88214356400c0774ba921174b9d7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 2 Aug 2023 17:13:09 -0700 Subject: [PATCH 066/326] Add the ability to jump between channels while in a channel --- crates/call/src/call.rs | 6 +++ crates/client/src/client.rs | 6 ++- crates/collab/src/db.rs | 29 +++++++++++++++ crates/collab/src/rpc.rs | 25 ++++++++++++- crates/collab/src/tests/channel_tests.rs | 47 ++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 3cd868a438..6e58be4f15 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -279,15 +279,21 @@ impl ActiveCall { channel_id: u64, cx: &mut ModelContext, ) -> Task> { + let leave_room; if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { return Task::ready(Ok(())); + } else { + leave_room = room.update(cx, |room, cx| room.leave(cx)); } + } else { + leave_room = Task::ready(Ok(())); } let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx); cx.spawn(|this, mut cx| async move { + leave_room.await?; let room = join.await?; this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) .await?; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1e86cef4cc..8ef3e32ea8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -540,6 +540,7 @@ impl Client { } } + #[track_caller] pub fn add_message_handler( self: &Arc, model: ModelHandle, @@ -575,8 +576,11 @@ impl Client { }), ); if prev_handler.is_some() { + let location = std::panic::Location::caller(); panic!( - "registered handler for the same message {} twice", + "{}:{} registered handler for the same message {} twice", + location.file(), + location.line(), std::any::type_name::() ); } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 85f5d5f0b8..36b226b97b 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1342,6 +1342,35 @@ impl Database { .await } + pub async fn is_current_room_different_channel( + &self, + user_id: UserId, + channel_id: ChannelId, + ) -> Result { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + ChannelId, + } + + let channel_id_model: Option = room_participant::Entity::find() + .select_only() + .column_as(room::Column::ChannelId, QueryAs::ChannelId) + .inner_join(room::Entity) + .filter(room_participant::Column::UserId.eq(user_id)) + .into_values::<_, QueryAs>() + .one(&*tx) + .await?; + + let result = channel_id_model + .map(|channel_id_model| channel_id_model != channel_id) + .unwrap_or(false); + + Ok(result) + }) + .await + } + pub async fn join_room( &self, room_id: RoomId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 526f12d812..15237049c3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2276,6 +2276,14 @@ async fn join_channel( let joined_room = { let db = session.db().await; + + if db + .is_current_room_different_channel(session.user_id, channel_id) + .await? + { + leave_room_for_session_with_guard(&session, &db).await?; + } + let room_id = db.room_id_for_channel(channel_id).await?; let joined_room = db @@ -2531,6 +2539,14 @@ fn channel_updated( async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> { let db = session.db().await; + update_user_contacts_with_guard(user_id, session, &db).await +} + +async fn update_user_contacts_with_guard( + user_id: UserId, + session: &Session, + db: &DbHandle, +) -> Result<()> { let contacts = db.get_contacts(user_id).await?; let busy = db.is_user_busy(user_id).await?; @@ -2564,6 +2580,11 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> } async fn leave_room_for_session(session: &Session) -> Result<()> { + let db = session.db().await; + leave_room_for_session_with_guard(session, &db).await +} + +async fn leave_room_for_session_with_guard(session: &Session, db: &DbHandle) -> Result<()> { let mut contacts_to_update = HashSet::default(); let room_id; @@ -2574,7 +2595,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { let channel_members; let channel_id; - if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? { + if let Some(mut left_room) = db.leave_room(session.connection_id).await? { contacts_to_update.insert(session.user_id); for project in left_room.left_projects.values() { @@ -2624,7 +2645,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { } for contact_user_id in contacts_to_update { - update_user_contacts(contact_user_id, &session).await?; + update_user_contacts_with_guard(contact_user_id, &session, db).await?; } if let Some(live_kit) = session.live_kit_client.as_ref() { diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index c41ac84d1d..3999740557 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -304,3 +304,50 @@ async fn test_channel_room( } ); } + +#[gpui::test] +async fn test_channel_jumping(deterministic: Arc, cx_a: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + let rust_id = server + .make_channel("rust", (&client_a, cx_a), &mut []) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + + active_call_a + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) + .await + .unwrap(); + + // Give everything a chance to observe user A joining + deterministic.run_until_parked(); + + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq( + channels.channel_participants(zed_id), + &[client_a.user_id().unwrap()], + ); + assert_participants_eq(channels.channel_participants(rust_id), &[]); + }); + + active_call_a + .update(cx_a, |active_call, cx| { + active_call.join_channel(rust_id, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq(channels.channel_participants(zed_id), &[]); + assert_participants_eq( + channels.channel_participants(rust_id), + &[client_a.user_id().unwrap()], + ); + }); +} From 7d83d15bf340d5b730df9c4c2f2fbd677531774f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:05:24 +0200 Subject: [PATCH 067/326] Move navigation buttons to the tab bar. Co-authored-by: Kyle --- crates/theme/src/theme.rs | 2 +- crates/workspace/src/pane.rs | 108 ++++++++++++++++++++++++++++++- crates/workspace/src/toolbar.rs | 66 ------------------- styles/src/style_tree/tab_bar.ts | 19 +++++- 4 files changed, 126 insertions(+), 69 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c3b3502770..da6a188527 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -295,6 +295,7 @@ pub struct TabBar { pub inactive_pane: TabStyles, pub dragged_tab: Tab, pub height: f32, + pub nav_button: Interactive, } impl TabBar { @@ -359,7 +360,6 @@ pub struct Toolbar { pub container: ContainerStyle, pub height: f32, pub item_spacing: f32, - pub nav_button: Interactive, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ee658c9cc9..be1460f200 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -222,6 +222,56 @@ impl TabBarContextMenu { } } +#[allow(clippy::too_many_arguments)] +fn nav_button)>( + svg_path: &'static str, + style: theme::Interactive, + nav_button_height: f32, + tooltip_style: TooltipStyle, + enabled: bool, + on_click: F, + tooltip_action: A, + action_name: &str, + cx: &mut ViewContext, +) -> AnyElement { + MouseEventHandler::::new(0, cx, |state, _| { + let style = if enabled { + style.style_for(state) + } else { + style.disabled_style() + }; + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(nav_button_height) + .aligned() + .top() + }) + .with_cursor_style(if enabled { + CursorStyle::PointingHand + } else { + CursorStyle::default() + }) + .on_click(MouseButton::Left, move |_, toolbar, cx| { + on_click(toolbar, cx) + }) + .with_tooltip::( + 0, + action_name.to_string(), + Some(Box::new(tooltip_action)), + tooltip_style, + cx, + ) + .contained() + .into_any_named("nav button") +} + impl Pane { pub fn new( workspace: WeakViewHandle, @@ -236,6 +286,11 @@ impl Pane { context_menu.update(cx, |menu, _| { menu.set_position_mode(OverlayPositionMode::Local) }); + let theme = theme::current(cx).workspace.tab_bar.clone(); + let mut border_for_nav_buttons = theme.tab_style(false, false).container.border.clone(); + border_for_nav_buttons.left = false; + let nav_button_height = theme.height; + let button_style = theme.nav_button; Self { items: Vec::new(), @@ -265,8 +320,59 @@ impl Pane { has_focus: false, can_drop: Rc::new(|_, _| true), can_split: true, - render_tab_bar_buttons: Rc::new(|pane, cx| { + render_tab_bar_buttons: Rc::new(move |pane, cx| { + let tooltip_style = theme::current(cx).tooltip.clone(); Flex::row() + .with_child(nav_button( + "icons/arrow_left_16.svg", + button_style.clone(), + nav_button_height, + tooltip_style.clone(), + pane.can_navigate_backward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_back(pane, cx).detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoBack, + "Go Back", + cx, + )) + .with_child( + nav_button( + "icons/arrow_right_16.svg", + button_style.clone(), + nav_button_height, + tooltip_style, + pane.can_navigate_forward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_forward(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoForward, + "Go Forward", + cx, + ) + .contained() + .with_border(border_for_nav_buttons), + ) // New menu .with_child(Self::render_tab_bar_button( 0, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 3fa37f3666..1d6e8b7e4b 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -118,76 +118,10 @@ impl View for Toolbar { } } - let pane = self.pane.clone(); - let mut enable_go_backward = false; - let mut enable_go_forward = false; - if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) { - let pane = pane.read(cx); - enable_go_backward = pane.can_navigate_backward(); - enable_go_forward = pane.can_navigate_forward(); - } - let container_style = theme.container; let height = theme.height * primary_items_row_count as f32; - let nav_button_height = theme.height; - let button_style = theme.nav_button; - let tooltip_style = theme::current(cx).tooltip.clone(); let mut primary_items = Flex::row(); - if self.can_navigate { - primary_items.add_child(nav_button( - "icons/arrow_left_16.svg", - button_style, - nav_button_height, - tooltip_style.clone(), - enable_go_backward, - spacing, - { - move |toolbar, cx| { - if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx)) - { - if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) { - let pane = pane.downgrade(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, cx).detach_and_log_err(cx); - }); - }) - } - } - } - }, - super::GoBack, - "Go Back", - cx, - )); - primary_items.add_child(nav_button( - "icons/arrow_right_16.svg", - button_style, - nav_button_height, - tooltip_style, - enable_go_forward, - spacing, - { - move |toolbar, cx| { - if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx)) - { - if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) { - let pane = pane.downgrade(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_forward(pane, cx).detach_and_log_err(cx); - }); - }) - } - } - } - }, - super::GoForward, - "Go Forward", - cx, - )); - } primary_items.extend(primary_left_items); primary_items.extend(primary_right_items); diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index e7b04246c4..f27ae4b2e6 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -84,7 +84,23 @@ export default function tab_bar(): any { bottom: false, }, } - + const nav_button = interactive({ + base: { + color: foreground(theme.highest, "on"), + icon_width: 12, + button_width: 24, + corner_radius: 6, + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + disabled: { + color: foreground(theme.highest, "on", "disabled"), + }, + }, + }); const dragged_tab = { ...active_pane_active_tab, background: with_opacity(tab.background, 0.9), @@ -141,5 +157,6 @@ export default function tab_bar(): any { right: false, }, }, + nav_button: nav_button } } From 8831e03ebaf3eddec26f37abc074aade7cc6d829 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:09:26 +0200 Subject: [PATCH 068/326] Remove reference to pane from a toolbar. Co-authored-by: Kyle --- assets/keymaps/default.json | 2 +- crates/ai/src/assistant.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/toolbar.rs | 56 +----------------------------- styles/src/style_tree/tab_bar.ts | 2 +- styles/src/style_tree/workspace.ts | 17 --------- 6 files changed, 5 insertions(+), 76 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 9fbe89975f..2f13ee6d03 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -238,7 +238,7 @@ "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "project_search::CycleMode", + "alt-tab": "project_search::CycleMode" } }, { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 957c5e1c06..fa81d6a400 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -158,7 +158,7 @@ impl AssistantPanel { }); let toolbar = cx.add_view(|cx| { - let mut toolbar = Toolbar::new(None); + let mut toolbar = Toolbar::new(); toolbar.set_can_navigate(false, cx); toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); toolbar diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index be1460f200..3a4c85b24c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -308,7 +308,7 @@ impl Pane { pane: handle.clone(), next_timestamp, }))), - toolbar: cx.add_view(|_| Toolbar::new(Some(handle))), + toolbar: cx.add_view(|_| Toolbar::new()), tab_bar_context_menu: TabBarContextMenu { kind: TabBarContextMenuKind::New, handle: context_menu, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 1d6e8b7e4b..a7c00cbe0f 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -54,7 +54,6 @@ pub struct Toolbar { active_item: Option>, hidden: bool, can_navigate: bool, - pane: Option>, items: Vec<(Box, ToolbarItemLocation)>, } @@ -144,63 +143,10 @@ impl View for Toolbar { } } -#[allow(clippy::too_many_arguments)] -fn nav_button)>( - svg_path: &'static str, - style: theme::Interactive, - nav_button_height: f32, - tooltip_style: TooltipStyle, - enabled: bool, - spacing: f32, - on_click: F, - tooltip_action: A, - action_name: &str, - cx: &mut ViewContext, -) -> AnyElement { - MouseEventHandler::::new(0, cx, |state, _| { - let style = if enabled { - style.style_for(state) - } else { - style.disabled_style() - }; - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(nav_button_height) - .aligned() - .top() - }) - .with_cursor_style(if enabled { - CursorStyle::PointingHand - } else { - CursorStyle::default() - }) - .on_click(MouseButton::Left, move |_, toolbar, cx| { - on_click(toolbar, cx) - }) - .with_tooltip::( - 0, - action_name.to_string(), - Some(Box::new(tooltip_action)), - tooltip_style, - cx, - ) - .contained() - .with_margin_right(spacing) - .into_any_named("nav button") -} - impl Toolbar { - pub fn new(pane: Option>) -> Self { + pub fn new() -> Self { Self { active_item: None, - pane, items: Default::default(), hidden: false, can_navigate: true, diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index f27ae4b2e6..73b9e82d0c 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -100,7 +100,7 @@ export default function tab_bar(): any { color: foreground(theme.highest, "on", "disabled"), }, }, - }); + }) const dragged_tab = { ...active_pane_active_tab, background: with_opacity(tab.background, 0.9), diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 5aee3c987d..c78b9b2909 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -132,23 +132,6 @@ export default function workspace(): any { background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, - nav_button: interactive({ - base: { - color: foreground(theme.highest, "on"), - icon_width: 12, - button_width: 24, - corner_radius: 6, - }, - state: { - hovered: { - color: foreground(theme.highest, "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - }, - disabled: { - color: foreground(theme.highest, "on", "disabled"), - }, - }, - }), padding: { left: 8, right: 8, top: 4, bottom: 4 }, }, breadcrumb_height: 24, From 822b1ec002dd2449672f3fa71b6e417276bdd9e7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:12:08 +0200 Subject: [PATCH 069/326] Clean up compiler warnings Co-authored-by: Kyle --- crates/search/src/project_search.rs | 2 +- crates/workspace/src/toolbar.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 03e9f47d8d..264ca0bd46 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1426,7 +1426,7 @@ impl ProjectSearchBar { fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { + search_view.update(cx, |search_view, _cx| { search_view.toggle_search_option(option); }); cx.notify(); diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index a7c00cbe0f..e8c1240d43 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -1,7 +1,7 @@ -use crate::{ItemHandle, Pane}; +use crate::ItemHandle; use gpui::{ - elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle, - AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + elements::*, AnyElement, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle, + WindowContext, }; pub trait ToolbarItemView: View { From 358e4e5ccf626f28456d8901a112353bfcdf0eab Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 3 Aug 2023 10:22:31 -0400 Subject: [PATCH 070/326] update minor text when actively searching --- crates/search/src/project_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 264ca0bd46..b8ff3d94c5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -419,7 +419,7 @@ impl View for ProjectSearchView { }; let minor_text = if let Some(no_results) = model.no_results { - if no_results { + if model.pending_search.is_none() && no_results { vec!["No results found in this project for the provided query".to_owned()] } else { vec![] From 8dd330a160586a199e51d18f889542d6d552cc0a Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 3 Aug 2023 12:52:20 -0400 Subject: [PATCH 071/326] add semantic search prompt for the indexing permission on first search Co-authored-by: Piotr --- crates/search/src/project_search.rs | 132 +++++++++++++------- crates/semantic_index/src/semantic_index.rs | 7 +- 2 files changed, 93 insertions(+), 46 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b8ff3d94c5..054eb5bee7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,7 +2,7 @@ use crate::{ NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; -use anyhow::Context; +use anyhow::{Context, Result}; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, @@ -13,6 +13,7 @@ use gpui::color::Color; use gpui::geometry::rect::RectF; use gpui::geometry::vector::IntoVector2F; use gpui::json::{self, ToJson}; +use gpui::platform::PromptLevel; use gpui::SceneBuilder; use gpui::{ actions, @@ -127,7 +128,8 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, - semantic: Option, + semantic_state: Option, + semantic_permissioned: bool, search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, @@ -402,7 +404,7 @@ impl View for ProjectSearchView { } }; - let semantic_status = if let Some(semantic) = &self.semantic { + let semantic_status = if let Some(semantic) = &self.semantic_state { if semantic.outstanding_file_count > 0 { let dots_count = semantic.outstanding_file_count % 3 + 1; let dots: String = std::iter::repeat('.').take(dots_count).collect(); @@ -709,65 +711,108 @@ impl ProjectSearchView { fn toggle_search_option(&mut self, option: SearchOptions) { self.search_options.toggle(option); } + + fn index_project(&mut self, cx: &mut ViewContext) { + if let Some(semantic_index) = SemanticIndex::global(cx) { + // Semantic search uses no options + self.search_options = SearchOptions::none(); + + let project = self.model.read(cx).project.clone(); + let index_task = semantic_index.update(cx, |semantic_index, cx| { + semantic_index.index_project(project, cx) + }); + + cx.spawn(|search_view, mut cx| async move { + let (files_to_index, mut files_remaining_rx) = index_task.await?; + + search_view.update(&mut cx, |search_view, cx| { + cx.notify(); + search_view.semantic_state = Some(SemanticSearchState { + file_count: files_to_index, + outstanding_file_count: files_to_index, + _progress_task: cx.spawn(|search_view, mut cx| async move { + while let Some(count) = files_remaining_rx.recv().await { + search_view + .update(&mut cx, |search_view, cx| { + if let Some(semantic_search_state) = + &mut search_view.semantic_state + { + semantic_search_state.outstanding_file_count = count; + cx.notify(); + if count == 0 { + return; + } + } + }) + .ok(); + } + }), + }); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { self.model.update(cx, |model, _| model.kill_search()); self.current_mode = mode; match mode { SearchMode::Semantic => { - if let Some(semantic_index) = SemanticIndex::global(cx) { - // Semantic search uses no options - self.search_options = SearchOptions::none(); - + // let semantic_permissioned = self.semantic_permissioned.await; + // if semantic_permissioned.is_ok_and(|permission| !permission) { + if !self.semantic_permissioned { + // TODO: Change this to read from the project name let project = self.model.read(cx).project.clone(); - let index_task = semantic_index.update(cx, |semantic_index, cx| { - semantic_index.index_project(project, cx) - }); + 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 semantic search? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + let mut answer = cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ); cx.spawn(|search_view, mut cx| async move { - let (files_to_index, mut files_remaining_rx) = index_task.await?; - - search_view.update(&mut cx, |search_view, cx| { - cx.notify(); - search_view.semantic = Some(SemanticSearchState { - file_count: files_to_index, - outstanding_file_count: files_to_index, - _progress_task: cx.spawn(|search_view, mut cx| async move { - while let Some(count) = files_remaining_rx.recv().await { - search_view - .update(&mut cx, |search_view, cx| { - if let Some(semantic_search_state) = - &mut search_view.semantic - { - semantic_search_state.outstanding_file_count = - count; - cx.notify(); - if count == 0 { - return; - } - } - }) - .ok(); - } - }), + if answer.next().await == Some(0) { + search_view.update(&mut cx, |search_view, cx| { + search_view.semantic_permissioned = true; + search_view.index_project(cx); + })?; + anyhow::Ok(()) + } else { + search_view.update(&mut cx, |search_view, cx| { + search_view.activate_search_mode(SearchMode::Regex, cx); }); - })?; - anyhow::Ok(()) + anyhow::Ok(()) + } }) .detach_and_log_err(cx); + } else { + self.index_project(cx); } } SearchMode::Regex => { if !self.is_option_enabled(SearchOptions::REGEX) { self.toggle_search_option(SearchOptions::REGEX); } - self.semantic = None; + self.semantic_state = None; } SearchMode::Text => { if self.is_option_enabled(SearchOptions::REGEX) { self.toggle_search_option(SearchOptions::REGEX); } - self.semantic = None; + self.semantic_state = None; } } cx.notify(); @@ -856,12 +901,15 @@ impl ProjectSearchView { }) .detach(); let filters_enabled = false; + + // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { search_id: model.read(cx).search_id, model, query_editor, results_editor, - semantic: None, + semantic_state: None, + semantic_permissioned: false, search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, @@ -953,7 +1001,7 @@ impl ProjectSearchView { let mode = self.current_mode; match mode { SearchMode::Semantic => { - if let Some(semantic) = &mut self.semantic { + if let Some(semantic) = &mut self.semantic_state { if semantic.outstanding_file_count > 0 { return; } @@ -1747,7 +1795,7 @@ impl View for ProjectSearchBar { .into_any() }; let search = _search.read(cx); - let is_semantic_disabled = search.semantic.is_none(); + let is_semantic_disabled = search.semantic_state.is_none(); let case_sensitive = if is_semantic_disabled { Some(self.render_option_button_icon( diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 851c656d9a..772dff9af7 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -16,7 +16,7 @@ use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; use postage::watch; -use project::{search::PathMatcher, Fs, Project, WorktreeId}; +use project::{project_settings, search::PathMatcher, Fs, Project, WorktreeId}; use smol::channel; use std::{ cmp::Ordering, @@ -49,9 +49,8 @@ pub fn init( .join(Path::new(RELEASE_CHANNEL_NAME.as_str())) .join("embeddings_db"); - if *RELEASE_CHANNEL == ReleaseChannel::Stable - || !settings::get::(cx).enabled - { + // This needs to be removed at some point before stable. + if *RELEASE_CHANNEL == ReleaseChannel::Stable { return; } From 799adf6c25e8a4dd938c4f6d0252b1d54a9751a3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:18:14 +0200 Subject: [PATCH 072/326] Query Semantic Index Engine for permission state. Co-authored-by: Kyle --- crates/search/src/project_search.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 054eb5bee7..1a2320bda3 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,7 +2,7 @@ use crate::{ NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; -use anyhow::{Context, Result}; +use anyhow::Context; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, @@ -793,7 +793,7 @@ impl ProjectSearchView { } else { search_view.update(&mut cx, |search_view, cx| { search_view.activate_search_mode(SearchMode::Regex, cx); - }); + })?; anyhow::Ok(()) } }) @@ -851,7 +851,7 @@ impl ProjectSearchView { .detach(); let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx); + let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); editor.set_searchable(false); editor }); @@ -901,6 +901,14 @@ impl ProjectSearchView { }) .detach(); let filters_enabled = false; + let semantic_permissioned = SemanticIndex::global(cx) + .and_then(|semantic| { + smol::block_on( + semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx)), + ) + .ok() + }) + .unwrap_or_default(); // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { @@ -909,7 +917,7 @@ impl ProjectSearchView { query_editor, results_editor, semantic_state: None, - semantic_permissioned: false, + semantic_permissioned, search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, From d157e3598d2431b6a6aadd4fdd16ffc0897b51ad Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:27:27 +0200 Subject: [PATCH 073/326] Query semantic_permissioned on demand. Co-authored-by: Kyle --- crates/search/src/project_search.rs | 31 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1a2320bda3..014ec13197 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -129,7 +129,7 @@ pub struct ProjectSearchView { query_editor: ViewHandle, results_editor: ViewHandle, semantic_state: Option, - semantic_permissioned: bool, + semantic_permissioned: Option, search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, @@ -763,7 +763,7 @@ impl ProjectSearchView { SearchMode::Semantic => { // let semantic_permissioned = self.semantic_permissioned.await; // if semantic_permissioned.is_ok_and(|permission| !permission) { - if !self.semantic_permissioned { + if !self.semantic_permissioned(cx) { // TODO: Change this to read from the project name let project = self.model.read(cx).project.clone(); let project_name = project @@ -786,7 +786,7 @@ impl ProjectSearchView { cx.spawn(|search_view, mut cx| async move { if answer.next().await == Some(0) { search_view.update(&mut cx, |search_view, cx| { - search_view.semantic_permissioned = true; + search_view.semantic_permissioned = Some(true); search_view.index_project(cx); })?; anyhow::Ok(()) @@ -901,14 +901,6 @@ impl ProjectSearchView { }) .detach(); let filters_enabled = false; - let semantic_permissioned = SemanticIndex::global(cx) - .and_then(|semantic| { - smol::block_on( - semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx)), - ) - .ok() - }) - .unwrap_or_default(); // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { @@ -917,7 +909,7 @@ impl ProjectSearchView { query_editor, results_editor, semantic_state: None, - semantic_permissioned, + semantic_permissioned: None, search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, @@ -930,7 +922,20 @@ impl ProjectSearchView { this.model_changed(cx); this } - + fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> bool { + *self.semantic_permissioned.get_or_insert_with(|| { + SemanticIndex::global(cx) + .and_then(|semantic| { + let project = self.model.read(cx).project.clone(); + smol::block_on( + semantic + .update(cx, |this, cx| this.project_previously_indexed(project, cx)), + ) + .ok() + }) + .unwrap_or_default() + }) + } pub fn new_search_in_directory( workspace: &mut Workspace, dir_entry: &Entry, From d450c4be9a0c051c40041d1ba803fc229e215d4f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 3 Aug 2023 10:59:09 -0700 Subject: [PATCH 074/326] WIP: add custom channel modal --- crates/client/src/channel_store.rs | 4 +-- crates/collab_ui/src/panel.rs | 23 ++++++++++++- crates/collab_ui/src/panel/channel_modal.rs | 36 ++++++++++++++++++--- crates/theme/src/theme.rs | 6 ++++ styles/src/style_tree/channel_modal.ts | 9 ++++++ styles/src/style_tree/collab_panel.ts | 2 ++ 6 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 styles/src/style_tree/channel_modal.ts diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 534bd0b05a..1d3ed24d1b 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -6,8 +6,8 @@ use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; -type ChannelId = u64; -type UserId = u64; +pub type ChannelId = u64; +pub type UserId = u64; pub struct ChannelStore { channels: Vec>, diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 667e8d3a5c..4092351a75 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -42,6 +42,8 @@ use workspace::{ use crate::face_pile::FacePile; +use self::channel_modal::ChannelModal; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { channel_id: u64, @@ -52,9 +54,14 @@ struct NewChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct AddMember { + channel_id: u64, +} + actions!(collab_panel, [ToggleFocus]); -impl_actions!(collab_panel, [RemoveChannel, NewChannel]); +impl_actions!(collab_panel, [RemoveChannel, NewChannel, AddMember]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -69,6 +76,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::confirm); cx.add_action(CollabPanel::remove_channel); cx.add_action(CollabPanel::new_subchannel); + cx.add_action(CollabPanel::add_member); } #[derive(Debug, Default)] @@ -1506,6 +1514,7 @@ impl CollabPanel { vec![ ContextMenuItem::action("New Channel", NewChannel { channel_id }), ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), + ContextMenuItem::action("Add member", AddMember { channel_id }), ], cx, ); @@ -1668,6 +1677,18 @@ impl CollabPanel { cx.notify(); } + fn add_member(&mut self, action: &AddMember, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |_, cx| { + cx.add_view(|cx| { + ChannelModal::new(action.channel_id, self.channel_store.clone(), cx) + }) + }) + }); + } + } + fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { let channel_id = action.channel_id; let channel_store = self.channel_store.clone(); diff --git a/crates/collab_ui/src/panel/channel_modal.rs b/crates/collab_ui/src/panel/channel_modal.rs index aa1b3e5a13..96424114c7 100644 --- a/crates/collab_ui/src/panel/channel_modal.rs +++ b/crates/collab_ui/src/panel/channel_modal.rs @@ -1,5 +1,8 @@ +use client::{ChannelId, ChannelStore}; use editor::Editor; -use gpui::{elements::*, AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; +use gpui::{ + elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle, +}; use menu::Cancel; use workspace::{item::ItemHandle, Modal}; @@ -10,6 +13,10 @@ pub fn init(cx: &mut AppContext) { pub struct ChannelModal { has_focus: bool, filter_editor: ViewHandle, + selection: usize, + list_state: ListState, + channel_store: ModelHandle, + channel_id: ChannelId, } pub enum Event { @@ -21,16 +28,28 @@ impl Entity for ChannelModal { } impl ChannelModal { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new( + channel_id: ChannelId, + channel_store: ModelHandle, + cx: &mut ViewContext, + ) -> Self { let input_editor = cx.add_view(|cx| { let mut editor = Editor::single_line(None, cx); - editor.set_placeholder_text("Create or add a channel", cx); + editor.set_placeholder_text("Add a member", cx); editor }); + let list_state = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { + Empty::new().into_any() + }); + ChannelModal { has_focus: false, filter_editor: input_editor, + selection: 0, + list_state, + channel_id, + channel_store, } } @@ -49,14 +68,21 @@ impl View for ChannelModal { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let style = theme::current(cx).editor.hint_diagnostic.message.clone(); + let theme = theme::current(cx).clone(); + let style = &theme.collab_panel.modal; let modal_container = theme::current(cx).picker.container.clone(); enum ChannelModal {} MouseEventHandler::::new(0, cx, |_, cx| { Flex::column() .with_child(ChildView::new(self.filter_editor.as_any(), cx)) - .with_child(Label::new("ADD OR BROWSE CHANNELS HERE", style)) + .with_child( + List::new(self.list_state.clone()) + .constrained() + .with_width(style.width) + .flex(1., true) + .into_any(), + ) .contained() .with_style(modal_container) .constrained() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 96eac81a50..8f0ceeab88 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,6 +220,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub modal: ChannelModal, pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub leave_call_button: IconButton, @@ -244,6 +245,11 @@ pub struct CollabPanel { pub face_overlap: f32, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct ChannelModal { + pub width: f32, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct ProjectRow { #[serde(flatten)] diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts new file mode 100644 index 0000000000..95ae337cbc --- /dev/null +++ b/styles/src/style_tree/channel_modal.ts @@ -0,0 +1,9 @@ +import { useTheme } from "../theme" + +export default function contacts_panel(): any { + const theme = useTheme() + + return { + width: 100, + } +} diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 3390dd51f8..37145d0c46 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -7,6 +7,7 @@ import { } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +import channel_modal from "./channel_modal" export default function contacts_panel(): any { @@ -51,6 +52,7 @@ export default function contacts_panel(): any { } return { + modal: channel_modal(), background: background(layer), padding: { top: 12, From 31fb5034180242d6036d377cab8fc670656a7cf6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:26:56 +0200 Subject: [PATCH 075/326] Rewrite permission queries (it no longer blocks) Co-authored-by: Kyle Co-authored-by: Max --- crates/search/src/project_search.rs | 99 +++++++++++---------- crates/semantic_index/src/semantic_index.rs | 14 --- 2 files changed, 51 insertions(+), 62 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 014ec13197..0fc13d992c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,7 +2,7 @@ use crate::{ NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; -use anyhow::Context; +use anyhow::{Context, Result}; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, @@ -761,46 +761,50 @@ impl ProjectSearchView { match mode { SearchMode::Semantic => { - // let semantic_permissioned = self.semantic_permissioned.await; - // if semantic_permissioned.is_ok_and(|permission| !permission) { - if !self.semantic_permissioned(cx) { - // TODO: Change this to read from the project name - let project = self.model.read(cx).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 semantic search? This requires sending code to the OpenAI API", project_name, - if is_plural { - "s" - } else {""}); - let mut answer = cx.prompt( - PromptLevel::Info, - prompt_text.as_str(), - &["Continue", "Cancel"], - ); + let has_permission = self.semantic_permissioned(cx); + cx.spawn(|this, mut cx| async move { + let has_permission = has_permission.await?; + + if !has_permission { + let mut answer = this.update(&mut cx, |this, cx| { + let project = this.model.read(cx).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 semantic search? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; - cx.spawn(|search_view, mut cx| async move { if answer.next().await == Some(0) { - search_view.update(&mut cx, |search_view, cx| { - search_view.semantic_permissioned = Some(true); - search_view.index_project(cx); + this.update(&mut cx, |this, cx| { + this.semantic_permissioned = Some(true); })?; - anyhow::Ok(()) } else { - search_view.update(&mut cx, |search_view, cx| { - search_view.activate_search_mode(SearchMode::Regex, cx); + this.update(&mut cx, |this, cx| { + this.semantic_permissioned = Some(false); + this.activate_search_mode(SearchMode::Regex, cx); })?; - anyhow::Ok(()) + return anyhow::Ok(()); } - }) - .detach_and_log_err(cx); - } else { - self.index_project(cx); - } + } + + this.update(&mut cx, |this, cx| { + this.index_project(cx); + })?; + + anyhow::Ok(()) + }).detach_and_log_err(cx); } SearchMode::Regex => { if !self.is_option_enabled(SearchOptions::REGEX) { @@ -922,19 +926,18 @@ impl ProjectSearchView { this.model_changed(cx); this } - fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> bool { - *self.semantic_permissioned.get_or_insert_with(|| { - SemanticIndex::global(cx) - .and_then(|semantic| { - let project = self.model.read(cx).project.clone(); - smol::block_on( - semantic - .update(cx, |this, cx| this.project_previously_indexed(project, cx)), - ) - .ok() - }) - .unwrap_or_default() - }) + + fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + SemanticIndex::global(cx) + .map(|semantic| { + let project = self.model.read(cx).project.clone(); + semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) } pub fn new_search_in_directory( workspace: &mut Workspace, diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 772dff9af7..50b871d454 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -500,26 +500,12 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { - let worktree_scans_complete = project - .read(cx) - .worktrees(cx) - .map(|worktree| { - let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete(); - async move { - scan_complete.await; - } - }) - .collect::>(); - let worktrees_indexed_previously = project .read(cx) .worktrees(cx) .map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path())) .collect::>(); - cx.spawn(|_, _cx| async move { - futures::future::join_all(worktree_scans_complete).await; - let worktree_indexed_previously = futures::future::join_all(worktrees_indexed_previously).await; From 4658bc610c1bc6fe31ea12ef0e9ecbe87d5f3e4c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:35:10 +0200 Subject: [PATCH 076/326] Update styling of nav buttons (width, corner_radius) Co-authored-by: Kyle --- styles/src/style_tree/tab_bar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 73b9e82d0c..81e1f7adf3 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -88,8 +88,8 @@ export default function tab_bar(): any { base: { color: foreground(theme.highest, "on"), icon_width: 12, - button_width: 24, - corner_radius: 6, + + button_width: active_pane_active_tab.height, }, state: { hovered: { From 6c4964f0710b0a0c51ffec138e8f3d1df05175a2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 3 Aug 2023 11:40:55 -0700 Subject: [PATCH 077/326] WIP: continue channel management modal and rename panel to collab_panel --- crates/client/src/channel_store.rs | 25 +++ .../src/{panel.rs => collab_panel.rs} | 9 +- .../src/collab_panel/channel_modal.rs | 178 ++++++++++++++++++ .../{panel => collab_panel}/contact_finder.rs | 0 .../{panel => collab_panel}/panel_settings.rs | 0 crates/collab_ui/src/collab_ui.rs | 4 +- crates/collab_ui/src/panel/channel_modal.rs | 119 ------------ crates/rpc/proto/zed.proto | 10 + crates/rpc/src/proto.rs | 5 +- crates/theme/src/theme.rs | 9 +- crates/zed/src/zed.rs | 6 +- styles/src/style_tree/channel_modal.ts | 67 ++++++- styles/src/style_tree/collab_panel.ts | 2 +- 13 files changed, 303 insertions(+), 131 deletions(-) rename crates/collab_ui/src/{panel.rs => collab_panel.rs} (99%) create mode 100644 crates/collab_ui/src/collab_panel/channel_modal.rs rename crates/collab_ui/src/{panel => collab_panel}/contact_finder.rs (100%) rename crates/collab_ui/src/{panel => collab_panel}/panel_settings.rs (100%) delete mode 100644 crates/collab_ui/src/panel/channel_modal.rs diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 1d3ed24d1b..fcd0083c3b 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -30,6 +30,11 @@ impl Entity for ChannelStore { type Event = (); } +pub enum ChannelMemberStatus { + Invited, + Member, +} + impl ChannelStore { pub fn new( client: Arc, @@ -115,6 +120,26 @@ impl ChannelStore { } } + pub fn get_channel_members( + &self, + channel_id: ChannelId, + ) -> impl Future>> { + let client = self.client.clone(); + async move { + let response = client + .request(proto::GetChannelMembers { channel_id }) + .await?; + let mut result = HashMap::default(); + for member_id in response.members { + result.insert(member_id, ChannelMemberStatus::Member); + } + for invitee_id in response.invited_members { + result.insert(invitee_id, ChannelMemberStatus::Invited); + } + Ok(result) + } + } + pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { let client = self.client.clone(); async move { diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/collab_panel.rs similarity index 99% rename from crates/collab_ui/src/panel.rs rename to crates/collab_ui/src/collab_panel.rs index 4092351a75..daad527979 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -42,7 +42,7 @@ use workspace::{ use crate::face_pile::FacePile; -use self::channel_modal::ChannelModal; +use self::channel_modal::build_channel_modal; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { @@ -1682,7 +1682,12 @@ impl CollabPanel { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(cx, |_, cx| { cx.add_view(|cx| { - ChannelModal::new(action.channel_id, self.channel_store.clone(), cx) + build_channel_modal( + self.user_store.clone(), + self.channel_store.clone(), + action.channel_id, + cx, + ) }) }) }); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs new file mode 100644 index 0000000000..0cf24dbaf5 --- /dev/null +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -0,0 +1,178 @@ +use client::{ + ChannelId, ChannelMemberStatus, ChannelStore, ContactRequestStatus, User, UserId, UserStore, +}; +use collections::HashMap; +use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; +use picker::{Picker, PickerDelegate, PickerEvent}; +use std::sync::Arc; +use util::TryFutureExt; + +pub fn init(cx: &mut AppContext) { + Picker::::init(cx); +} + +pub type ChannelModal = Picker; + +pub fn build_channel_modal( + user_store: ModelHandle, + channel_store: ModelHandle, + channel: ChannelId, + cx: &mut ViewContext, +) -> ChannelModal { + Picker::new( + ChannelModalDelegate { + potential_contacts: Arc::from([]), + selected_index: 0, + user_store, + channel_store, + channel_id: channel, + member_statuses: Default::default(), + }, + cx, + ) + .with_theme(|theme| theme.picker.clone()) +} + +pub struct ChannelModalDelegate { + potential_contacts: Arc<[Arc]>, + user_store: ModelHandle, + channel_store: ModelHandle, + channel_id: ChannelId, + selected_index: usize, + member_statuses: HashMap, +} + +impl PickerDelegate for ChannelModalDelegate { + fn placeholder_text(&self) -> Arc { + "Search collaborator by username...".into() + } + + fn match_count(&self) -> usize { + self.potential_contacts.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + let search_users = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + + cx.spawn(|picker, mut cx| async move { + async { + let potential_contacts = search_users.await?; + picker.update(&mut cx, |picker, cx| { + picker.delegate_mut().potential_contacts = potential_contacts.into(); + cx.notify(); + })?; + anyhow::Ok(()) + } + .log_err() + .await; + }) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if let Some(user) = self.potential_contacts.get(self.selected_index) { + let user_store = self.user_store.read(cx); + match user_store.contact_request_status(user) { + ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { + self.user_store + .update(cx, |store, cx| store.request_contact(user.id, cx)) + .detach(); + } + ContactRequestStatus::RequestSent => { + self.user_store + .update(cx, |store, cx| store.remove_contact(user.id, cx)) + .detach(); + } + _ => {} + } + } + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + cx.emit(PickerEvent::Dismiss); + } + + fn render_header( + &self, + cx: &mut ViewContext>, + ) -> Option>> { + let theme = &theme::current(cx).collab_panel.channel_modal; + + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .map(|channel| { + Label::new( + format!("Add members for #{}", channel.name), + theme.picker.item.default_style().label.clone(), + ) + .into_any() + }) + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> AnyElement> { + let theme = &theme::current(cx).collab_panel.channel_modal; + let user = &self.potential_contacts[ix]; + let request_status = self.member_statuses.get(&user.id); + + let icon_path = match request_status { + Some(ChannelMemberStatus::Member) => Some("icons/check_8.svg"), + Some(ChannelMemberStatus::Invited) => Some("icons/x_mark_8.svg"), + None => None, + }; + let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { + &theme.disabled_contact_button + } else { + &theme.contact_button + }; + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::from_data(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left() + })) + .with_child( + Label::new(user.github_login.clone(), style.label.clone()) + .contained() + .with_style(theme.contact_username) + .aligned() + .left(), + ) + .with_children(icon_path.map(|icon_path| { + Svg::new(icon_path) + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .aligned() + .contained() + .with_style(button_style.container) + .constrained() + .with_width(button_style.button_width) + .with_height(button_style.button_width) + .aligned() + .flex_float() + })) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.row_height) + .into_any() + } +} diff --git a/crates/collab_ui/src/panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs similarity index 100% rename from crates/collab_ui/src/panel/contact_finder.rs rename to crates/collab_ui/src/collab_panel/contact_finder.rs diff --git a/crates/collab_ui/src/panel/panel_settings.rs b/crates/collab_ui/src/collab_panel/panel_settings.rs similarity index 100% rename from crates/collab_ui/src/panel/panel_settings.rs rename to crates/collab_ui/src/collab_panel/panel_settings.rs diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index c42ed34de6..1e48026f46 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,9 +1,9 @@ +pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; mod face_pile; mod incoming_call_notification; mod notifications; -pub mod panel; mod project_shared_notification; mod sharing_status_indicator; @@ -22,7 +22,7 @@ actions!( pub fn init(app_state: &Arc, cx: &mut AppContext) { vcs_menu::init(cx); collab_titlebar_item::init(cx); - panel::init(app_state.client.clone(), cx); + collab_panel::init(app_state.client.clone(), cx); incoming_call_notification::init(&app_state, cx); project_shared_notification::init(&app_state, cx); sharing_status_indicator::init(cx); diff --git a/crates/collab_ui/src/panel/channel_modal.rs b/crates/collab_ui/src/panel/channel_modal.rs deleted file mode 100644 index 96424114c7..0000000000 --- a/crates/collab_ui/src/panel/channel_modal.rs +++ /dev/null @@ -1,119 +0,0 @@ -use client::{ChannelId, ChannelStore}; -use editor::Editor; -use gpui::{ - elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle, -}; -use menu::Cancel; -use workspace::{item::ItemHandle, Modal}; - -pub fn init(cx: &mut AppContext) { - cx.add_action(ChannelModal::cancel) -} - -pub struct ChannelModal { - has_focus: bool, - filter_editor: ViewHandle, - selection: usize, - list_state: ListState, - channel_store: ModelHandle, - channel_id: ChannelId, -} - -pub enum Event { - Dismiss, -} - -impl Entity for ChannelModal { - type Event = Event; -} - -impl ChannelModal { - pub fn new( - channel_id: ChannelId, - channel_store: ModelHandle, - cx: &mut ViewContext, - ) -> Self { - let input_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line(None, cx); - editor.set_placeholder_text("Add a member", cx); - editor - }); - - let list_state = ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { - Empty::new().into_any() - }); - - ChannelModal { - has_focus: false, - filter_editor: input_editor, - selection: 0, - list_state, - channel_id, - channel_store, - } - } - - pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - self.dismiss(cx); - } - - fn dismiss(&mut self, cx: &mut ViewContext) { - cx.emit(Event::Dismiss) - } -} - -impl View for ChannelModal { - fn ui_name() -> &'static str { - "Channel Modal" - } - - fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let theme = theme::current(cx).clone(); - let style = &theme.collab_panel.modal; - let modal_container = theme::current(cx).picker.container.clone(); - - enum ChannelModal {} - MouseEventHandler::::new(0, cx, |_, cx| { - Flex::column() - .with_child(ChildView::new(self.filter_editor.as_any(), cx)) - .with_child( - List::new(self.list_state.clone()) - .constrained() - .with_width(style.width) - .flex(1., true) - .into_any(), - ) - .contained() - .with_style(modal_container) - .constrained() - .with_max_width(540.) - .with_max_height(420.) - }) - .on_click(gpui::platform::MouseButton::Left, |_, _, _| {}) // Capture click and down events - .on_down_out(gpui::platform::MouseButton::Left, |_, v, cx| v.dismiss(cx)) - .into_any_named("channel modal") - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - if cx.is_self_focused() { - cx.focus(&self.filter_editor); - } - } - - fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } -} - -impl Modal for ChannelModal { - fn has_focus(&self) -> bool { - self.has_focus - } - - fn dismiss_on_event(event: &Self::Event) -> bool { - match event { - Event::Dismiss => true, - } - } -} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c4fb5aa653..1fdeef98f0 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -138,6 +138,8 @@ message Envelope { UpdateChannels update_channels = 124; JoinChannel join_channel = 126; RemoveChannel remove_channel = 127; + GetChannelMembers get_channel_members = 128; + GetChannelMembersResponse get_channel_members_response = 129; } } @@ -886,6 +888,14 @@ message RemoveChannel { uint64 channel_id = 1; } +message GetChannelMembers { + uint64 channel_id = 1; +} + +message GetChannelMembersResponse { + repeated uint64 members = 1; + repeated uint64 invited_members = 2; +} message CreateChannel { string name = 1; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 07d54ce4db..c23bbb23e4 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -244,7 +244,9 @@ messages!( (UpdateWorktreeSettings, Foreground), (UpdateDiffBase, Foreground), (GetPrivateUserInfo, Foreground), - (GetPrivateUserInfoResponse, Foreground) + (GetPrivateUserInfoResponse, Foreground), + (GetChannelMembers, Foreground), + (GetChannelMembersResponse, Foreground) ); request_messages!( @@ -296,6 +298,7 @@ request_messages!( (RemoveContact, Ack), (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), + (GetChannelMembers, GetChannelMembersResponse), (JoinChannel, JoinRoomResponse), (RemoveChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8f0ceeab88..c557fbcf52 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,7 +220,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, - pub modal: ChannelModal, + pub channel_modal: ChannelModal, pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub leave_call_button: IconButton, @@ -247,7 +247,12 @@ pub struct CollabPanel { #[derive(Deserialize, Default, JsonSchema)] pub struct ChannelModal { - pub width: f32, + pub picker: Picker, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainerStyle, + pub contact_button: IconButton, + pub disabled_contact_button: IconButton, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a779f39f57..500a82d1ce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -209,9 +209,9 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { ); cx.add_action( |workspace: &mut Workspace, - _: &collab_ui::panel::ToggleFocus, + _: &collab_ui::collab_panel::ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); + workspace.toggle_panel_focus::(cx); }, ); cx.add_action( @@ -333,7 +333,7 @@ pub fn initialize_workspace( let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); let channels_panel = - collab_ui::panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!( project_panel, terminal_panel, diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 95ae337cbc..3eff0e4b9a 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -1,9 +1,74 @@ import { useTheme } from "../theme" +import { background, border, foreground, text } from "./components" +import picker from "./picker" export default function contacts_panel(): any { const theme = useTheme() + const side_margin = 6 + const contact_button = { + background: background(theme.middle, "variant"), + color: foreground(theme.middle, "variant"), + icon_width: 8, + button_width: 16, + corner_radius: 8, + } + + const picker_style = picker() + const picker_input = { + background: background(theme.middle, "on"), + corner_radius: 6, + text: text(theme.middle, "mono"), + placeholder_text: text(theme.middle, "mono", "on", "disabled", { + size: "xs", + }), + selection: theme.players[0], + border: border(theme.middle), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: side_margin, + right: side_margin, + }, + } + return { - width: 100, + picker: { + empty_container: {}, + item: { + ...picker_style.item, + margin: { left: side_margin, right: side_margin }, + }, + no_matches: picker_style.no_matches, + input_editor: picker_input, + empty_input_editor: picker_input, + header: picker_style.header, + footer: picker_style.footer, + }, + row_height: 28, + contact_avatar: { + corner_radius: 10, + width: 18, + }, + contact_username: { + padding: { + left: 8, + }, + }, + contact_button: { + ...contact_button, + hover: { + background: background(theme.middle, "variant", "hovered"), + }, + }, + disabled_contact_button: { + ...contact_button, + background: background(theme.middle, "disabled"), + color: foreground(theme.middle, "disabled"), + }, } } diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 37145d0c46..ea550dea6b 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -52,7 +52,7 @@ export default function contacts_panel(): any { } return { - modal: channel_modal(), + channel_modal: channel_modal(), background: background(layer), padding: { top: 12, From 9a1dd0c6bc3680cbf0e81f0b1864d7fbc068efef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 3 Aug 2023 12:10:53 -0700 Subject: [PATCH 078/326] Fetch channel members before constructing channel mgmt modal --- crates/client/src/channel_store.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 24 ++++++++++++------- .../src/collab_panel/channel_modal.rs | 3 ++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index fcd0083c3b..a1ee7ad6bc 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -123,7 +123,7 @@ impl ChannelStore { pub fn get_channel_members( &self, channel_id: ChannelId, - ) -> impl Future>> { + ) -> impl 'static + Future>> { let client = self.client.clone(); async move { let response = client diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index daad527979..34cb4f3e91 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1678,20 +1678,28 @@ impl CollabPanel { } fn add_member(&mut self, action: &AddMember, cx: &mut ViewContext) { - if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { + let channel_id = action.channel_id; + let workspace = self.workspace.clone(); + let user_store = self.user_store.clone(); + let channel_store = self.channel_store.clone(); + let members = self.channel_store.read(cx).get_channel_members(channel_id); + cx.spawn(|_, mut cx| async move { + let members = members.await?; + workspace.update(&mut cx, |workspace, cx| { workspace.toggle_modal(cx, |_, cx| { cx.add_view(|cx| { build_channel_modal( - self.user_store.clone(), - self.channel_store.clone(), - action.channel_id, + user_store.clone(), + channel_store.clone(), + channel_id, + members, cx, ) }) - }) - }); - } + }); + }) + }) + .detach(); } fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 0cf24dbaf5..164759587d 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -17,6 +17,7 @@ pub fn build_channel_modal( user_store: ModelHandle, channel_store: ModelHandle, channel: ChannelId, + members: HashMap, cx: &mut ViewContext, ) -> ChannelModal { Picker::new( @@ -26,7 +27,7 @@ pub fn build_channel_modal( user_store, channel_store, channel_id: channel, - member_statuses: Default::default(), + member_statuses: members, }, cx, ) From 129f2890c5989357c58eaeb71fc605f396bb050d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 3 Aug 2023 13:26:28 -0700 Subject: [PATCH 079/326] simplify server implementation --- crates/collab/src/rpc.rs | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 15237049c3..7ee2a2ba83 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2277,13 +2277,6 @@ async fn join_channel( let joined_room = { let db = session.db().await; - if db - .is_current_room_different_channel(session.user_id, channel_id) - .await? - { - leave_room_for_session_with_guard(&session, &db).await?; - } - let room_id = db.room_id_for_channel(channel_id).await?; let joined_room = db @@ -2539,14 +2532,7 @@ fn channel_updated( async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> { let db = session.db().await; - update_user_contacts_with_guard(user_id, session, &db).await -} -async fn update_user_contacts_with_guard( - user_id: UserId, - session: &Session, - db: &DbHandle, -) -> Result<()> { let contacts = db.get_contacts(user_id).await?; let busy = db.is_user_busy(user_id).await?; @@ -2580,11 +2566,6 @@ async fn update_user_contacts_with_guard( } async fn leave_room_for_session(session: &Session) -> Result<()> { - let db = session.db().await; - leave_room_for_session_with_guard(session, &db).await -} - -async fn leave_room_for_session_with_guard(session: &Session, db: &DbHandle) -> Result<()> { let mut contacts_to_update = HashSet::default(); let room_id; @@ -2595,7 +2576,7 @@ async fn leave_room_for_session_with_guard(session: &Session, db: &DbHandle) -> let channel_members; let channel_id; - if let Some(mut left_room) = db.leave_room(session.connection_id).await? { + if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? { contacts_to_update.insert(session.user_id); for project in left_room.left_projects.values() { @@ -2645,7 +2626,7 @@ async fn leave_room_for_session_with_guard(session: &Session, db: &DbHandle) -> } for contact_user_id in contacts_to_update { - update_user_contacts_with_guard(contact_user_id, &session, db).await?; + update_user_contacts(contact_user_id, &session).await?; } if let Some(live_kit) = session.live_kit_client.as_ref() { From c14a99d8fa14aaea960c38bc46d622d2cee9056b Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 3 Aug 2023 17:02:46 -0400 Subject: [PATCH 080/326] updated project_search text --- crates/search/src/project_search.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0fc13d992c..74635bf53c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -398,7 +398,7 @@ impl View for ProjectSearchView { match current_mode { SearchMode::Text => Cow::Borrowed("Text search all files and folders"), SearchMode::Semantic => { - Cow::Borrowed("Search all files and folders using Natural Language") + Cow::Borrowed("Search all code objects using Natural Language") } SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), } @@ -429,10 +429,13 @@ impl View for ProjectSearchView { } else { match current_mode { SearchMode::Semantic => vec![ + "".to_owned(), semantic_status, - "ex. 'list all available languages'".to_owned(), + "Simply explain the code you are looking to find.".to_owned(), + "ex. 'prompt user for permissions to index their project'".to_owned(), ], _ => vec![ + "".to_owned(), "Include/exclude specific paths with the filter option.".to_owned(), "Matching exact word and/or casing is available too.".to_owned(), ], From a7e883d956852ef761edcb64096490c95ba0f4a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 3 Aug 2023 14:49:01 -0700 Subject: [PATCH 081/326] Implement basic channel member management UI Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 80 +++++--- crates/collab/src/db.rs | 80 ++++++-- crates/collab/src/db/tests.rs | 45 ++++- crates/collab/src/rpc.rs | 13 ++ crates/collab/src/tests.rs | 9 +- crates/collab/src/tests/channel_tests.rs | 45 ++++- crates/collab_ui/src/collab_panel.rs | 10 +- .../src/collab_panel/channel_modal.rs | 172 +++++++++++++----- crates/rpc/proto/zed.proto | 14 +- 9 files changed, 368 insertions(+), 100 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index a1ee7ad6bc..8568317355 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -1,6 +1,8 @@ use crate::{Client, Subscription, User, UserStore}; +use anyhow::anyhow; use anyhow::Result; use collections::HashMap; +use collections::HashSet; use futures::Future; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, TypedEnvelope}; @@ -13,6 +15,7 @@ pub struct ChannelStore { channels: Vec>, channel_invitations: Vec>, channel_participants: HashMap>>, + outgoing_invites: HashSet<(ChannelId, UserId)>, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -33,6 +36,7 @@ impl Entity for ChannelStore { pub enum ChannelMemberStatus { Invited, Member, + NotMember, } impl ChannelStore { @@ -48,6 +52,7 @@ impl ChannelStore { channels: vec![], channel_invitations: vec![], channel_participants: Default::default(), + outgoing_invites: Default::default(), client, user_store, _rpc_subscription: rpc_subscription, @@ -88,13 +93,19 @@ impl ChannelStore { } pub fn invite_member( - &self, + &mut self, channel_id: ChannelId, user_id: UserId, admin: bool, - ) -> impl Future> { + cx: &mut ModelContext, + ) -> Task> { + if !self.outgoing_invites.insert((channel_id, user_id)) { + return Task::ready(Err(anyhow!("invite request already in progress"))); + } + + cx.notify(); let client = self.client.clone(); - async move { + cx.spawn(|this, mut cx| async move { client .request(proto::InviteChannelMember { channel_id, @@ -102,8 +113,12 @@ impl ChannelStore { admin, }) .await?; + this.update(&mut cx, |this, cx| { + this.outgoing_invites.remove(&(channel_id, user_id)); + cx.notify(); + }); Ok(()) - } + }) } pub fn respond_to_channel_invite( @@ -120,24 +135,34 @@ impl ChannelStore { } } - pub fn get_channel_members( + pub fn get_channel_member_details( &self, channel_id: ChannelId, - ) -> impl 'static + Future>> { + cx: &mut ModelContext, + ) -> Task, proto::channel_member::Kind)>>> { let client = self.client.clone(); - async move { + let user_store = self.user_store.downgrade(); + cx.spawn(|_, mut cx| async move { let response = client .request(proto::GetChannelMembers { channel_id }) .await?; - let mut result = HashMap::default(); - for member_id in response.members { - result.insert(member_id, ChannelMemberStatus::Member); - } - for invitee_id in response.invited_members { - result.insert(invitee_id, ChannelMemberStatus::Invited); - } - Ok(result) - } + + let user_ids = response.members.iter().map(|m| m.user_id).collect(); + let user_store = user_store + .upgrade(&cx) + .ok_or_else(|| anyhow!("user store dropped"))?; + let users = user_store + .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx)) + .await?; + + Ok(users + .into_iter() + .zip(response.members) + .filter_map(|(user, member)| { + Some((user, proto::channel_member::Kind::from_i32(member.kind)?)) + }) + .collect()) + }) } pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future> { @@ -148,25 +173,22 @@ impl ChannelStore { } } - pub fn is_channel_invite_pending(&self, _: &Arc) -> bool { + pub fn has_pending_channel_invite_response(&self, _: &Arc) -> bool { false } + pub fn has_pending_channel_invite(&self, channel_id: ChannelId, user_id: UserId) -> bool { + self.outgoing_invites.contains(&(channel_id, user_id)) + } + pub fn remove_member( &self, - channel_id: ChannelId, - user_id: u64, - cx: &mut ModelContext, + _channel_id: ChannelId, + _user_id: u64, + _cx: &mut ModelContext, ) -> Task> { - todo!() - } - - pub fn channel_members( - &self, - channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>>> { - todo!() + dbg!("TODO"); + Task::Ready(Some(Ok(()))) } async fn handle_update_channels( diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 36b226b97b..d942b8cab9 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -213,20 +213,21 @@ 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? + let channel_members; + if let Some(channel_id) = channel_id { + channel_members = self.get_channel_members_internal(channel_id, &tx).await?; } else { - Vec::new() - }; + channel_members = Vec::new(); - // Delete the room if it becomes empty. - if room.participants.is_empty() { - project::Entity::delete_many() - .filter(project::Column::RoomId.eq(room_id)) - .exec(&*tx) - .await?; - room::Entity::delete_by_id(room_id).exec(&*tx).await?; - } + // Delete the room if it becomes empty. + if room.participants.is_empty() { + project::Entity::delete_many() + .filter(project::Column::RoomId.eq(room_id)) + .exec(&*tx) + .await?; + room::Entity::delete_by_id(room_id).exec(&*tx).await?; + } + }; Ok(RefreshedRoom { room, @@ -3475,10 +3476,61 @@ 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 }) + .await + } + + // TODO: Add a chekc whether this user is allowed to read this channel + pub async fn get_channel_member_details( + &self, + id: ChannelId, + ) -> Result> { self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryMemberDetails { + UserId, + IsDirectMember, + Accepted, + } + let tx = tx; - let user_ids = self.get_channel_members_internal(id, &*tx).await?; - Ok(user_ids) + let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?; + let mut stream = channel_member::Entity::find() + .distinct() + .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .column_as( + channel_member::Column::ChannelId.eq(id), + QueryMemberDetails::IsDirectMember, + ) + .column(channel_member::Column::Accepted) + .order_by_asc(channel_member::Column::UserId) + .into_values::<_, QueryMemberDetails>() + .stream(&*tx) + .await?; + + let mut rows = Vec::::new(); + while let Some(row) = stream.next().await { + let (user_id, is_direct_member, is_invite_accepted): (UserId, bool, bool) = row?; + let kind = match (is_direct_member, is_invite_accepted) { + (true, true) => proto::channel_member::Kind::Member, + (true, false) => proto::channel_member::Kind::Invitee, + (false, true) => proto::channel_member::Kind::AncestorMember, + (false, false) => continue, + }; + 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 { + last_row.kind = last_row.kind.min(kind); + continue; + } + } + rows.push(proto::ChannelMember { user_id, kind }); + } + + Ok(rows) }) .await } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index a1d1a23dc9..e4161d3b55 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1161,7 +1161,50 @@ test_both_dbs!( .map(|channel| channel.id) .collect::>(); - assert_eq!(user_3_invites, &[channel_1_1]) + assert_eq!(user_3_invites, &[channel_1_1]); + + let members = db.get_channel_member_details(channel_1_1).await.unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + }, + ] + ); + + db.respond_to_channel_invite(channel_1_1, user_2, true) + .await + .unwrap(); + + let channel_1_3 = db + .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .await + .unwrap(); + + let members = db.get_channel_member_details(channel_1_3).await.unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + }, + ] + ); } ); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7ee2a2ba83..fdfccea98f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -246,6 +246,7 @@ impl Server { .add_request_handler(remove_channel) .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) + .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) .add_request_handler(follow) @@ -2236,6 +2237,18 @@ async fn remove_channel_member( Ok(()) } +async fn get_channel_members( + request: proto::GetChannelMembers, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let members = db.get_channel_member_details(channel_id).await?; + response.send(proto::GetChannelMembersResponse { members })?; + Ok(()) +} + async fn respond_to_channel_invite( request: proto::RespondToChannelInvite, response: Response, diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 26ca5a008e..a8e2a12962 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -291,8 +291,13 @@ impl TestServer { admin_client .app_state .channel_store - .update(admin_cx, |channel_store, _| { - channel_store.invite_member(channel_id, member_client.user_id().unwrap(), false) + .update(admin_cx, |channel_store, cx| { + channel_store.invite_member( + channel_id, + member_client.user_id().unwrap(), + false, + cx, + ) }) .await .unwrap(); diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 3999740557..b4f8477a2d 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,6 +1,7 @@ use call::ActiveCall; use client::{Channel, User}; use gpui::{executor::Deterministic, TestAppContext}; +use rpc::proto; use std::sync::Arc; use crate::tests::{room_participants, RoomParticipants}; @@ -46,8 +47,14 @@ async fn test_basic_channels( // Invite client B to channel A as client A. client_a .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.invite_member(channel_a_id, client_b.user_id().unwrap(), false) + .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); + + // Make sure we're synchronously storing the pending invite + assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); + invite }) .await .unwrap(); @@ -66,6 +73,27 @@ async fn test_basic_channels( })] ) }); + let members = client_a + .channel_store() + .update(cx_a, |store, cx| { + assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); + store.get_channel_member_details(channel_a_id, cx) + }) + .await + .unwrap(); + assert_members_eq( + &members, + &[ + ( + client_a.user_id().unwrap(), + proto::channel_member::Kind::Member, + ), + ( + client_b.user_id().unwrap(), + proto::channel_member::Kind::Invitee, + ), + ], + ); // Client B now sees that they are a member channel A. client_b @@ -113,6 +141,19 @@ fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u ); } +fn assert_members_eq( + members: &[(Arc, proto::channel_member::Kind)], + expected_members: &[(u64, proto::channel_member::Kind)], +) { + assert_eq!( + members + .iter() + .map(|(user, status)| (user.id, *status)) + .collect::>(), + expected_members + ); +} + #[gpui::test] async fn test_channel_room( deterministic: Arc, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 34cb4f3e91..771927c8ac 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1333,7 +1333,9 @@ impl CollabPanel { enum Accept {} let channel_id = channel.id; - let is_invite_pending = channel_store.read(cx).is_channel_invite_pending(&channel); + let is_invite_pending = channel_store + .read(cx) + .has_pending_channel_invite_response(&channel); let button_spacing = theme.contact_button_spacing; Flex::row() @@ -1682,7 +1684,10 @@ impl CollabPanel { let workspace = self.workspace.clone(); let user_store = self.user_store.clone(); let channel_store = self.channel_store.clone(); - let members = self.channel_store.read(cx).get_channel_members(channel_id); + let members = self.channel_store.update(cx, |channel_store, cx| { + channel_store.get_channel_member_details(channel_id, cx) + }); + cx.spawn(|_, mut cx| async move { let members = members.await?; workspace.update(&mut cx, |workspace, cx| { @@ -1692,6 +1697,7 @@ impl CollabPanel { user_store.clone(), channel_store.clone(), channel_id, + channel_modal::Mode::InviteMembers, members, cx, ) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 164759587d..e6a3ba9288 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,7 +1,5 @@ -use client::{ - ChannelId, ChannelMemberStatus, ChannelStore, ContactRequestStatus, User, UserId, UserStore, -}; -use collections::HashMap; +use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore}; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; use picker::{Picker, PickerDelegate, PickerEvent}; use std::sync::Arc; @@ -17,30 +15,48 @@ pub fn build_channel_modal( user_store: ModelHandle, channel_store: ModelHandle, channel: ChannelId, - members: HashMap, + mode: Mode, + members: Vec<(Arc, proto::channel_member::Kind)>, cx: &mut ViewContext, ) -> ChannelModal { Picker::new( ChannelModalDelegate { - potential_contacts: Arc::from([]), + matches: Vec::new(), selected_index: 0, user_store, channel_store, channel_id: channel, - member_statuses: members, + match_candidates: members + .iter() + .enumerate() + .map(|(id, member)| StringMatchCandidate { + id, + string: member.0.github_login.clone(), + char_bag: member.0.github_login.chars().collect(), + }) + .collect(), + members, + mode, }, cx, ) .with_theme(|theme| theme.picker.clone()) } +pub enum Mode { + ManageMembers, + InviteMembers, +} + pub struct ChannelModalDelegate { - potential_contacts: Arc<[Arc]>, + matches: Vec<(Arc, Option)>, user_store: ModelHandle, channel_store: ModelHandle, channel_id: ChannelId, selected_index: usize, - member_statuses: HashMap, + mode: Mode, + match_candidates: Arc<[StringMatchCandidate]>, + members: Vec<(Arc, proto::channel_member::Kind)>, } impl PickerDelegate for ChannelModalDelegate { @@ -49,7 +65,7 @@ impl PickerDelegate for ChannelModalDelegate { } fn match_count(&self) -> usize { - self.potential_contacts.len() + self.matches.len() } fn selected_index(&self) -> usize { @@ -61,39 +77,80 @@ impl PickerDelegate for ChannelModalDelegate { } fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { - let search_users = self - .user_store - .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); - - cx.spawn(|picker, mut cx| async move { - async { - let potential_contacts = search_users.await?; - picker.update(&mut cx, |picker, cx| { - picker.delegate_mut().potential_contacts = potential_contacts.into(); - cx.notify(); - })?; - anyhow::Ok(()) + match self.mode { + Mode::ManageMembers => { + let match_candidates = self.match_candidates.clone(); + cx.spawn(|picker, mut cx| async move { + async move { + let matches = match_strings( + &match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + cx.background().clone(), + ) + .await; + picker.update(&mut cx, |picker, cx| { + let delegate = picker.delegate_mut(); + delegate.matches.clear(); + delegate.matches.extend(matches.into_iter().map(|m| { + let member = &delegate.members[m.candidate_id]; + (member.0.clone(), Some(member.1)) + })); + cx.notify(); + })?; + anyhow::Ok(()) + } + .log_err() + .await; + }) } - .log_err() - .await; - }) + Mode::InviteMembers => { + let search_users = self + .user_store + .update(cx, |store, cx| store.fuzzy_search_users(query, cx)); + cx.spawn(|picker, mut cx| async move { + async { + let users = search_users.await?; + picker.update(&mut cx, |picker, cx| { + let delegate = picker.delegate_mut(); + delegate.matches.clear(); + delegate + .matches + .extend(users.into_iter().map(|user| (user, None))); + cx.notify(); + })?; + anyhow::Ok(()) + } + .log_err() + .await; + }) + } + } } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if let Some(user) = self.potential_contacts.get(self.selected_index) { - let user_store = self.user_store.read(cx); - match user_store.contact_request_status(user) { - ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { - self.user_store - .update(cx, |store, cx| store.request_contact(user.id, cx)) - .detach(); + if let Some((user, _)) = self.matches.get(self.selected_index) { + match self.mode { + Mode::ManageMembers => { + // } - ContactRequestStatus::RequestSent => { - self.user_store - .update(cx, |store, cx| store.remove_contact(user.id, cx)) - .detach(); - } - _ => {} + Mode::InviteMembers => match self.member_status(user.id, cx) { + Some(proto::channel_member::Kind::Member) => {} + Some(proto::channel_member::Kind::Invitee) => self + .channel_store + .update(cx, |store, cx| { + store.remove_member(self.channel_id, user.id, cx) + }) + .detach(), + Some(proto::channel_member::Kind::AncestorMember) | None => self + .channel_store + .update(cx, |store, cx| { + store.invite_member(self.channel_id, user.id, false, cx) + }) + .detach(), + }, } } } @@ -108,12 +165,16 @@ impl PickerDelegate for ChannelModalDelegate { ) -> Option>> { let theme = &theme::current(cx).collab_panel.channel_modal; + let operation = match self.mode { + Mode::ManageMembers => "Manage", + Mode::InviteMembers => "Add", + }; self.channel_store .read(cx) .channel_for_id(self.channel_id) .map(|channel| { Label::new( - format!("Add members for #{}", channel.name), + format!("{} members for #{}", operation, channel.name), theme.picker.item.default_style().label.clone(), ) .into_any() @@ -128,19 +189,17 @@ impl PickerDelegate for ChannelModalDelegate { cx: &gpui::AppContext, ) -> AnyElement> { let theme = &theme::current(cx).collab_panel.channel_modal; - let user = &self.potential_contacts[ix]; - let request_status = self.member_statuses.get(&user.id); + let (user, _) = &self.matches[ix]; + let request_status = self.member_status(user.id, cx); let icon_path = match request_status { - Some(ChannelMemberStatus::Member) => Some("icons/check_8.svg"), - Some(ChannelMemberStatus::Invited) => Some("icons/x_mark_8.svg"), + Some(proto::channel_member::Kind::AncestorMember) => Some("icons/check_8.svg"), + Some(proto::channel_member::Kind::Member) => Some("icons/check_8.svg"), + Some(proto::channel_member::Kind::Invitee) => Some("icons/x_mark_8.svg"), None => None, }; - let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { - &theme.disabled_contact_button - } else { - &theme.contact_button - }; + let button_style = &theme.contact_button; + let style = theme.picker.item.in_state(selected).style_for(mouse_state); Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -177,3 +236,20 @@ impl PickerDelegate for ChannelModalDelegate { .into_any() } } + +impl ChannelModalDelegate { + fn member_status( + &self, + user_id: UserId, + cx: &AppContext, + ) -> Option { + self.members + .iter() + .find_map(|(user, status)| (user.id == user_id).then_some(*status)) + .or(self + .channel_store + .read(cx) + .has_pending_channel_invite(self.channel_id, user_id) + .then_some(proto::channel_member::Kind::Invitee)) + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1fdeef98f0..602b34529e 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -893,8 +893,18 @@ message GetChannelMembers { } message GetChannelMembersResponse { - repeated uint64 members = 1; - repeated uint64 invited_members = 2; + repeated ChannelMember members = 1; +} + +message ChannelMember { + uint64 user_id = 1; + Kind kind = 2; + + enum Kind { + Member = 0; + Invitee = 1; + AncestorMember = 2; + } } message CreateChannel { From 4a6c73c6fda413f66ba55eb167fc2649bd21d6ee Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 3 Aug 2023 16:15:29 -0700 Subject: [PATCH 082/326] Lay-out channel modal with picker beneath channel name and mode buttons Co-authored-by: Mikayla --- .../src/collab_panel/channel_modal.rs | 199 +++++++++++++----- crates/picker/src/picker.rs | 1 + crates/theme/src/theme.rs | 4 + styles/src/style_tree/channel_modal.ts | 57 ++++- 4 files changed, 213 insertions(+), 48 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index e6a3ba9288..9af6099f65 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,48 +1,175 @@ use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore}; use fuzzy::{match_strings, StringMatchCandidate}; -use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; +use gpui::{ + elements::*, + platform::{CursorStyle, MouseButton}, + AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, +}; use picker::{Picker, PickerDelegate, PickerEvent}; use std::sync::Arc; use util::TryFutureExt; +use workspace::Modal; pub fn init(cx: &mut AppContext) { Picker::::init(cx); } -pub type ChannelModal = Picker; +pub struct ChannelModal { + picker: ViewHandle>, + channel_store: ModelHandle, + channel_id: ChannelId, + has_focus: bool, +} + +impl Entity for ChannelModal { + type Event = PickerEvent; +} + +impl View for ChannelModal { + fn ui_name() -> &'static str { + "ChannelModal" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &theme::current(cx).collab_panel.channel_modal; + + let mode = self.picker.read(cx).delegate().mode; + let Some(channel) = self + .channel_store + .read(cx) + .channel_for_id(self.channel_id) else { + return Empty::new().into_any() + }; + + enum InviteMembers {} + enum ManageMembers {} + + fn render_mode_button( + mode: Mode, + text: &'static str, + current_mode: Mode, + theme: &theme::ChannelModal, + cx: &mut ViewContext, + ) -> AnyElement { + let active = mode == current_mode; + MouseEventHandler::::new(0, cx, move |state, _| { + let contained_text = theme.mode_button.style_for(active, state); + Label::new(text, contained_text.text.clone()) + .contained() + .with_style(contained_text.container.clone()) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if !active { + this.picker.update(cx, |picker, cx| { + picker.delegate_mut().mode = mode; + picker.update_matches(picker.query(cx), cx); + cx.notify(); + }) + } + }) + .with_cursor_style(if active { + CursorStyle::Arrow + } else { + CursorStyle::PointingHand + }) + .into_any() + } + + Flex::column() + .with_child(Label::new( + format!("#{}", channel.name), + theme.header.clone(), + )) + .with_child(Flex::row().with_children([ + render_mode_button::( + Mode::InviteMembers, + "Invite members", + mode, + theme, + cx, + ), + render_mode_button::( + Mode::ManageMembers, + "Manage members", + mode, + theme, + cx, + ), + ])) + .with_child(ChildView::new(&self.picker, cx)) + .constrained() + .with_height(theme.height) + .contained() + .with_style(theme.container) + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = true; + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl Modal for ChannelModal { + fn has_focus(&self) -> bool { + self.has_focus + } + + fn dismiss_on_event(event: &Self::Event) -> bool { + match event { + PickerEvent::Dismiss => true, + } + } +} pub fn build_channel_modal( user_store: ModelHandle, channel_store: ModelHandle, - channel: ChannelId, + channel_id: ChannelId, mode: Mode, members: Vec<(Arc, proto::channel_member::Kind)>, cx: &mut ViewContext, ) -> ChannelModal { - Picker::new( - ChannelModalDelegate { - matches: Vec::new(), - selected_index: 0, - user_store, - channel_store, - channel_id: channel, - match_candidates: members - .iter() - .enumerate() - .map(|(id, member)| StringMatchCandidate { - id, - string: member.0.github_login.clone(), - char_bag: member.0.github_login.chars().collect(), - }) - .collect(), - members, - mode, - }, - cx, - ) - .with_theme(|theme| theme.picker.clone()) + let picker = cx.add_view(|cx| { + Picker::new( + ChannelModalDelegate { + matches: Vec::new(), + selected_index: 0, + user_store: user_store.clone(), + channel_store: channel_store.clone(), + channel_id, + match_candidates: members + .iter() + .enumerate() + .map(|(id, member)| StringMatchCandidate { + id, + string: member.0.github_login.clone(), + char_bag: member.0.github_login.chars().collect(), + }) + .collect(), + members, + mode, + }, + cx, + ) + .with_theme(|theme| theme.collab_panel.channel_modal.picker.clone()) + }); + + cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); + let has_focus = picker.read(cx).has_focus(); + + ChannelModal { + picker, + channel_store, + channel_id, + has_focus, + } } +#[derive(Copy, Clone, PartialEq)] pub enum Mode { ManageMembers, InviteMembers, @@ -159,28 +286,6 @@ impl PickerDelegate for ChannelModalDelegate { cx.emit(PickerEvent::Dismiss); } - fn render_header( - &self, - cx: &mut ViewContext>, - ) -> Option>> { - let theme = &theme::current(cx).collab_panel.channel_modal; - - let operation = match self.mode { - Mode::ManageMembers => "Manage", - Mode::InviteMembers => "Add", - }; - self.channel_store - .read(cx) - .channel_for_id(self.channel_id) - .map(|channel| { - Label::new( - format!("{} members for #{}", operation, channel.name), - theme.picker.item.default_style().label.clone(), - ) - .into_any() - }) - } - fn render_match( &self, ix: usize, diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 6efa33e961..ef8b75d1b3 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -13,6 +13,7 @@ use std::{cmp, sync::Arc}; use util::ResultExt; use workspace::Modal; +#[derive(Clone, Copy)] pub enum PickerEvent { Dismiss, } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c557fbcf52..8d0159d7ad 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -247,6 +247,10 @@ pub struct CollabPanel { #[derive(Deserialize, Default, JsonSchema)] pub struct ChannelModal { + pub container: ContainerStyle, + pub height: f32, + pub header: TextStyle, + pub mode_button: Toggleable>, pub picker: Picker, pub row_height: f32, pub contact_avatar: ImageStyle, diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 3eff0e4b9a..951591676b 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -1,8 +1,9 @@ import { useTheme } from "../theme" +import { interactive, toggleable } from "../element" import { background, border, foreground, text } from "./components" import picker from "./picker" -export default function contacts_panel(): any { +export default function channel_modal(): any { const theme = useTheme() const side_margin = 6 @@ -15,6 +16,9 @@ export default function contacts_panel(): any { } const picker_style = picker() + delete picker_style.shadow + delete picker_style.border + const picker_input = { background: background(theme.middle, "on"), corner_radius: 6, @@ -37,6 +41,57 @@ export default function contacts_panel(): any { } return { + container: { + background: background(theme.lowest), + border: border(theme.lowest), + shadow: theme.modal_shadow, + corner_radius: 12, + padding: { + bottom: 4, + left: 20, + right: 20, + top: 20, + }, + }, + height: 400, + header: text(theme.middle, "sans", "on", { size: "lg" }), + mode_button: toggleable({ + base: interactive({ + base: { + ...text(theme.middle, "sans", { size: "xs" }), + border: border(theme.middle, "active"), + corner_radius: 4, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + + margin: { left: 6, top: 6, bottom: 6 }, + }, + state: { + hovered: { + ...text(theme.middle, "sans", "default", { size: "xs" }), + background: background(theme.middle, "hovered"), + border: border(theme.middle, "active"), + }, + }, + }), + state: { + active: { + default: { + color: foreground(theme.middle, "accent"), + }, + hovered: { + color: foreground(theme.middle, "accent", "hovered"), + }, + clicked: { + color: foreground(theme.middle, "accent", "pressed"), + }, + }, + } + }), picker: { empty_container: {}, item: { From 13a6b65a35b886b9ac380e56354acb7e562bcfab Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 02:33:29 +0200 Subject: [PATCH 083/326] Focus filters when enabled --- crates/search/src/project_search.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 74635bf53c..97edb1b1aa 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1507,6 +1507,11 @@ impl ProjectSearchBar { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { search_view.filters_enabled = !search_view.filters_enabled; + if search_view.filters_enabled { + cx.focus(&search_view.included_files_editor); + } else { + cx.focus(&search_view.query_editor); + } search_view .included_files_editor .update(cx, |_, cx| cx.notify()); From 95b1ab9574aeb6934c9b18ade8a2a84d3efc3932 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 3 Aug 2023 18:03:40 -0700 Subject: [PATCH 084/326] Implement channel member removal, permission check for member retrieval --- crates/client/src/channel_store.rs | 37 ++- crates/collab/src/db.rs | 244 +++++++++--------- crates/collab/src/db/tests.rs | 16 +- crates/collab/src/rpc.rs | 20 +- .../src/collab_panel/channel_modal.rs | 9 +- 5 files changed, 186 insertions(+), 140 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 8568317355..1d3bbd4435 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -121,6 +121,33 @@ impl ChannelStore { }) } + pub fn remove_member( + &mut self, + channel_id: ChannelId, + user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + if !self.outgoing_invites.insert((channel_id, user_id)) { + return Task::ready(Err(anyhow!("invite request already in progress"))); + } + + cx.notify(); + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + client + .request(proto::RemoveChannelMember { + channel_id, + user_id, + }) + .await?; + this.update(&mut cx, |this, cx| { + this.outgoing_invites.remove(&(channel_id, user_id)); + cx.notify(); + }); + Ok(()) + }) + } + pub fn respond_to_channel_invite( &mut self, channel_id: ChannelId, @@ -181,16 +208,6 @@ impl ChannelStore { self.outgoing_invites.contains(&(channel_id, user_id)) } - pub fn remove_member( - &self, - _channel_id: ChannelId, - _user_id: u64, - _cx: &mut ModelContext, - ) -> Task> { - dbg!("TODO"); - Task::Ready(Some(Ok(()))) - } - async fn handle_update_channels( this: ModelHandle, message: TypedEnvelope, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d942b8cab9..5a2ab24b1e 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3165,30 +3165,17 @@ impl Database { creator_id: UserId, ) -> Result { self.transaction(move |tx| async move { - let tx = tx; - if let Some(parent) = parent { - let channels = self.get_channel_ancestors(parent, &*tx).await?; - channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channels.iter().copied())) - .filter( - channel_member::Column::UserId - .eq(creator_id) - .and(channel_member::Column::Accepted.eq(true)), - ) - .one(&*tx) - .await? - .ok_or_else(|| { - anyhow!("User does not have the permissions to create this channel") - })?; + self.check_user_is_channel_admin(parent, creator_id, &*tx) + .await?; } let channel = channel::ActiveModel { name: ActiveValue::Set(name.to_string()), ..Default::default() - }; - - let channel = channel.insert(&*tx).await?; + } + .insert(&*tx) + .await?; if let Some(parent) = parent { channel_parent::ActiveModel { @@ -3228,45 +3215,36 @@ impl Database { user_id: UserId, ) -> Result<(Vec, Vec)> { self.transaction(move |tx| async move { - let tx = tx; - - // Check if user is an admin - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Admin.eq(true)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not allowed to remove this channel"))?; - - let mut descendants = self.get_channel_descendants([channel_id], &*tx).await?; - - // Keep channels which have another active - let mut channels_to_keep = channel_parent::Entity::find() - .filter( - channel_parent::Column::ChildId - .is_in(descendants.keys().copied().filter(|&id| id != channel_id)) - .and( - channel_parent::Column::ParentId.is_not_in(descendants.keys().copied()), - ), - ) - .stream(&*tx) + self.check_user_is_channel_admin(channel_id, user_id, &*tx) .await?; - while let Some(row) = channels_to_keep.next().await { - let row = row?; - descendants.remove(&row.child_id); + // 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_keep = channel_parent::Entity::find() + .filter( + channel_parent::Column::ChildId + .is_in( + channels_to_remove + .keys() + .copied() + .filter(|&id| id != channel_id), + ) + .and( + channel_parent::Column::ParentId + .is_not_in(channels_to_remove.keys().copied()), + ), + ) + .stream(&*tx) + .await?; + while let Some(row) = channels_to_keep.next().await { + let row = row?; + channels_to_remove.remove(&row.child_id); + } } - drop(channels_to_keep); - - let channels_to_remove = descendants.keys().copied().collect::>(); - let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channels_to_remove.iter().copied())) + .filter(channel_member::Column::ChannelId.is_in(channels_to_remove.keys().copied())) .select_only() .column(channel_member::Column::UserId) .distinct() @@ -3274,13 +3252,12 @@ impl Database { .all(&*tx) .await?; - // Channel members and parents should delete via cascade channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied())) + .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) .exec(&*tx) .await?; - Ok((channels_to_remove, members_to_notify)) + Ok((channels_to_remove.into_keys().collect(), members_to_notify)) }) .await } @@ -3293,31 +3270,18 @@ impl Database { is_admin: bool, ) -> Result<()> { self.transaction(move |tx| async move { - let tx = tx; + self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + .await?; - // Check if inviter is a member - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(inviter_id)) - .and(channel_member::Column::Admin.eq(true)), - ) - .one(&*tx) - .await? - .ok_or_else(|| { - anyhow!("Inviter does not have permissions to invite the invitee") - })?; - - let channel_membership = channel_member::ActiveModel { + channel_member::ActiveModel { channel_id: ActiveValue::Set(channel_id), user_id: ActiveValue::Set(invitee_id), accepted: ActiveValue::Set(false), admin: ActiveValue::Set(is_admin), ..Default::default() - }; - - channel_membership.insert(&*tx).await?; + } + .insert(&*tx) + .await?; Ok(()) }) @@ -3331,8 +3295,6 @@ impl Database { accept: bool, ) -> Result<()> { self.transaction(move |tx| async move { - let tx = tx; - let rows_affected = if accept { channel_member::Entity::update_many() .set(channel_member::ActiveModel { @@ -3368,10 +3330,36 @@ impl Database { .await } - pub async fn get_channel_invites(&self, user_id: UserId) -> Result> { + pub async fn remove_channel_member( + &self, + channel_id: ChannelId, + member_id: UserId, + remover_id: UserId, + ) -> Result<()> { self.transaction(|tx| async move { - let tx = tx; + self.check_user_is_channel_admin(channel_id, remover_id, &*tx) + .await?; + let result = channel_member::Entity::delete_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(member_id)), + ) + .exec(&*tx) + .await?; + + if result.rows_affected == 0 { + Err(anyhow!("no such member"))?; + } + + Ok(()) + }) + .await + } + + pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { + self.transaction(|tx| async move { let channel_invites = channel_member::Entity::find() .filter( channel_member::Column::UserId @@ -3406,7 +3394,7 @@ impl Database { .await } - pub async fn get_channels( + pub async fn get_channels_for_user( &self, user_id: UserId, ) -> Result<(Vec, HashMap>)> { @@ -3430,47 +3418,48 @@ impl Database { .await?; 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, - parent_id: parents_by_child_id.get(&row.id).copied().flatten(), - }); + { + 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, + parent_id: parents_by_child_id.get(&row.id).copied().flatten(), + }); + } } - drop(rows); - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIdsAndChannelIds { ChannelId, UserId, } - let mut participants = room_participant::Entity::find() - .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) - .select_only() - .column(room::Column::ChannelId) - .column(room_participant::Column::UserId) - .into_values::<_, QueryUserIdsAndChannelIds>() - .stream(&*tx) - .await?; - - let mut participant_map: HashMap> = HashMap::default(); - while let Some(row) = participants.next().await { - let row: (ChannelId, UserId) = row?; - participant_map.entry(row.0).or_default().push(row.1) + let mut participants_by_channel: HashMap> = HashMap::default(); + { + let mut rows = room_participant::Entity::find() + .inner_join(room::Entity) + .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) + .select_only() + .column(room::Column::ChannelId) + .column(room_participant::Column::UserId) + .into_values::<_, QueryUserIdsAndChannelIds>() + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row: (ChannelId, UserId) = row?; + participants_by_channel + .entry(row.0) + .or_default() + .push(row.1) + } } - drop(participants); - - Ok((channels, participant_map)) + Ok((channels, participants_by_channel)) }) .await } @@ -3480,12 +3469,15 @@ impl Database { .await } - // TODO: Add a chekc whether this user is allowed to read this channel pub async fn get_channel_member_details( &self, - id: ChannelId, + channel_id: ChannelId, + user_id: UserId, ) -> Result> { self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { UserId, @@ -3494,14 +3486,14 @@ impl Database { } let tx = tx; - let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?; + let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; let mut stream = channel_member::Entity::find() .distinct() .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) .select_only() .column(channel_member::Column::UserId) .column_as( - channel_member::Column::ChannelId.eq(id), + channel_member::Column::ChannelId.eq(channel_id), QueryMemberDetails::IsDirectMember, ) .column(channel_member::Column::Accepted) @@ -3552,9 +3544,29 @@ impl Database { Ok(user_ids) } + async fn check_user_is_channel_admin( + &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)) + .and(channel_member::Column::Admin.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not allowed to remove this channel"))?; + Ok(()) + } + async fn get_channel_ancestors( &self, - id: ChannelId, + channel_id: ChannelId, tx: &DatabaseTransaction, ) -> Result> { let sql = format!( @@ -3570,7 +3582,7 @@ impl Database { SELECT DISTINCT channel_tree.parent_id FROM channel_tree "#, - id + channel_id ); #[derive(FromQueryResult, Debug, PartialEq)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index e4161d3b55..b4c22430e5 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -951,7 +951,7 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .await .unwrap(); - let (channels, _) = db.get_channels(a_id).await.unwrap(); + let (channels, _) = db.get_channels_for_user(a_id).await.unwrap(); assert_eq!( channels, @@ -1144,7 +1144,7 @@ test_both_dbs!( .unwrap(); let user_2_invites = db - .get_channel_invites(user_2) // -> [channel_1_1, channel_1_2] + .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] .await .unwrap() .into_iter() @@ -1154,7 +1154,7 @@ test_both_dbs!( assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); let user_3_invites = db - .get_channel_invites(user_3) // -> [channel_1_1] + .get_channel_invites_for_user(user_3) // -> [channel_1_1] .await .unwrap() .into_iter() @@ -1163,7 +1163,10 @@ test_both_dbs!( assert_eq!(user_3_invites, &[channel_1_1]); - let members = db.get_channel_member_details(channel_1_1).await.unwrap(); + let members = db + .get_channel_member_details(channel_1_1, user_1) + .await + .unwrap(); assert_eq!( members, &[ @@ -1191,7 +1194,10 @@ test_both_dbs!( .await .unwrap(); - let members = db.get_channel_member_details(channel_1_3).await.unwrap(); + let members = db + .get_channel_member_details(channel_1_3, user_1) + .await + .unwrap(); assert_eq!( members, &[ diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fdfccea98f..17f1334544 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -530,8 +530,8 @@ impl Server { let (contacts, invite_code, (channels, channel_participants), channel_invites) = future::try_join4( this.app_state.db.get_contacts(user_id), this.app_state.db.get_invite_code_for_user(user_id), - this.app_state.db.get_channels(user_id), - this.app_state.db.get_channel_invites(user_id) + this.app_state.db.get_channels_for_user(user_id), + this.app_state.db.get_channel_invites_for_user(user_id) ).await?; { @@ -2230,10 +2230,16 @@ async fn invite_channel_member( } async fn remove_channel_member( - _request: proto::RemoveChannelMember, - _response: Response, - _session: Session, + request: proto::RemoveChannelMember, + 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); + db.remove_channel_member(channel_id, member_id, session.user_id) + .await?; + response.send(proto::Ack {})?; Ok(()) } @@ -2244,7 +2250,9 @@ async fn get_channel_members( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let members = db.get_channel_member_details(channel_id).await?; + let members = db + .get_channel_member_details(channel_id, session.user_id) + .await?; response.send(proto::GetChannelMembersResponse { members })?; Ok(()) } diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 9af6099f65..5628540022 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -260,9 +260,12 @@ impl PickerDelegate for ChannelModalDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { if let Some((user, _)) = self.matches.get(self.selected_index) { match self.mode { - Mode::ManageMembers => { - // - } + Mode::ManageMembers => self + .channel_store + .update(cx, |store, cx| { + store.remove_member(self.channel_id, user.id, cx) + }) + .detach(), Mode::InviteMembers => match self.member_status(user.id, cx) { Some(proto::channel_member::Kind::Member) => {} Some(proto::channel_member::Kind::Invitee) => self From 7a04ee3b71da58f73a858cb676c7bb9809b822f4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 3 Aug 2023 18:31:00 -0700 Subject: [PATCH 085/326] Start work on exposing which channels the user has admin rights to --- crates/client/src/channel_store.rs | 4 +++ crates/client/src/channel_store_tests.rs | 21 +++++++------ crates/collab/src/db.rs | 39 ++++++++++++++++-------- crates/collab/src/db/tests.rs | 15 ++++++--- crates/collab/src/rpc.rs | 15 ++++++--- crates/collab/src/tests/channel_tests.rs | 9 +++--- crates/rpc/proto/zed.proto | 3 +- 7 files changed, 70 insertions(+), 36 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 1d3bbd4435..ee04865e50 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -26,6 +26,7 @@ pub struct Channel { pub id: ChannelId, pub name: String, pub parent_id: Option, + pub user_is_admin: bool, pub depth: usize, } @@ -247,6 +248,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, + user_is_admin: false, parent_id: None, depth: 0, }), @@ -267,6 +269,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, + user_is_admin: channel.user_is_admin, parent_id: Some(parent_id), depth, }), @@ -278,6 +281,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, + user_is_admin: channel.user_is_admin, parent_id: None, depth: 0, }), diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs index 0d4ec6ce35..7f31243dad 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/client/src/channel_store_tests.rs @@ -18,11 +18,13 @@ fn test_update_channels(cx: &mut AppContext) { id: 1, name: "b".to_string(), parent_id: None, + user_is_admin: true, }, proto::Channel { id: 2, name: "a".to_string(), parent_id: None, + user_is_admin: false, }, ], ..Default::default() @@ -33,8 +35,8 @@ fn test_update_channels(cx: &mut AppContext) { &channel_store, &[ // - (0, "a"), - (0, "b"), + (0, "a", true), + (0, "b", false), ], cx, ); @@ -47,11 +49,13 @@ fn test_update_channels(cx: &mut AppContext) { id: 3, name: "x".to_string(), parent_id: Some(1), + user_is_admin: false, }, proto::Channel { id: 4, name: "y".to_string(), parent_id: Some(2), + user_is_admin: false, }, ], ..Default::default() @@ -61,11 +65,10 @@ fn test_update_channels(cx: &mut AppContext) { assert_channels( &channel_store, &[ - // - (0, "a"), - (1, "y"), - (0, "b"), - (1, "x"), + (0, "a", true), + (1, "y", true), + (0, "b", false), + (1, "x", false), ], cx, ); @@ -81,14 +84,14 @@ fn update_channels( fn assert_channels( channel_store: &ModelHandle, - expected_channels: &[(usize, &str)], + expected_channels: &[(usize, &str, bool)], cx: &AppContext, ) { channel_store.read_with(cx, |store, _| { let actual = store .channels() .iter() - .map(|c| (c.depth, c.name.as_str())) + .map(|c| (c.depth, c.name.as_str(), c.user_is_admin)) .collect::>(); assert_eq!(actual, expected_channels); }); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 5a2ab24b1e..6ebf5933df 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3385,6 +3385,7 @@ impl Database { .map(|channel| Channel { id: channel.id, name: channel.name, + user_is_admin: false, parent_id: None, }) .collect(); @@ -3401,20 +3402,21 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - let starting_channel_ids: Vec = channel_member::Entity::find() + let channel_memberships = channel_member::Entity::find() .filter( channel_member::Column::UserId .eq(user_id) .and(channel_member::Column::Accepted.eq(true)), ) - .select_only() - .column(channel_member::Column::ChannelId) - .into_values::<_, QueryChannelIds>() .all(&*tx) .await?; + let admin_channel_ids = channel_memberships + .iter() + .filter_map(|m| m.admin.then_some(m.channel_id)) + .collect::>(); let parents_by_child_id = self - .get_channel_descendants(starting_channel_ids, &*tx) + .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; let mut channels = Vec::with_capacity(parents_by_child_id.len()); @@ -3428,6 +3430,7 @@ impl Database { channels.push(Channel { id: row.id, name: row.name, + user_is_admin: admin_channel_ids.contains(&row.id), parent_id: parents_by_child_id.get(&row.id).copied().flatten(), }); } @@ -3627,7 +3630,7 @@ impl Database { r#" WITH RECURSIVE channel_tree(child_id, parent_id) AS ( SELECT root_ids.column1 as child_id, CAST(NULL as INTEGER) as parent_id - FROM (VALUES {}) as root_ids + FROM (VALUES {values}) as root_ids UNION SELECT channel_parents.child_id, channel_parents.parent_id FROM channel_parents, channel_tree @@ -3637,7 +3640,6 @@ impl Database { FROM channel_tree ORDER BY child_id, parent_id IS NOT NULL "#, - values ); #[derive(FromQueryResult, Debug, PartialEq)] @@ -3663,14 +3665,29 @@ impl Database { Ok(parents_by_child_id) } - pub async fn get_channel(&self, channel_id: ChannelId) -> Result> { + pub async fn get_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result> { self.transaction(|tx| async move { let tx = tx; let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + let user_is_admin = channel_member::Entity::find() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(user_id)) + .and(channel_member::Column::Admin.eq(true)), + ) + .count(&*tx) + .await? + > 0; Ok(channel.map(|channel| Channel { id: channel.id, name: channel.name, + user_is_admin, parent_id: None, })) }) @@ -3942,6 +3959,7 @@ pub struct NewUserResult { pub struct Channel { pub id: ChannelId, pub name: String, + pub user_is_admin: bool, pub parent_id: Option, } @@ -4199,11 +4217,6 @@ pub struct WorktreeSettingsFile { pub content: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -enum QueryChannelIds { - ChannelId, -} - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryUserIds { UserId, diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index b4c22430e5..5ffcd12776 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -960,43 +960,50 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { id: zed_id, name: "zed".to_string(), parent_id: None, + user_is_admin: true, }, Channel { id: crdb_id, name: "crdb".to_string(), parent_id: Some(zed_id), + user_is_admin: true, }, Channel { id: livestreaming_id, name: "livestreaming".to_string(), parent_id: Some(zed_id), + user_is_admin: true, }, Channel { id: replace_id, name: "replace".to_string(), parent_id: Some(zed_id), + user_is_admin: true, }, Channel { id: rust_id, name: "rust".to_string(), parent_id: None, + user_is_admin: true, }, Channel { id: cargo_id, name: "cargo".to_string(), parent_id: Some(rust_id), + user_is_admin: true, }, Channel { id: cargo_ra_id, name: "cargo-ra".to_string(), parent_id: Some(cargo_id), + user_is_admin: true, } ] ); // Remove a single channel db.remove_channel(crdb_id, a_id).await.unwrap(); - assert!(db.get_channel(crdb_id).await.unwrap().is_none()); + assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none()); // Remove a channel tree let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap(); @@ -1004,9 +1011,9 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]); assert_eq!(user_ids, &[a_id]); - assert!(db.get_channel(rust_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_id).await.unwrap().is_none()); - assert!(db.get_channel(cargo_ra_id).await.unwrap().is_none()); + 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()); }); test_both_dbs!( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 17f1334544..31b0b2280a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2150,6 +2150,7 @@ async fn create_channel( id: id.to_proto(), name: request.name, parent_id: request.parent_id, + user_is_admin: true, }); if let Some(parent_id) = parent_id { @@ -2204,7 +2205,7 @@ async fn invite_channel_member( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let channel = db - .get_channel(channel_id) + .get_channel(channel_id, session.user_id) .await? .ok_or_else(|| anyhow!("channel not found"))?; let invitee_id = UserId::from_proto(request.user_id); @@ -2216,6 +2217,7 @@ async fn invite_channel_member( id: channel.id.to_proto(), name: channel.name, parent_id: None, + user_is_admin: false, }); for connection_id in session .connection_pool() @@ -2264,12 +2266,12 @@ async fn respond_to_channel_invite( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db - .get_channel(channel_id) - .await? - .ok_or_else(|| anyhow!("no such channel"))?; db.respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; + let channel = db + .get_channel(channel_id, session.user_id) + .await? + .ok_or_else(|| anyhow!("no such channel"))?; let mut update = proto::UpdateChannels::default(); update @@ -2279,6 +2281,7 @@ async fn respond_to_channel_invite( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + user_is_admin: channel.user_is_admin, parent_id: None, }); } @@ -2430,6 +2433,7 @@ fn build_initial_channels_update( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + user_is_admin: channel.user_is_admin, parent_id: channel.parent_id.map(|id| id.to_proto()), }); } @@ -2447,6 +2451,7 @@ fn build_initial_channels_update( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, + user_is_admin: false, parent_id: None, }); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index b4f8477a2d..abaedb52a8 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,13 +1,10 @@ +use crate::tests::{room_participants, RoomParticipants, TestServer}; use call::ActiveCall; use client::{Channel, User}; use gpui::{executor::Deterministic, TestAppContext}; use rpc::proto; use std::sync::Arc; -use crate::tests::{room_participants, RoomParticipants}; - -use super::TestServer; - #[gpui::test] async fn test_basic_channels( deterministic: Arc, @@ -35,6 +32,7 @@ async fn test_basic_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, + user_is_admin: true, depth: 0, })] ) @@ -69,6 +67,7 @@ async fn test_basic_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, + user_is_admin: false, depth: 0, })] ) @@ -111,6 +110,7 @@ async fn test_basic_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, + user_is_admin: false, depth: 0, })] ) @@ -204,6 +204,7 @@ async fn test_channel_room( id: zed_id, name: "zed".to_string(), parent_id: None, + user_is_admin: false, depth: 0, })] ) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 602b34529e..7dd5a0a893 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1295,7 +1295,8 @@ message Nonce { message Channel { uint64 id = 1; string name = 2; - optional uint64 parent_id = 3; + bool user_is_admin = 3; + optional uint64 parent_id = 4; } message Contact { From 50d03ffc8c3bd179845ea04d63acf53983bd8f8f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:24:57 +0200 Subject: [PATCH 086/326] Revert "Focus filters when enabled" This reverts commit 13a6b65a35b886b9ac380e56354acb7e562bcfab. --- crates/search/src/project_search.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 97edb1b1aa..74635bf53c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1507,11 +1507,6 @@ impl ProjectSearchBar { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { search_view.filters_enabled = !search_view.filters_enabled; - if search_view.filters_enabled { - cx.focus(&search_view.included_files_editor); - } else { - cx.focus(&search_view.query_editor); - } search_view .included_files_editor .update(cx, |_, cx| cx.notify()); From 792f29e288597e3432a26cae6895d8c57767bbcb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:25:18 +0200 Subject: [PATCH 087/326] Refresh windows on toggle_filters. --- crates/search/src/project_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 74635bf53c..651ec3a537 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1513,6 +1513,7 @@ impl ProjectSearchBar { search_view .excluded_files_editor .update(cx, |_, cx| cx.notify()); + cx.refresh_windows(); cx.notify(); }); cx.notify(); From fcefb37ca07635ae99023c8105fe36b927ae1cdd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:54:50 +0200 Subject: [PATCH 088/326] Prevent modes from being vertically centered when row_count changes --- crates/search/src/project_search.rs | 35 +++++++++++++++++------------ styles/src/style_tree/search.ts | 4 +++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 651ec3a537..d7ca0b6b46 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1954,20 +1954,27 @@ impl View for ProjectSearchBar { .flex(2., true), ) .with_child( - Flex::column().with_child( - Flex::row() - .align_children_center() - .with_child(self.render_search_mode_button(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(self.render_search_mode_button(SearchMode::Regex, cx)) - .constrained() - .with_height(theme.workspace.toolbar.height) - .contained() - .with_style(theme.search.container) - .aligned() - .right() - .flex(1., true), - ), + Flex::column() + .with_child( + Flex::row() + .align_children_center() + .with_child(self.render_search_mode_button(SearchMode::Text, cx)) + .with_children(semantic_index) + .with_child(self.render_search_mode_button(SearchMode::Regex, cx)) + .constrained() + .with_height(theme.workspace.toolbar.height) + .contained() + .with_style(theme.search.container) + .aligned() + .right() + .flex(1., true), + ) + .with_children( + _search + .read(cx) + .filters_enabled + .then(|| Flex::row().flex(1., true)), + ), ) .contained() .flex_float() diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 370d12c6f0..3565db1f9f 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -44,7 +44,7 @@ export default function search(): any { base: { ...text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - + corner_radius: 2, border: border(theme.highest, "on"), padding: { @@ -130,6 +130,8 @@ export default function search(): any { padding: { left: 12, right: 12, + top: 3, + bottom: 3, }, }, include_exclude_inputs: { From 2c0e3886a5d42e38360e5b82b1a63c16b061cad0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:38:53 +0200 Subject: [PATCH 089/326] Align search bar in the middle --- crates/search/src/project_search.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d7ca0b6b46..2f19c91868 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1950,8 +1950,9 @@ impl View for ProjectSearchBar { .contained() .with_style(theme.search.container) .aligned() + .left() .top() - .flex(2., true), + .flex(1., true), ) .with_child( Flex::column() From 22927fa1d7c8041e71bcba78f12230814a8d006d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 4 Aug 2023 14:39:05 +0100 Subject: [PATCH 090/326] Fix visual selection cursor in multibuffers --- crates/editor/src/element.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index bc6c2f0bb8..d2e7ab00d7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -860,9 +860,15 @@ impl EditorElement { } else if cursor_row > 0 && cursor_position != layout.position_map.snapshot.max_point() { - cursor_row -= 1; - cursor_column = - layout.position_map.snapshot.line_len(cursor_row) as usize; + let new = layout.position_map.snapshot.clip_point( + DisplayPoint::new( + cursor_row - 1, + layout.position_map.snapshot.line_len(cursor_row), + ), + Bias::Left, + ); + cursor_row = new.row(); + cursor_column = new.column() as usize; } } dbg!(selection.head, cursor_row, cursor_column); From 82eb6d8bc3ff327a6b598155f2601b569ce28073 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:35:59 +0200 Subject: [PATCH 091/326] Change styling of match nav buttons Co-authored-by: Kyle --- crates/search/src/project_search.rs | 83 ++++++++++++++++++++++------- crates/theme/src/theme.rs | 1 + styles/src/style_tree/search.ts | 28 ++++++++++ 3 files changed, 94 insertions(+), 18 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2f19c91868..6a000091e4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1531,6 +1531,7 @@ impl ProjectSearchBar { ) -> AnyElement { let action: Box; let tooltip; + match direction { Direction::Prev => { action = Box::new(SelectPrevMatch); @@ -1546,10 +1547,51 @@ impl ProjectSearchBar { enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.option_button.inactive_state().style_for(state); - Label::new(icon, style.text.clone()) + let mut style = theme.search.nav_button.style_for(state).clone(); + + match direction { + Direction::Prev => style.container.border.left = false, + Direction::Next => style.container.border.right = false, + }; + let mut label = Label::new(icon, style.label.clone()) .contained() - .with_style(style.container) + .with_style(style.container.clone()); + match direction { + Direction::Prev => Flex::row() + .with_child( + ButtonSide::left( + style + .clone() + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .with_child(label) + .constrained() + .with_height(theme.workspace.toolbar.height), + Direction::Next => Flex::row() + .with_child(label) + .with_child( + ButtonSide::right( + style + .clone() + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .constrained() + .with_height(theme.workspace.toolbar.height), + } }) .on_click(MouseButton::Left, move |_, this, cx| { if let Some(search) = this.active_project_search.as_ref() { @@ -1911,7 +1953,26 @@ impl View for ProjectSearchBar { Flex::row() .with_child( Flex::column() - .with_child(Flex::row().with_children(matches).aligned().left()) + .with_child( + Flex::row() + .with_child( + Flex::row() + .with_child(self.render_nav_button( + "<", + Direction::Prev, + cx, + )) + .with_child(self.render_nav_button( + ">", + Direction::Next, + cx, + )) + .aligned(), + ) + .with_children(matches) + .aligned() + .left(), + ) .flex(1., true), ) .with_child( @@ -1929,20 +1990,6 @@ impl View for ProjectSearchBar { .with_max_width(theme.search.editor.max_width) .flex(1., false), ) - .with_child( - Flex::row() - .with_child(self.render_nav_button( - "<", - Direction::Prev, - cx, - )) - .with_child(self.render_nav_button( - ">", - Direction::Next, - cx, - )) - .aligned(), - ) .contained() .with_margin_bottom(row_spacing), ) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index da6a188527..1107906ca7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -389,6 +389,7 @@ pub struct Search { pub editor_icon: IconStyle, pub mode_button: Toggleable>, pub mode_filling_width: f32, + pub nav_button: Interactive, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 3565db1f9f..fb9b356591 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -225,5 +225,33 @@ export default function search(): any { }, }), mode_filling_width: 4.0, + nav_button: interactive({ + base: { + text: text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 2, + border: border(theme.highest, "on"), + + padding: { + bottom: 6, + left: 6, + right: 6, + top: 6, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }), + } } From 8fa082c28b3d7c8da6beef1f2e524e1523419fe6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:49:54 +0200 Subject: [PATCH 092/326] Center the query editor (for real now) Co-authored-by: Kyle --- crates/search/src/project_search.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6a000091e4..6b303d2ce8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1973,10 +1973,12 @@ impl View for ProjectSearchBar { .aligned() .left(), ) + .contained() .flex(1., true), ) .with_child( Flex::column() + .align_children_center() .with_child( Flex::row() .with_child( @@ -1997,9 +1999,8 @@ impl View for ProjectSearchBar { .contained() .with_style(theme.search.container) .aligned() - .left() .top() - .flex(1., true), + .flex(1., false), ) .with_child( Flex::column() @@ -2022,7 +2023,9 @@ impl View for ProjectSearchBar { .read(cx) .filters_enabled .then(|| Flex::row().flex(1., true)), - ), + ) + .contained() + .flex(1., true), ) .contained() .flex_float() From de8e1852a8eef049ea1c51b52159d33f1f489692 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:21:46 +0200 Subject: [PATCH 093/326] Align match count Co-authored-by: Kyle --- crates/search/src/project_search.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6b303d2ce8..e746d75cdd 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1918,8 +1918,6 @@ impl View for ProjectSearchBar { ) .contained() .with_style(theme.search.match_index.container) - .aligned() - .left() }); let filters = search.filters_enabled.then(|| { @@ -1955,6 +1953,7 @@ impl View for ProjectSearchBar { Flex::column() .with_child( Flex::row() + .align_children_center() .with_child( Flex::row() .with_child(self.render_nav_button( From 1762d2c6d43651edcb113b97e3609de545a796f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 4 Aug 2023 09:51:37 -0700 Subject: [PATCH 094/326] Add test assertion where user is not admin of channel --- crates/collab/src/db/tests.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 5ffcd12776..3067fd063e 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -952,7 +952,6 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .unwrap(); let (channels, _) = db.get_channels_for_user(a_id).await.unwrap(); - assert_eq!( channels, vec![ @@ -1001,6 +1000,37 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { ] ); + let (channels, _) = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + user_is_admin: true, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + user_is_admin: false, + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + user_is_admin: false, + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + user_is_admin: false, + }, + ] + ); + // Remove a single channel db.remove_channel(crdb_id, a_id).await.unwrap(); assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none()); From 9889449a81f2f552c807ed687d4312acc34eb0f0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:54:27 +0200 Subject: [PATCH 095/326] Adjust row count for project search. Instead of using the same row count as for the breadcrumbs, we double the height so that there's some space for padding. Co-authored-by: Kyle --- crates/search/src/project_search.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e746d75cdd..e2ad89117f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1970,6 +1970,7 @@ impl View for ProjectSearchBar { ) .with_children(matches) .aligned() + .top() .left(), ) .contained() @@ -2015,6 +2016,7 @@ impl View for ProjectSearchBar { .with_style(theme.search.container) .aligned() .right() + .top() .flex(1., true), ) .with_children( @@ -2027,6 +2029,7 @@ impl View for ProjectSearchBar { .flex(1., true), ) .contained() + .with_uniform_padding(theme.workspace.toolbar.height / 3.) .flex_float() .into_any_named("project search") } else { @@ -2060,9 +2063,9 @@ impl ToolbarItemView for ProjectSearchBar { .as_ref() .map(|search| { let offset = search.read(cx).filters_enabled as usize; - 1 + offset + 2 + offset }) - .unwrap_or_else(|| 1) + .unwrap_or_else(|| 2) } } From a2486de04502226bc15f5495b61a0aebbca0d835 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 4 Aug 2023 09:58:10 -0700 Subject: [PATCH 096/326] Don't expose channel admin actions in UI if user isn't admin --- crates/client/src/channel_store.rs | 5 +- crates/collab/src/rpc.rs | 29 ++++--- crates/collab/src/tests/channel_tests.rs | 105 +++++++++++++++++++---- crates/collab_ui/src/collab_panel.rs | 28 +++--- 4 files changed, 123 insertions(+), 44 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index ee04865e50..c04b123acf 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -263,13 +263,14 @@ impl ChannelStore { if let Some(parent_id) = channel.parent_id { if let Some(ix) = self.channels.iter().position(|c| c.id == parent_id) { - let depth = self.channels[ix].depth + 1; + let parent_channel = &self.channels[ix]; + let depth = parent_channel.depth + 1; self.channels.insert( ix + 1, Arc::new(Channel { id: channel.id, name: channel.name, - user_is_admin: channel.user_is_admin, + user_is_admin: channel.user_is_admin || parent_channel.user_is_admin, parent_id: Some(parent_id), depth, }), diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 31b0b2280a..6893c4bde4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2209,7 +2209,7 @@ async fn invite_channel_member( .await? .ok_or_else(|| anyhow!("channel not found"))?; let invitee_id = UserId::from_proto(request.user_id); - db.invite_channel_member(channel_id, invitee_id, session.user_id, false) + db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin) .await?; let mut update = proto::UpdateChannels::default(); @@ -2268,22 +2268,29 @@ async fn respond_to_channel_invite( let channel_id = ChannelId::from_proto(request.channel_id); db.respond_to_channel_invite(channel_id, session.user_id, request.accept) .await?; - let channel = db - .get_channel(channel_id, session.user_id) - .await? - .ok_or_else(|| anyhow!("no such channel"))?; let mut update = proto::UpdateChannels::default(); update .remove_channel_invitations .push(channel_id.to_proto()); if request.accept { - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - user_is_admin: channel.user_is_admin, - parent_id: None, - }); + let (channels, participants) = db.get_channels_for_user(session.user_id).await?; + update + .channels + .extend(channels.into_iter().map(|channel| proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + user_is_admin: channel.user_is_admin, + parent_id: channel.parent_id.map(ChannelId::to_proto), + })); + update + .channel_participants + .extend(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(), + } + })); } session.peer.send(session.connection_id, update)?; response.send(proto::Ack {})?; diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index abaedb52a8..43e5a296c4 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -23,18 +23,34 @@ async fn test_basic_channels( }) .await .unwrap(); + let channel_b_id = client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.create_channel("channel-b", Some(channel_a_id)) + }) + .await + .unwrap(); deterministic.run_until_parked(); client_a.channel_store().read_with(cx_a, |channels, _| { assert_eq!( channels.channels(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - user_is_admin: true, - depth: 0, - })] + &[ + Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + }), + Arc::new(Channel { + id: channel_b_id, + name: "channel-b".to_string(), + parent_id: Some(channel_a_id), + user_is_admin: true, + depth: 1, + }) + ] ) }); @@ -48,7 +64,7 @@ async fn test_basic_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(), true, cx); // Make sure we're synchronously storing the pending invite assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); @@ -57,9 +73,8 @@ async fn test_basic_channels( .await .unwrap(); - // Wait for client b to see the invitation + // Client A sees that B has been invited. deterministic.run_until_parked(); - client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!( channels.channel_invitations(), @@ -69,7 +84,7 @@ async fn test_basic_channels( parent_id: None, user_is_admin: false, depth: 0, - })] + }),] ) }); let members = client_a @@ -94,7 +109,7 @@ async fn test_basic_channels( ], ); - // Client B now sees that they are a member channel A. + // Client B accepts the invitation. client_b .channel_store() .update(cx_b, |channels, _| { @@ -102,17 +117,69 @@ async fn test_basic_channels( }) .await .unwrap(); + + // Client B now sees that they are a member of channel A and its existing + // subchannels. Their admin priveleges extend to subchannels of channel A. + deterministic.run_until_parked(); client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!(channels.channel_invitations(), &[]); assert_eq!( channels.channels(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - user_is_admin: false, - depth: 0, - })] + &[ + Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + }), + Arc::new(Channel { + id: channel_b_id, + name: "channel-b".to_string(), + parent_id: Some(channel_a_id), + user_is_admin: true, + depth: 1, + }) + ] + ) + }); + + let channel_c_id = client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.create_channel("channel-c", Some(channel_a_id)) + }) + .await + .unwrap(); + + // TODO - ensure sibling channels are sorted in a stable way + deterministic.run_until_parked(); + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channels(), + &[ + Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + }), + Arc::new(Channel { + id: channel_c_id, + name: "channel-c".to_string(), + parent_id: Some(channel_a_id), + user_is_admin: true, + depth: 1, + }), + Arc::new(Channel { + id: channel_b_id, + name: "channel-b".to_string(), + parent_id: Some(channel_a_id), + user_is_admin: true, + depth: 1, + }), + ] ) }); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 771927c8ac..df27ea5005 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1509,18 +1509,22 @@ impl CollabPanel { channel_id: u64, cx: &mut ViewContext, ) { - self.context_menu.update(cx, |context_menu, cx| { - context_menu.show( - position, - gpui::elements::AnchorCorner::BottomLeft, - vec![ - ContextMenuItem::action("New Channel", NewChannel { channel_id }), - ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), - ContextMenuItem::action("Add member", AddMember { channel_id }), - ], - cx, - ); - }); + if let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) { + if channel.user_is_admin { + self.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + position, + gpui::elements::AnchorCorner::BottomLeft, + vec![ + ContextMenuItem::action("New Channel", NewChannel { channel_id }), + ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), + ContextMenuItem::action("Add member", AddMember { channel_id }), + ], + cx, + ); + }); + } + } } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { From 87b2d599c187e6cd4fb3a341ddf3101fb0872f3c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 4 Aug 2023 14:12:08 -0700 Subject: [PATCH 097/326] Flesh out channel member management Co-authored-by: Mikayla --- assets/icons/channels.svg | 6 + assets/keymaps/default.json | 8 + crates/client/src/channel_store.rs | 66 +++- crates/collab/src/db.rs | 56 ++- crates/collab/src/db/tests.rs | 46 ++- crates/collab/src/rpc.rs | 75 +++- crates/collab/src/tests/channel_tests.rs | 192 +++++++--- crates/collab_ui/src/collab_panel.rs | 42 +- .../src/collab_panel/channel_modal.rs | 358 +++++++++++++----- crates/rpc/proto/zed.proto | 10 +- crates/rpc/src/proto.rs | 2 + crates/theme/src/theme.rs | 8 +- styles/src/style_tree/channel_modal.ts | 55 +++ 13 files changed, 728 insertions(+), 196 deletions(-) create mode 100644 assets/icons/channels.svg diff --git a/assets/icons/channels.svg b/assets/icons/channels.svg new file mode 100644 index 0000000000..edd0462678 --- /dev/null +++ b/assets/icons/channels.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a7f4b55084..d99a660850 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -550,6 +550,14 @@ "alt-shift-f": "project_panel::NewSearchInDirectory" } }, + { + "context": "ChannelModal", + "bindings": { + "left": "channel_modal::SelectNextControl", + "right": "channel_modal::SelectNextControl", + "tab": "channel_modal::ToggleMode" + } + }, { "context": "Terminal", "bindings": { diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index c04b123acf..51176986ef 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -30,6 +30,12 @@ pub struct Channel { pub depth: usize, } +pub struct ChannelMembership { + pub user: Arc, + pub kind: proto::channel_member::Kind, + pub admin: bool, +} + impl Entity for ChannelStore { type Event = (); } @@ -72,6 +78,20 @@ impl ChannelStore { self.channels.iter().find(|c| c.id == channel_id).cloned() } + pub fn is_user_admin(&self, mut channel_id: ChannelId) -> bool { + while let Some(channel) = self.channel_for_id(channel_id) { + if channel.user_is_admin { + return true; + } + if let Some(parent_id) = channel.parent_id { + channel_id = parent_id; + } else { + break; + } + } + false + } + pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { self.channel_participants .get(&channel_id) @@ -149,6 +169,35 @@ impl ChannelStore { }) } + pub fn set_member_admin( + &mut self, + channel_id: ChannelId, + user_id: UserId, + admin: bool, + cx: &mut ModelContext, + ) -> Task> { + if !self.outgoing_invites.insert((channel_id, user_id)) { + return Task::ready(Err(anyhow!("member request already in progress"))); + } + + cx.notify(); + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + client + .request(proto::SetChannelMemberAdmin { + channel_id, + user_id, + admin, + }) + .await?; + this.update(&mut cx, |this, cx| { + this.outgoing_invites.remove(&(channel_id, user_id)); + cx.notify(); + }); + Ok(()) + }) + } + pub fn respond_to_channel_invite( &mut self, channel_id: ChannelId, @@ -167,7 +216,7 @@ impl ChannelStore { &self, channel_id: ChannelId, cx: &mut ModelContext, - ) -> Task, proto::channel_member::Kind)>>> { + ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.downgrade(); cx.spawn(|_, mut cx| async move { @@ -187,7 +236,11 @@ impl ChannelStore { .into_iter() .zip(response.members) .filter_map(|(user, member)| { - Some((user, proto::channel_member::Kind::from_i32(member.kind)?)) + Some(ChannelMembership { + user, + admin: member.admin, + kind: proto::channel_member::Kind::from_i32(member.kind)?, + }) }) .collect()) }) @@ -239,7 +292,8 @@ impl ChannelStore { .iter_mut() .find(|c| c.id == channel.id) { - Arc::make_mut(existing_channel).name = channel.name; + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.name = channel.name; continue; } @@ -257,7 +311,9 @@ impl ChannelStore { for channel in payload.channels { if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { - Arc::make_mut(existing_channel).name = channel.name; + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.name = channel.name; + existing_channel.user_is_admin = channel.user_is_admin; continue; } @@ -270,7 +326,7 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, - user_is_admin: channel.user_is_admin || parent_channel.user_is_admin, + user_is_admin: channel.user_is_admin, parent_id: Some(parent_id), depth, }), diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 6ebf5933df..9dc4ad805b 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3243,8 +3243,9 @@ impl Database { } } + let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channels_to_remove.keys().copied())) + .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) .select_only() .column(channel_member::Column::UserId) .distinct() @@ -3472,6 +3473,39 @@ impl Database { .await } + pub async fn set_channel_member_admin( + &self, + channel_id: ChannelId, + from: UserId, + for_user: UserId, + admin: bool, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, from, &*tx) + .await?; + + let result = channel_member::Entity::update_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(for_user)), + ) + .set(channel_member::ActiveModel { + admin: ActiveValue::set(admin), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected == 0 { + Err(anyhow!("no such member"))?; + } + + Ok(()) + }) + .await + } + pub async fn get_channel_member_details( &self, channel_id: ChannelId, @@ -3484,6 +3518,7 @@ impl Database { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryMemberDetails { UserId, + Admin, IsDirectMember, Accepted, } @@ -3495,6 +3530,7 @@ 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_as( channel_member::Column::ChannelId.eq(channel_id), QueryMemberDetails::IsDirectMember, @@ -3507,7 +3543,12 @@ impl Database { let mut rows = Vec::::new(); while let Some(row) = stream.next().await { - let (user_id, is_direct_member, is_invite_accepted): (UserId, bool, bool) = row?; + let (user_id, is_admin, is_direct_member, is_invite_accepted): ( + UserId, + bool, + bool, + bool, + ) = row?; let kind = match (is_direct_member, is_invite_accepted) { (true, true) => proto::channel_member::Kind::Member, (true, false) => proto::channel_member::Kind::Invitee, @@ -3518,11 +3559,18 @@ impl Database { let kind = kind.into(); if let Some(last_row) = rows.last_mut() { if last_row.user_id == user_id { - last_row.kind = last_row.kind.min(kind); + if is_direct_member { + last_row.kind = kind; + last_row.admin = is_admin; + } continue; } } - rows.push(proto::ChannelMember { user_id, kind }); + rows.push(proto::ChannelMember { + user_id, + kind, + admin: is_admin, + }); } Ok(rows) diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 3067fd063e..efc35a5c24 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -915,7 +915,7 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); - db.invite_channel_member(zed_id, b_id, a_id, true) + db.invite_channel_member(zed_id, b_id, a_id, false) .await .unwrap(); @@ -1000,6 +1000,43 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { ] ); + let (channels, _) = db.get_channels_for_user(b_id).await.unwrap(); + assert_eq!( + channels, + vec![ + Channel { + id: zed_id, + name: "zed".to_string(), + parent_id: None, + user_is_admin: false, + }, + Channel { + id: crdb_id, + name: "crdb".to_string(), + parent_id: Some(zed_id), + user_is_admin: false, + }, + Channel { + id: livestreaming_id, + name: "livestreaming".to_string(), + parent_id: Some(zed_id), + user_is_admin: false, + }, + Channel { + id: replace_id, + name: "replace".to_string(), + parent_id: Some(zed_id), + user_is_admin: false, + }, + ] + ); + + // Update member permissions + let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await; + assert!(set_subchannel_admin.is_err()); + let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; + assert!(set_channel_admin.is_ok()); + let (channels, _) = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( channels, @@ -1176,7 +1213,7 @@ test_both_dbs!( db.invite_channel_member(channel_1_2, user_2, user_1, false) .await .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, false) + db.invite_channel_member(channel_1_1, user_3, user_1, true) .await .unwrap(); @@ -1210,14 +1247,17 @@ test_both_dbs!( proto::ChannelMember { user_id: user_1.to_proto(), kind: proto::channel_member::Kind::Member.into(), + admin: true, }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), + admin: false, }, proto::ChannelMember { user_id: user_3.to_proto(), kind: proto::channel_member::Kind::Invitee.into(), + admin: true, }, ] ); @@ -1241,10 +1281,12 @@ test_both_dbs!( proto::ChannelMember { user_id: user_1.to_proto(), kind: proto::channel_member::Kind::Member.into(), + admin: true, }, proto::ChannelMember { user_id: user_2.to_proto(), kind: proto::channel_member::Kind::AncestorMember.into(), + admin: false, }, ] ); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6893c4bde4..f1fd97db41 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -246,6 +246,7 @@ impl Server { .add_request_handler(remove_channel) .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) + .add_request_handler(set_channel_member_admin) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -2150,19 +2151,24 @@ async fn create_channel( id: id.to_proto(), name: request.name, parent_id: request.parent_id, - user_is_admin: true, + user_is_admin: false, }); - if let Some(parent_id) = parent_id { - let member_ids = db.get_channel_members(parent_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())?; - } - } + let user_ids_to_notify = if let Some(parent_id) = parent_id { + db.get_channel_members(parent_id).await? } else { - session.peer.send(session.connection_id, update)?; + vec![session.user_id] + }; + + let connection_pool = session.connection_pool().await; + for user_id in user_ids_to_notify { + for connection_id in connection_pool.user_connection_ids(user_id) { + let mut update = update.clone(); + if user_id == session.user_id { + update.channels[0].user_is_admin = true; + } + session.peer.send(connection_id, update)?; + } } Ok(()) @@ -2239,8 +2245,57 @@ async fn remove_channel_member( let db = session.db().await; 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) .await?; + + let mut update = proto::UpdateChannels::default(); + update.remove_channels.push(channel_id.to_proto()); + + for connection_id in session + .connection_pool() + .await + .user_connection_ids(member_id) + { + session.peer.send(connection_id, update.clone())?; + } + + response.send(proto::Ack {})?; + Ok(()) +} + +async fn set_channel_member_admin( + request: proto::SetChannelMemberAdmin, + 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); + db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin) + .await?; + + let channel = db + .get_channel(channel_id, member_id) + .await? + .ok_or_else(|| anyhow!("channel not found"))?; + + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + user_is_admin: request.admin, + }); + + for connection_id in session + .connection_pool() + .await + .user_connection_ids(member_id) + { + session.peer.send(connection_id, update.clone())?; + } + response.send(proto::Ack {})?; Ok(()) } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 43e5a296c4..ae149f6a8a 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,12 +1,12 @@ use crate::tests::{room_participants, RoomParticipants, TestServer}; use call::ActiveCall; -use client::{Channel, User}; +use client::{Channel, ChannelMembership, User}; use gpui::{executor::Deterministic, TestAppContext}; use rpc::proto; use std::sync::Arc; #[gpui::test] -async fn test_basic_channels( +async fn test_core_channels( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -64,7 +64,7 @@ async fn test_basic_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(), true, cx); + let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx); // Make sure we're synchronously storing the pending invite assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); @@ -84,7 +84,7 @@ async fn test_basic_channels( parent_id: None, user_is_admin: false, depth: 0, - }),] + })] ) }); let members = client_a @@ -100,10 +100,12 @@ async fn test_basic_channels( &[ ( client_a.user_id().unwrap(), + true, proto::channel_member::Kind::Member, ), ( client_b.user_id().unwrap(), + false, proto::channel_member::Kind::Invitee, ), ], @@ -117,10 +119,82 @@ async fn test_basic_channels( }) .await .unwrap(); - - // Client B now sees that they are a member of channel A and its existing - // subchannels. Their admin priveleges extend to subchannels of channel A. deterministic.run_until_parked(); + + // Client B now sees that they are a member of channel A and its existing subchannels. + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!(channels.channel_invitations(), &[]); + assert_eq!( + channels.channels(), + &[ + Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: false, + depth: 0, + }), + Arc::new(Channel { + id: channel_b_id, + name: "channel-b".to_string(), + parent_id: Some(channel_a_id), + user_is_admin: false, + depth: 1, + }) + ] + ) + }); + + let channel_c_id = client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.create_channel("channel-c", Some(channel_b_id)) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channels(), + &[ + Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: false, + depth: 0, + }), + Arc::new(Channel { + id: channel_b_id, + name: "channel-b".to_string(), + parent_id: Some(channel_a_id), + user_is_admin: false, + depth: 1, + }), + Arc::new(Channel { + id: channel_c_id, + name: "channel-c".to_string(), + parent_id: Some(channel_b_id), + user_is_admin: false, + depth: 2, + }), + ] + ) + }); + + // Update client B's membership to channel A to be an admin. + client_a + .channel_store() + .update(cx_a, |store, cx| { + store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Observe that client B is now an admin of channel A, and that + // their admin priveleges extend to subchannels of channel A. client_b.channel_store().read_with(cx_b, |channels, _| { assert_eq!(channels.channel_invitations(), &[]); assert_eq!( @@ -137,65 +211,83 @@ async fn test_basic_channels( id: channel_b_id, name: "channel-b".to_string(), parent_id: Some(channel_a_id), - user_is_admin: true, + user_is_admin: false, depth: 1, - }) - ] - ) - }); - - let channel_c_id = client_a - .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.create_channel("channel-c", Some(channel_a_id)) - }) - .await - .unwrap(); - - // TODO - ensure sibling channels are sorted in a stable way - deterministic.run_until_parked(); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channels(), - &[ - Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - user_is_admin: true, - depth: 0, }), Arc::new(Channel { id: channel_c_id, name: "channel-c".to_string(), - parent_id: Some(channel_a_id), - user_is_admin: true, - depth: 1, - }), - Arc::new(Channel { - id: channel_b_id, - name: "channel-b".to_string(), - parent_id: Some(channel_a_id), - user_is_admin: true, - depth: 1, + parent_id: Some(channel_b_id), + user_is_admin: false, + depth: 2, }), ] - ) + ); + + assert!(channels.is_user_admin(channel_c_id)) }); - // Client A deletes the channel + // Client A deletes the channel, deletion also deletes subchannels. client_a .channel_store() .update(cx_a, |channel_store, _| { - channel_store.remove_channel(channel_a_id) + channel_store.remove_channel(channel_b_id) }) .await .unwrap(); deterministic.run_until_parked(); + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + })] + ) + }); + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + })] + ) + }); + + // Remove client B client_a .channel_store() - .read_with(cx_a, |channels, _| assert_eq!(channels.channels(), &[])); + .update(cx_a, |channel_store, cx| { + channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // Client A still has their channel + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + })] + ) + }); + + // Client B is gone client_b .channel_store() .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); @@ -209,13 +301,13 @@ fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u } fn assert_members_eq( - members: &[(Arc, proto::channel_member::Kind)], - expected_members: &[(u64, proto::channel_member::Kind)], + members: &[ChannelMembership], + expected_members: &[(u64, bool, proto::channel_member::Kind)], ) { assert_eq!( members .iter() - .map(|(user, status)| (user.id, *status)) + .map(|member| (member.user.id, member.admin, member.kind)) .collect::>(), expected_members ); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index df27ea5005..a84c5c111e 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -41,8 +41,7 @@ use workspace::{ }; use crate::face_pile::FacePile; - -use self::channel_modal::build_channel_modal; +use channel_modal::ChannelModal; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { @@ -1284,7 +1283,14 @@ impl CollabPanel { let channel_id = channel.id; MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() - .with_child(Svg::new("icons/file_icons/hash.svg").aligned().left()) + .with_child( + Svg::new("icons/channels.svg") + .with_color(theme.add_channel_button.color) + .constrained() + .with_width(14.) + .aligned() + .left(), + ) .with_child( Label::new(channel.name.clone(), theme.contact_username.text.clone()) .contained() @@ -1509,21 +1515,19 @@ impl CollabPanel { channel_id: u64, cx: &mut ViewContext, ) { - if let Some(channel) = self.channel_store.read(cx).channel_for_id(channel_id) { - if channel.user_is_admin { - self.context_menu.update(cx, |context_menu, cx| { - context_menu.show( - position, - gpui::elements::AnchorCorner::BottomLeft, - vec![ - ContextMenuItem::action("New Channel", NewChannel { channel_id }), - ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), - ContextMenuItem::action("Add member", AddMember { channel_id }), - ], - cx, - ); - }); - } + if self.channel_store.read(cx).is_user_admin(channel_id) { + self.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + position, + gpui::elements::AnchorCorner::BottomLeft, + vec![ + ContextMenuItem::action("New Channel", NewChannel { channel_id }), + ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), + ContextMenuItem::action("Add member", AddMember { channel_id }), + ], + cx, + ); + }); } } @@ -1697,7 +1701,7 @@ impl CollabPanel { workspace.update(&mut cx, |workspace, cx| { workspace.toggle_modal(cx, |_, cx| { cx.add_view(|cx| { - build_channel_modal( + ChannelModal::new( user_store.clone(), channel_store.clone(), channel_id, diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 5628540022..0286e30b80 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,6 +1,7 @@ -use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore}; +use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ + actions, elements::*, platform::{CursorStyle, MouseButton}, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, @@ -10,8 +11,12 @@ use std::sync::Arc; use util::TryFutureExt; use workspace::Modal; +actions!(channel_modal, [SelectNextControl, ToggleMode]); + pub fn init(cx: &mut AppContext) { Picker::::init(cx); + cx.add_action(ChannelModal::toggle_mode); + cx.add_action(ChannelModal::select_next_control); } pub struct ChannelModal { @@ -21,6 +26,110 @@ pub struct ChannelModal { has_focus: bool, } +impl ChannelModal { + pub fn new( + user_store: ModelHandle, + channel_store: ModelHandle, + channel_id: ChannelId, + mode: Mode, + members: Vec, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&channel_store, |_, _, cx| cx.notify()).detach(); + let picker = cx.add_view(|cx| { + Picker::new( + ChannelModalDelegate { + matching_users: Vec::new(), + matching_member_indices: Vec::new(), + selected_index: 0, + user_store: user_store.clone(), + channel_store: channel_store.clone(), + channel_id, + match_candidates: members + .iter() + .enumerate() + .map(|(id, member)| StringMatchCandidate { + id, + string: member.user.github_login.clone(), + char_bag: member.user.github_login.chars().collect(), + }) + .collect(), + members, + mode, + selected_column: None, + }, + cx, + ) + .with_theme(|theme| theme.collab_panel.channel_modal.picker.clone()) + }); + + cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); + let has_focus = picker.read(cx).has_focus(); + + Self { + picker, + channel_store, + channel_id, + has_focus, + } + } + + fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext) { + let mode = match self.picker.read(cx).delegate().mode { + Mode::ManageMembers => Mode::InviteMembers, + Mode::InviteMembers => Mode::ManageMembers, + }; + self.set_mode(mode, cx); + } + + fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext) { + let channel_store = self.channel_store.clone(); + let channel_id = self.channel_id; + cx.spawn(|this, mut cx| async move { + if mode == Mode::ManageMembers { + let members = channel_store + .update(&mut cx, |channel_store, cx| { + channel_store.get_channel_member_details(channel_id, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.picker + .update(cx, |picker, _| picker.delegate_mut().members = members); + })?; + } + + this.update(&mut cx, |this, cx| { + this.picker.update(cx, |picker, cx| { + let delegate = picker.delegate_mut(); + delegate.mode = mode; + picker.update_matches(picker.query(cx), cx); + cx.notify() + }); + }) + }) + .detach(); + } + + fn select_next_control(&mut self, _: &SelectNextControl, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + let delegate = picker.delegate_mut(); + match delegate.mode { + Mode::ManageMembers => match delegate.selected_column { + Some(UserColumn::Remove) => { + delegate.selected_column = Some(UserColumn::ToggleAdmin) + } + Some(UserColumn::ToggleAdmin) => { + delegate.selected_column = Some(UserColumn::Remove) + } + None => todo!(), + }, + Mode::InviteMembers => {} + } + cx.notify() + }); + } +} + impl Entity for ChannelModal { type Event = PickerEvent; } @@ -60,11 +169,7 @@ impl View for ChannelModal { }) .on_click(MouseButton::Left, move |_, this, cx| { if !active { - this.picker.update(cx, |picker, cx| { - picker.delegate_mut().mode = mode; - picker.update_matches(picker.query(cx), cx); - cx.notify(); - }) + this.set_mode(mode, cx); } }) .with_cursor_style(if active { @@ -125,65 +230,29 @@ impl Modal for ChannelModal { } } -pub fn build_channel_modal( - user_store: ModelHandle, - channel_store: ModelHandle, - channel_id: ChannelId, - mode: Mode, - members: Vec<(Arc, proto::channel_member::Kind)>, - cx: &mut ViewContext, -) -> ChannelModal { - let picker = cx.add_view(|cx| { - Picker::new( - ChannelModalDelegate { - matches: Vec::new(), - selected_index: 0, - user_store: user_store.clone(), - channel_store: channel_store.clone(), - channel_id, - match_candidates: members - .iter() - .enumerate() - .map(|(id, member)| StringMatchCandidate { - id, - string: member.0.github_login.clone(), - char_bag: member.0.github_login.chars().collect(), - }) - .collect(), - members, - mode, - }, - cx, - ) - .with_theme(|theme| theme.collab_panel.channel_modal.picker.clone()) - }); - - cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); - let has_focus = picker.read(cx).has_focus(); - - ChannelModal { - picker, - channel_store, - channel_id, - has_focus, - } -} - #[derive(Copy, Clone, PartialEq)] pub enum Mode { ManageMembers, InviteMembers, } +#[derive(Copy, Clone, PartialEq)] +pub enum UserColumn { + ToggleAdmin, + Remove, +} + pub struct ChannelModalDelegate { - matches: Vec<(Arc, Option)>, + matching_users: Vec>, + matching_member_indices: Vec, user_store: ModelHandle, channel_store: ModelHandle, channel_id: ChannelId, selected_index: usize, mode: Mode, + selected_column: Option, match_candidates: Arc<[StringMatchCandidate]>, - members: Vec<(Arc, proto::channel_member::Kind)>, + members: Vec, } impl PickerDelegate for ChannelModalDelegate { @@ -192,7 +261,10 @@ impl PickerDelegate for ChannelModalDelegate { } fn match_count(&self) -> usize { - self.matches.len() + match self.mode { + Mode::ManageMembers => self.matching_member_indices.len(), + Mode::InviteMembers => self.matching_users.len(), + } } fn selected_index(&self) -> usize { @@ -201,6 +273,10 @@ impl PickerDelegate for ChannelModalDelegate { fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { self.selected_index = ix; + self.selected_column = match self.mode { + Mode::ManageMembers => Some(UserColumn::ToggleAdmin), + Mode::InviteMembers => None, + }; } fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { @@ -220,11 +296,10 @@ impl PickerDelegate for ChannelModalDelegate { .await; picker.update(&mut cx, |picker, cx| { let delegate = picker.delegate_mut(); - delegate.matches.clear(); - delegate.matches.extend(matches.into_iter().map(|m| { - let member = &delegate.members[m.candidate_id]; - (member.0.clone(), Some(member.1)) - })); + delegate.matching_member_indices.clear(); + delegate + .matching_member_indices + .extend(matches.into_iter().map(|m| m.candidate_id)); cx.notify(); })?; anyhow::Ok(()) @@ -242,10 +317,7 @@ impl PickerDelegate for ChannelModalDelegate { let users = search_users.await?; picker.update(&mut cx, |picker, cx| { let delegate = picker.delegate_mut(); - delegate.matches.clear(); - delegate - .matches - .extend(users.into_iter().map(|user| (user, None))); + delegate.matching_users = users; cx.notify(); })?; anyhow::Ok(()) @@ -258,29 +330,23 @@ impl PickerDelegate for ChannelModalDelegate { } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if let Some((user, _)) = self.matches.get(self.selected_index) { - match self.mode { - Mode::ManageMembers => self - .channel_store - .update(cx, |store, cx| { - store.remove_member(self.channel_id, user.id, cx) - }) - .detach(), - Mode::InviteMembers => match self.member_status(user.id, cx) { - Some(proto::channel_member::Kind::Member) => {} - Some(proto::channel_member::Kind::Invitee) => self - .channel_store + if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) { + match self.member_status(selected_user.id, cx) { + Some(proto::channel_member::Kind::Member) + | Some(proto::channel_member::Kind::Invitee) => { + if self.selected_column == Some(UserColumn::ToggleAdmin) { + self.set_member_admin(selected_user.id, !admin.unwrap_or(false), cx); + } else { + self.remove_member(selected_user.id, cx); + } + } + Some(proto::channel_member::Kind::AncestorMember) | None => { + self.channel_store .update(cx, |store, cx| { - store.remove_member(self.channel_id, user.id, cx) + store.invite_member(self.channel_id, selected_user.id, false, cx) }) - .detach(), - Some(proto::channel_member::Kind::AncestorMember) | None => self - .channel_store - .update(cx, |store, cx| { - store.invite_member(self.channel_id, user.id, false, cx) - }) - .detach(), - }, + .detach(); + } } } } @@ -297,17 +363,9 @@ impl PickerDelegate for ChannelModalDelegate { cx: &gpui::AppContext, ) -> AnyElement> { let theme = &theme::current(cx).collab_panel.channel_modal; - let (user, _) = &self.matches[ix]; + let (user, admin) = self.user_at_index(ix).unwrap(); let request_status = self.member_status(user.id, cx); - let icon_path = match request_status { - Some(proto::channel_member::Kind::AncestorMember) => Some("icons/check_8.svg"), - Some(proto::channel_member::Kind::Member) => Some("icons/check_8.svg"), - Some(proto::channel_member::Kind::Invitee) => Some("icons/x_mark_8.svg"), - None => None, - }; - let button_style = &theme.contact_button; - let style = theme.picker.item.in_state(selected).style_for(mouse_state); Flex::row() .with_children(user.avatar.clone().map(|avatar| { @@ -323,20 +381,69 @@ impl PickerDelegate for ChannelModalDelegate { .aligned() .left(), ) - .with_children(icon_path.map(|icon_path| { - Svg::new(icon_path) - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .aligned() + .with_children(admin.map(|admin| { + let member_style = theme.admin_toggle_part.in_state(!admin); + let admin_style = theme.admin_toggle_part.in_state(admin); + Flex::row() + .with_child( + Label::new("member", member_style.text.clone()) + .contained() + .with_style(member_style.container), + ) + .with_child( + Label::new("admin", admin_style.text.clone()) + .contained() + .with_style(admin_style.container), + ) .contained() - .with_style(button_style.container) - .constrained() - .with_width(button_style.button_width) - .with_height(button_style.button_width) + .with_style(theme.admin_toggle) .aligned() .flex_float() })) + .with_children({ + match self.mode { + Mode::ManageMembers => match request_status { + Some(proto::channel_member::Kind::Member) => Some( + Label::new("remove member", theme.remove_member_button.text.clone()) + .contained() + .with_style(theme.remove_member_button.container) + .into_any(), + ), + Some(proto::channel_member::Kind::Invitee) => Some( + Label::new("cancel invite", theme.cancel_invite_button.text.clone()) + .contained() + .with_style(theme.cancel_invite_button.container) + .into_any(), + ), + Some(proto::channel_member::Kind::AncestorMember) | None => None, + }, + Mode::InviteMembers => { + let svg = match request_status { + Some(proto::channel_member::Kind::Member) => Some( + Svg::new("icons/check_8.svg") + .with_color(theme.member_icon.color) + .constrained() + .with_width(theme.member_icon.width) + .aligned() + .contained() + .with_style(theme.member_icon.container), + ), + Some(proto::channel_member::Kind::Invitee) => Some( + Svg::new("icons/check_8.svg") + .with_color(theme.invitee_icon.color) + .constrained() + .with_width(theme.invitee_icon.width) + .aligned() + .contained() + .with_style(theme.invitee_icon.container), + ), + Some(proto::channel_member::Kind::AncestorMember) | None => None, + }; + + svg.map(|svg| svg.aligned().flex_float().into_any()) + } + } + }) .contained() .with_style(style.container) .constrained() @@ -353,11 +460,56 @@ impl ChannelModalDelegate { ) -> Option { self.members .iter() - .find_map(|(user, status)| (user.id == user_id).then_some(*status)) + .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind)) .or(self .channel_store .read(cx) .has_pending_channel_invite(self.channel_id, user_id) .then_some(proto::channel_member::Kind::Invitee)) } + + 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), + )) + }), + Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)), + } + } + + fn set_member_admin(&mut self, user_id: u64, admin: bool, cx: &mut ViewContext>) { + let update = self.channel_store.update(cx, |store, cx| { + store.set_member_admin(self.channel_id, user_id, admin, 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; + } + }) + }) + .detach_and_log_err(cx); + } + + fn remove_member(&mut self, user_id: u64, cx: &mut ViewContext>) { + let update = self.channel_store.update(cx, |store, cx| { + store.remove_member(self.channel_id, user_id, cx) + }); + cx.spawn(|picker, mut cx| async move { + update.await?; + picker.update(&mut cx, |picker, cx| { + let this = picker.delegate_mut(); + if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) { + this.members.remove(ix); + } + }) + }) + .detach_and_log_err(cx); + } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7dd5a0a893..8f187a87c6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -140,6 +140,7 @@ message Envelope { RemoveChannel remove_channel = 127; GetChannelMembers get_channel_members = 128; GetChannelMembersResponse get_channel_members_response = 129; + SetChannelMemberAdmin set_channel_member_admin = 130; } } @@ -898,7 +899,8 @@ message GetChannelMembersResponse { message ChannelMember { uint64 user_id = 1; - Kind kind = 2; + bool admin = 2; + Kind kind = 3; enum Kind { Member = 0; @@ -927,6 +929,12 @@ message RemoveChannelMember { uint64 user_id = 2; } +message SetChannelMemberAdmin { + uint64 channel_id = 1; + uint64 user_id = 2; + bool admin = 3; +} + message RespondToChannelInvite { uint64 channel_id = 1; bool accept = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c23bbb23e4..fac011f803 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -217,6 +217,7 @@ messages!( (JoinChannel, Foreground), (RoomUpdated, Foreground), (SaveBuffer, Foreground), + (SetChannelMemberAdmin, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (ShareProject, Foreground), @@ -298,6 +299,7 @@ request_messages!( (RemoveContact, Ack), (RespondToContactRequest, Ack), (RespondToChannelInvite, Ack), + (SetChannelMemberAdmin, Ack), (GetChannelMembers, GetChannelMembersResponse), (JoinChannel, JoinRoomResponse), (RemoveChannel, Ack), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8d0159d7ad..448f6ca5dd 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -255,8 +255,12 @@ pub struct ChannelModal { pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainerStyle, - pub contact_button: IconButton, - pub disabled_contact_button: IconButton, + pub remove_member_button: ContainedText, + pub cancel_invite_button: ContainedText, + pub member_icon: Icon, + pub invitee_icon: Icon, + pub admin_toggle: ContainerStyle, + pub admin_toggle_part: Toggleable, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 951591676b..a097bc561f 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -41,6 +41,61 @@ export default function channel_modal(): any { } return { + member_icon: { + background: background(theme.middle), + padding: { + bottom: 4, + left: 4, + right: 4, + top: 4, + }, + width: 5, + color: foreground(theme.middle, "accent"), + }, + invitee_icon: { + background: background(theme.middle), + padding: { + bottom: 4, + left: 4, + right: 4, + top: 4, + }, + width: 5, + color: foreground(theme.middle, "accent"), + }, + remove_member_button: { + ...text(theme.middle, "sans", { size: "xs" }), + background: background(theme.middle), + padding: { + left: 7, + right: 7 + } + }, + cancel_invite_button: { + ...text(theme.middle, "sans", { size: "xs" }), + background: background(theme.middle), + }, + admin_toggle_part: toggleable({ + base: { + ...text(theme.middle, "sans", { size: "xs" }), + padding: { + left: 7, + right: 7, + }, + }, + state: { + active: { + background: background(theme.middle, "on"), + } + } + }), + admin_toggle: { + border: border(theme.middle, "active"), + background: background(theme.middle), + margin: { + right: 8, + } + }, container: { background: background(theme.lowest), border: border(theme.lowest), From 2ccd153233a986dbf7dabdec151a1f98d7dc2741 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 4 Aug 2023 16:14:01 -0700 Subject: [PATCH 098/326] Fix joining descendant channels, style channel invites Co-authored-by: Mikayla --- .../icons/{channels.svg => channel_hash.svg} | 0 crates/client/src/channel_store.rs | 2 +- crates/collab/src/db.rs | 34 +- crates/collab/src/tests/channel_tests.rs | 32 ++ crates/collab_ui/src/collab_panel.rs | 336 +++++++++--------- .../src/collab_panel/channel_modal.rs | 4 +- crates/theme/src/theme.rs | 8 +- styles/src/style_tree/collab_panel.ts | 37 +- 8 files changed, 260 insertions(+), 193 deletions(-) rename assets/icons/{channels.svg => channel_hash.svg} (100%) diff --git a/assets/icons/channels.svg b/assets/icons/channel_hash.svg similarity index 100% rename from assets/icons/channels.svg rename to assets/icons/channel_hash.svg diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 51176986ef..13510a1e1c 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -104,7 +104,7 @@ impl ChannelStore { parent_id: Option, ) -> impl Future> { let client = self.client.clone(); - let name = name.to_owned(); + let name = name.trim_start_matches("#").to_owned(); async move { Ok(client .request(proto::CreateChannel { name, parent_id }) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9dc4ad805b..c3ffc12634 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1381,16 +1381,8 @@ impl Database { ) -> Result> { self.room_transaction(room_id, |tx| async move { if let Some(channel_id) = channel_id { - channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Accepted.eq(true)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such channel membership"))?; + self.check_user_is_channel_member(channel_id, user_id, &*tx) + .await?; room_participant::ActiveModel { room_id: ActiveValue::set(room_id), @@ -1738,7 +1730,6 @@ 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? } else { @@ -3595,6 +3586,25 @@ impl Database { Ok(user_ids) } + 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"))?; + Ok(()) + } + async fn check_user_is_channel_admin( &self, channel_id: ChannelId, @@ -3611,7 +3621,7 @@ impl Database { ) .one(&*tx) .await? - .ok_or_else(|| anyhow!("user is not allowed to remove this channel"))?; + .ok_or_else(|| anyhow!("user is not a channel admin"))?; Ok(()) } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index ae149f6a8a..88d88a40fd 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -313,6 +313,38 @@ fn assert_members_eq( ); } +#[gpui::test] +async fn test_joining_channel_ancestor_member( + 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 parent_id = server + .make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let sub_id = client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.create_channel("sub_channel", Some(parent_id)) + }) + .await + .unwrap(); + + let active_call_b = cx_b.read(ActiveCall::global); + + assert!(active_call_b + .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx)) + .await + .is_ok()); +} + #[gpui::test] async fn test_channel_room( deterministic: Arc, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a84c5c111e..382381dba1 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -120,7 +120,8 @@ pub enum Event { enum Section { ActiveCall, Channels, - Requests, + ChannelInvites, + ContactRequests, Contacts, Online, Offline, @@ -404,17 +405,55 @@ impl CollabPanel { let old_entries = mem::take(&mut self.entries); if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - let mut participant_entries = Vec::new(); + self.entries.push(ListEntry::Header(Section::ActiveCall, 0)); - // Populate the active user. - if let Some(user) = user_store.current_user() { + if !self.collapsed_sections.contains(&Section::ActiveCall) { + let room = room.read(cx); + + // Populate the active user. + if let Some(user) = user_store.current_user() { + self.match_candidates.clear(); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + let user_id = user.id; + self.entries.push(ListEntry::CallParticipant { + user, + is_pending: false, + }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + self.entries.push(ListEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_last: projects.peek().is_none(), + }); + } + } + } + + // Populate remote participants. self.match_candidates.clear(); - self.match_candidates.push(StringMatchCandidate { - id: 0, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }); + self.match_candidates + .extend(room.remote_participants().iter().map(|(_, participant)| { + StringMatchCandidate { + id: participant.user.id as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + } + })); let matches = executor.block(match_strings( &self.match_candidates, &query, @@ -423,97 +462,54 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() { - let user_id = user.id; - participant_entries.push(ListEntry::CallParticipant { - user, + for mat in matches { + let user_id = mat.candidate_id as u64; + let participant = &room.remote_participants()[&user_id]; + self.entries.push(ListEntry::CallParticipant { + user: participant.user.clone(), is_pending: false, }); - let mut projects = room.local_participant().projects.iter().peekable(); + let mut projects = participant.projects.iter().peekable(); while let Some(project) = projects.next() { - participant_entries.push(ListEntry::ParticipantProject { + self.entries.push(ListEntry::ParticipantProject { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), - host_user_id: user_id, - is_last: projects.peek().is_none(), + host_user_id: participant.user.id, + is_last: projects.peek().is_none() + && participant.video_tracks.is_empty(), + }); + } + if !participant.video_tracks.is_empty() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: participant.peer_id, + is_last: true, }); } } - } - // Populate remote participants. - self.match_candidates.clear(); - self.match_candidates - .extend(room.remote_participants().iter().map(|(_, participant)| { - StringMatchCandidate { - id: participant.user.id as usize, - string: participant.user.github_login.clone(), - char_bag: participant.user.github_login.chars().collect(), - } - })); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - for mat in matches { - let user_id = mat.candidate_id as u64; - let participant = &room.remote_participants()[&user_id]; - participant_entries.push(ListEntry::CallParticipant { - user: participant.user.clone(), - is_pending: false, - }); - let mut projects = participant.projects.iter().peekable(); - while let Some(project) = projects.next() { - participant_entries.push(ListEntry::ParticipantProject { - project_id: project.id, - worktree_root_names: project.worktree_root_names.clone(), - host_user_id: participant.user.id, - is_last: projects.peek().is_none() && participant.video_tracks.is_empty(), - }); - } - if !participant.video_tracks.is_empty() { - participant_entries.push(ListEntry::ParticipantScreen { - peer_id: participant.peer_id, - is_last: true, - }); - } - } - - // Populate pending participants. - self.match_candidates.clear(); - self.match_candidates - .extend( - room.pending_participants() - .iter() - .enumerate() - .map(|(id, participant)| StringMatchCandidate { + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.pending_participants().iter().enumerate().map( + |(id, participant)| StringMatchCandidate { id, string: participant.github_login.clone(), char_bag: participant.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - participant_entries.extend(matches.iter().map(|mat| ListEntry::CallParticipant { - user: room.pending_participants()[mat.candidate_id].clone(), - is_pending: true, - })); - - if !participant_entries.is_empty() { - self.entries.push(ListEntry::Header(Section::ActiveCall, 0)); - if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(participant_entries); - } + }, + )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + self.entries + .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + is_pending: true, + })); } } @@ -559,8 +555,6 @@ impl CollabPanel { } } - self.entries.push(ListEntry::Header(Section::Contacts, 0)); - let mut request_entries = Vec::new(); let channel_invites = channel_store.channel_invitations(); if !channel_invites.is_empty() { @@ -586,8 +580,19 @@ impl CollabPanel { .iter() .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())), ); + + if !request_entries.is_empty() { + self.entries + .push(ListEntry::Header(Section::ChannelInvites, 1)); + if !self.collapsed_sections.contains(&Section::ChannelInvites) { + self.entries.append(&mut request_entries); + } + } } + self.entries.push(ListEntry::Header(Section::Contacts, 0)); + + request_entries.clear(); let incoming = user_store.incoming_contact_requests(); if !incoming.is_empty() { self.match_candidates.clear(); @@ -647,8 +652,9 @@ impl CollabPanel { } if !request_entries.is_empty() { - self.entries.push(ListEntry::Header(Section::Requests, 1)); - if !self.collapsed_sections.contains(&Section::Requests) { + self.entries + .push(ListEntry::Header(Section::ContactRequests, 1)); + if !self.collapsed_sections.contains(&Section::ContactRequests) { self.entries.append(&mut request_entries); } } @@ -1043,9 +1049,10 @@ impl CollabPanel { let tooltip_style = &theme.tooltip; let text = match section { Section::ActiveCall => "Current Call", - Section::Requests => "Requests", + Section::ContactRequests => "Requests", Section::Contacts => "Contacts", Section::Channels => "Channels", + Section::ChannelInvites => "Invites", Section::Online => "Online", Section::Offline => "Offline", }; @@ -1055,15 +1062,13 @@ impl CollabPanel { Section::ActiveCall => Some( MouseEventHandler::::new(0, cx, |_, _| { render_icon_button( - &theme.collab_panel.leave_call_button, + theme.collab_panel.leave_call_button.in_state(is_selected), "icons/radix/exit.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, _, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); + Self::leave_call(cx); }) .with_tooltip::( 0, @@ -1076,7 +1081,7 @@ impl CollabPanel { Section::Contacts => Some( MouseEventHandler::::new(0, cx, |_, _| { render_icon_button( - &theme.collab_panel.add_contact_button, + theme.collab_panel.add_contact_button.in_state(is_selected), "icons/user_plus_16.svg", ) }) @@ -1094,7 +1099,10 @@ impl CollabPanel { ), Section::Channels => Some( MouseEventHandler::::new(0, cx, |_, _| { - render_icon_button(&theme.collab_panel.add_contact_button, "icons/plus_16.svg") + render_icon_button( + theme.collab_panel.add_contact_button.in_state(is_selected), + "icons/plus_16.svg", + ) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) @@ -1284,10 +1292,10 @@ impl CollabPanel { MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() .with_child( - Svg::new("icons/channels.svg") - .with_color(theme.add_channel_button.color) + Svg::new("icons/channel_hash.svg") + .with_color(theme.channel_hash.color) .constrained() - .with_width(14.) + .with_width(theme.channel_hash.width) .aligned() .left(), ) @@ -1313,11 +1321,15 @@ impl CollabPanel { }), ), ) + .align_children_center() .constrained() .with_height(theme.row_height) .contained() .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) - .with_margin_left(20. * channel.depth as f32) + .with_padding_left( + theme.contact_row.default_style().padding.left + + theme.channel_indent * channel.depth as f32, + ) }) .on_click(MouseButton::Left, move |_, this, cx| { this.join_channel(channel_id, cx); @@ -1345,7 +1357,14 @@ impl CollabPanel { let button_spacing = theme.contact_button_spacing; Flex::row() - .with_child(Svg::new("icons/file_icons/hash.svg").aligned().left()) + .with_child( + Svg::new("icons/channel_hash.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) .with_child( Label::new(channel.name.clone(), theme.contact_username.text.clone()) .contained() @@ -1403,6 +1422,9 @@ impl CollabPanel { .in_state(is_selected) .style_for(&mut Default::default()), ) + .with_padding_left( + theme.contact_row.default_style().padding.left + theme.channel_indent, + ) .into_any() } @@ -1532,30 +1554,23 @@ impl CollabPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - let mut did_clear = self.filter_editor.update(cx, |editor, cx| { - if editor.buffer().read(cx).len(cx) > 0 { - editor.set_text("", cx); - true - } else { - false - } - }); - - did_clear |= self.take_editing_state(cx).is_some(); - - if !did_clear { - cx.emit(Event::Dismissed); + if self.take_editing_state(cx).is_some() { + cx.focus(&self.filter_editor); + } else { + self.filter_editor.update(cx, |editor, cx| { + if editor.buffer().read(cx).len(cx) > 0 { + editor.set_text("", cx); + } + }); } + + self.update_entries(cx); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - let mut ix = self.selection.map_or(0, |ix| ix + 1); - while let Some(entry) = self.entries.get(ix) { - if entry.is_selectable() { - self.selection = Some(ix); - break; - } - ix += 1; + let ix = self.selection.map_or(0, |ix| ix + 1); + if ix < self.entries.len() { + self.selection = Some(ix); } self.list_state.reset(self.entries.len()); @@ -1569,16 +1584,9 @@ impl CollabPanel { } fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(mut ix) = self.selection.take() { - while ix > 0 { - ix -= 1; - if let Some(entry) = self.entries.get(ix) { - if entry.is_selectable() { - self.selection = Some(ix); - break; - } - } - } + let ix = self.selection.take().unwrap_or(0); + if ix > 0 { + self.selection = Some(ix - 1); } self.list_state.reset(self.entries.len()); @@ -1595,9 +1603,17 @@ impl CollabPanel { if let Some(selection) = self.selection { if let Some(entry) = self.entries.get(selection) { match entry { - ListEntry::Header(section, _) => { - self.toggle_expanded(*section, cx); - } + ListEntry::Header(section, _) => match section { + Section::ActiveCall => Self::leave_call(cx), + Section::Channels => self.new_root_channel(cx), + Section::Contacts => self.toggle_contact_finder(cx), + Section::ContactRequests + | Section::Online + | Section::Offline + | Section::ChannelInvites => { + self.toggle_expanded(*section, cx); + } + }, ListEntry::Contact { contact, calling } => { if contact.online && !contact.busy && !calling { self.call(contact.user.id, Some(self.project.clone()), cx); @@ -1626,6 +1642,9 @@ impl CollabPanel { }); } } + ListEntry::Channel(channel) => { + self.join_channel(channel.id, cx); + } _ => {} } } @@ -1651,6 +1670,12 @@ impl CollabPanel { self.update_entries(cx); } + fn leave_call(cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + } + fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { @@ -1666,23 +1691,17 @@ impl CollabPanel { } fn new_root_channel(&mut self, cx: &mut ViewContext) { - if self.channel_editing_state.is_none() { - self.channel_editing_state = Some(ChannelEditingState { parent_id: None }); - self.update_entries(cx); - } - + self.channel_editing_state = Some(ChannelEditingState { parent_id: None }); + self.update_entries(cx); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { - if self.channel_editing_state.is_none() { - self.channel_editing_state = Some(ChannelEditingState { - parent_id: Some(action.channel_id), - }); - self.update_entries(cx); - } - + self.channel_editing_state = Some(ChannelEditingState { + parent_id: Some(action.channel_id), + }); + self.update_entries(cx); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } @@ -1825,6 +1844,13 @@ impl View for CollabPanel { fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { if !self.has_focus { self.has_focus = true; + if !self.context_menu.is_focused(cx) { + if self.channel_editing_state.is_some() { + cx.focus(&self.channel_name_editor); + } else { + cx.focus(&self.filter_editor); + } + } cx.emit(Event::Focus); } } @@ -1931,16 +1957,6 @@ impl Panel for CollabPanel { } } -impl ListEntry { - fn is_selectable(&self) -> bool { - if let ListEntry::Header(_, 0) = self { - false - } else { - true - } - } -} - impl PartialEq for ListEntry { fn eq(&self, other: &Self) -> bool { match self { diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 0286e30b80..1b1a50dbe4 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -487,7 +487,7 @@ impl ChannelModalDelegate { }); cx.spawn(|picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, cx| { + picker.update(&mut cx, |picker, _| { let this = picker.delegate_mut(); if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) { member.admin = admin; @@ -503,7 +503,7 @@ impl ChannelModalDelegate { }); cx.spawn(|picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, cx| { + picker.update(&mut cx, |picker, _| { let this = picker.delegate_mut(); if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) { this.members.remove(ix); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 448f6ca5dd..8bd673d1b3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,12 +220,13 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub channel_hash: Icon, pub channel_modal: ChannelModal, pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, - pub leave_call_button: IconButton, - pub add_contact_button: IconButton, - pub add_channel_button: IconButton, + pub leave_call_button: Toggleable, + pub add_contact_button: Toggleable, + pub add_channel_button: Toggleable, pub header_row: ContainedText, pub subheader_row: Toggleable>, pub leave_call: Interactive, @@ -239,6 +240,7 @@ pub struct CollabPanel { pub contact_username: ContainedText, pub contact_button: Interactive, pub contact_button_spacing: f32, + pub channel_indent: f32, pub disabled_button: IconButton, pub section_icon_size: f32, pub calling_indicator: ContainedText, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index ea550dea6b..f24468dca6 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -51,6 +51,20 @@ export default function contacts_panel(): any { }, } + const headerButton = toggleable({ + base: { + color: foreground(layer, "on"), + button_width: 28, + icon_width: 16, + }, + state: { + active: { + background: background(layer, "active"), + corner_radius: 8, + } + } + }) + return { channel_modal: channel_modal(), background: background(layer), @@ -77,23 +91,16 @@ export default function contacts_panel(): any { right: side_padding, }, }, + channel_hash: { + color: foreground(layer, "on"), + width: 14, + }, user_query_editor_height: 33, - add_contact_button: { - color: foreground(layer, "on"), - button_width: 28, - icon_width: 16, - }, - add_channel_button: { - color: foreground(layer, "on"), - button_width: 28, - icon_width: 16, - }, - leave_call_button: { - color: foreground(layer, "on"), - button_width: 28, - icon_width: 16, - }, + add_contact_button: headerButton, + add_channel_button: headerButton, + leave_call_button: headerButton, row_height: 28, + channel_indent: 10, section_icon_size: 8, header_row: { ...text(layer, "mono", { size: "sm", weight: "bold" }), From 7a1f40405a9785a234f69da5d17661a9db455e46 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:22:10 +0200 Subject: [PATCH 099/326] Add dismiss button to project search --- crates/search/src/buffer_search.rs | 42 +++++------------------------ crates/search/src/project_search.rs | 17 +++++++++++- crates/search/src/search.rs | 1 + crates/search/src/search_bar.rs | 34 +++++++++++++++++++++++ 4 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 crates/search/src/search_bar.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 45842aa561..baba45e0f6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -222,7 +222,12 @@ impl View for BufferSearchBar { ) .flex(1., true), ) - .with_child(self.render_close_button(&theme.search, cx)) + .with_child(super::search_bar::render_close_button( + &theme.search, + cx, + |_, this, cx| this.dismiss(&Default::default(), cx), + Some(Box::new(Dismiss)), + )) .contained() .with_style(theme.search.container) .into_any_named("search bar") @@ -518,41 +523,6 @@ impl BufferSearchBar { .into_any() } - fn render_close_button( - &self, - theme: &theme::Search, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip = "Dismiss Buffer Search"; - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum CloseButton {} - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx) - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - 0, - tooltip.to_string(), - Some(Box::new(Dismiss)), - tooltip_style, - cx, - ) - .into_any() - } - fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { let mut propagate_action = true; if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e2ad89117f..53b54192da 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -362,10 +362,12 @@ impl ProjectSearch { } } +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ViewEvent { UpdateTab, Activate, EditorEvent(editor::Event), + Dismiss, } impl Entity for ProjectSearchView { @@ -547,7 +549,9 @@ impl Item for ProjectSearchView { .then(|| query_text.into()) .or_else(|| Some("Project Search".into())) } - + fn should_close_item_on_event(event: &Self::Event) -> bool { + event == &Self::Event::Dismiss + } fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -679,6 +683,7 @@ impl Item for ProjectSearchView { smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab] } ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event), + ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem], _ => SmallVec::new(), } } @@ -2010,6 +2015,16 @@ impl View for ProjectSearchBar { .with_child(self.render_search_mode_button(SearchMode::Text, cx)) .with_children(semantic_index) .with_child(self.render_search_mode_button(SearchMode::Regex, cx)) + .with_child(super::search_bar::render_close_button( + &theme.search, + cx, + |_, this, cx| { + if let Some(search) = this.active_project_search.as_mut() { + search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) + } + }, + None, + )) .constrained() .with_height(theme.workspace.toolbar.height) .contained() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index f1711afec2..7940490de9 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -7,6 +7,7 @@ use smallvec::SmallVec; pub mod buffer_search; pub mod project_search; +pub(crate) mod search_bar; pub fn init(cx: &mut AppContext) { buffer_search::init(cx); diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs new file mode 100644 index 0000000000..5119e7866e --- /dev/null +++ b/crates/search/src/search_bar.rs @@ -0,0 +1,34 @@ +use gpui::{ + elements::{MouseEventHandler, Svg}, + platform::{CursorStyle, MouseButton}, + scene::MouseClick, + Action, AnyElement, Element, EventContext, View, ViewContext, +}; + +pub(super) fn render_close_button( + theme: &theme::Search, + cx: &mut ViewContext, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + dismiss_action: Option>, +) -> AnyElement { + let tooltip = "Dismiss Buffer Search"; + let tooltip_style = theme::current(cx).tooltip.clone(); + + enum CloseButton {} + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state); + Svg::new("icons/x_mark_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) + .into_any() +} From 19eb28035144f18111ae88657f14bc66aa957860 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Sun, 6 Aug 2023 23:42:30 +0100 Subject: [PATCH 100/326] Fix selection background too Refactor code to centralize the logic too --- assets/settings/default.json | 4 +- crates/editor/src/display_map.rs | 5 +- crates/editor/src/element.rs | 235 ++++++++++++++++++------------- 3 files changed, 141 insertions(+), 103 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 397dac0961..c6235e80a1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -214,7 +214,9 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [".env"] + "disabled_globs": [ + ".env" + ] }, // Settings specific to journaling "journal": { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 31183c484d..aee41e6c53 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -368,7 +368,8 @@ impl DisplaySnapshot { let new_end = if range.end.column == 0 { range.end } else if range.end.row < self.max_buffer_row() { - Point::new(range.end.row + 1, 0) + self.buffer_snapshot + .clip_point(Point::new(range.end.row + 1, 0), Bias::Left) } else { self.buffer_snapshot.max_point() }; @@ -376,7 +377,7 @@ impl DisplaySnapshot { new_start..new_end } - pub fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { + fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { let inlay_point = self.inlay_snapshot.to_inlay_point(point); let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); let tab_point = self.tab_snapshot.to_tab_point(fold_point); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d2e7ab00d7..6d0161e086 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -60,10 +60,10 @@ enum FoldMarkers {} struct SelectionLayout { head: DisplayPoint, - reversed: bool, cursor_shape: CursorShape, is_newest: bool, range: Range, + active_rows: Range, } impl SelectionLayout { @@ -74,27 +74,42 @@ impl SelectionLayout { map: &DisplaySnapshot, is_newest: bool, ) -> Self { + let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); + let display_selection = point_selection.map(|p| p.to_display_point(map)); + let mut range = display_selection.range(); + let mut head = display_selection.head(); + let mut active_rows = map.prev_line_boundary(point_selection.start).1.row() + ..map.next_line_boundary(point_selection.end).1.row(); + if line_mode { - let selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); - let point_range = map.expand_to_line(selection.range()); - Self { - head: selection.head().to_display_point(map), - reversed: selection.reversed, - cursor_shape, - is_newest, - range: point_range.start.to_display_point(map) - ..point_range.end.to_display_point(map), - } - } else { - let selection = selection.map(|p| p.to_display_point(map)); - Self { - head: selection.head(), - reversed: selection.reversed, - cursor_shape, - is_newest, - range: selection.range(), + let point_range = map.expand_to_line(point_selection.range()); + range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map); + } + + if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed { + if head.column() > 0 { + head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left) + } else if head.row() > 0 && head != map.max_point() { + head = map.clip_point( + DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)), + Bias::Left, + ); + + // updating range.end is a no-op unless you're on a multi-buffer divider + // in which case the clip_point may have moved the head up + // an additional row. + range.end = DisplayPoint::new(head.row() + 1, 0); + active_rows.end = head.row(); } } + + Self { + head, + cursor_shape, + is_newest, + range, + active_rows, + } } } @@ -847,36 +862,14 @@ impl EditorElement { if editor.show_local_cursors(cx) || replica_id != local_replica_id { let cursor_position = selection.head; - let mut cursor_column = cursor_position.column() as usize; - let mut cursor_row = cursor_position.row(); - - // highlight the last character in a selection - if CursorShape::Block == selection.cursor_shape - && !selection.range.is_empty() - && !selection.reversed + if layout + .visible_display_row_range + .contains(&cursor_position.row()) { - if cursor_column > 0 { - cursor_column -= 1; - } else if cursor_row > 0 - && cursor_position != layout.position_map.snapshot.max_point() - { - let new = layout.position_map.snapshot.clip_point( - DisplayPoint::new( - cursor_row - 1, - layout.position_map.snapshot.line_len(cursor_row), - ), - Bias::Left, - ); - cursor_row = new.row(); - cursor_column = new.column() as usize; - } - } - dbg!(selection.head, cursor_row, cursor_column); - - if layout.visible_display_row_range.contains(&cursor_row) { let cursor_row_layout = &layout.position_map.line_layouts - [(cursor_row - start_row) as usize] + [(cursor_position.row() - start_row) as usize] .line; + let cursor_column = cursor_position.column() as usize; let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); let mut block_width = @@ -888,10 +881,7 @@ impl EditorElement { layout .position_map .snapshot - .chars_at(DisplayPoint::new( - cursor_row as u32, - cursor_column as u32, - )) + .chars_at(cursor_position) .next() .and_then(|(character, _)| { let font_id = @@ -916,7 +906,8 @@ impl EditorElement { }; let x = cursor_character_x - scroll_left; - let y = cursor_row as f32 * layout.position_map.line_height - scroll_top; + let y = cursor_position.row() as f32 * layout.position_map.line_height + - scroll_top; if selection.is_newest { editor.pixel_position_of_newest_cursor = Some(vec2f( bounds.origin_x() + x + block_width / 2., @@ -2187,34 +2178,37 @@ impl Element for EditorElement { } selections.extend(remote_selections); + let mut newest_selection_head = None; + if editor.show_local_selections { - let mut local_selections = editor + let mut local_selections: Vec> = editor .selections .disjoint_in_range(start_anchor..end_anchor, cx); local_selections.extend(editor.selections.pending(cx)); + let mut layouts = Vec::new(); let newest = editor.selections.newest(cx); - for selection in &local_selections { + for selection in local_selections.drain(..) { let is_empty = selection.start == selection.end; - let selection_start = snapshot.prev_line_boundary(selection.start).1; - let mut selection_end = snapshot.next_line_boundary(selection.end).1; + let is_newest = selection == newest; - // in vim visual mode the newline is considered at the end of the previous line - // instead of at the start of the current line - if editor.cursor_shape == CursorShape::Block - && !is_empty - && !selection.reversed - && selection.end.column == 0 - && selection_end.row() > 0 - && selection_end.row() < snapshot.max_buffer_row() - { - selection_end = DisplayPoint::new(selection_end.row() - 1, 0); + let layout = SelectionLayout::new( + selection, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + is_newest, + ); + if is_newest { + newest_selection_head = Some(layout.head); } - for row in cmp::max(selection_start.row(), start_row) - ..=cmp::min(selection_end.row(), end_row) + + for row in cmp::max(layout.active_rows.start, start_row) + ..=cmp::min(layout.active_rows.end, end_row) { let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); *contains_non_empty_selection |= !is_empty; } + layouts.push(layout); } // Render the local selections in the leader's color when following. @@ -2222,22 +2216,7 @@ impl Element for EditorElement { .leader_replica_id .unwrap_or_else(|| editor.replica_id(cx)); - selections.push(( - local_replica_id, - local_selections - .into_iter() - .map(|selection| { - let is_newest = selection == newest; - SelectionLayout::new( - selection, - editor.selections.line_mode, - editor.cursor_shape, - &snapshot.display_snapshot, - is_newest, - ) - }) - .collect(), - )); + selections.push((local_replica_id, layouts)); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2342,28 +2321,26 @@ impl Element for EditorElement { snapshot = editor.snapshot(cx); } - let newest_selection_head = editor - .selections - .newest::(cx) - .head() - .to_display_point(&snapshot); let style = editor.style(cx); let mut context_menu = None; let mut code_actions_indicator = None; - if (start_row..end_row).contains(&newest_selection_head.row()) { - if editor.context_menu_visible() { - context_menu = editor.render_context_menu(newest_selection_head, style.clone(), cx); + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + if editor.context_menu_visible() { + context_menu = + editor.render_context_menu(newest_selection_head, style.clone(), cx); + } + + let active = matches!( + editor.context_menu, + Some(crate::ContextMenu::CodeActions(_)) + ); + + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|indicator| (newest_selection_head.row(), indicator)); } - - let active = matches!( - editor.context_menu, - Some(crate::ContextMenu::CodeActions(_)) - ); - - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|indicator| (newest_selection_head.row(), indicator)); } let visible_rows = start_row..start_row + line_layouts.len() as u32; @@ -3040,6 +3017,64 @@ mod tests { assert_eq!(layouts.len(), 6); } + #[gpui::test] + fn test_vim_visual_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let (_, editor) = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }); + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, state) = editor.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(1, 0), + Point::new(3, 2)..Point::new(3, 3), + Point::new(5, 6)..Point::new(6, 0), + ]); + }); + let mut new_parents = Default::default(); + let mut notify_views_if_parents_change = Default::default(); + let mut layout_cx = LayoutContext::new( + cx, + &mut new_parents, + &mut notify_views_if_parents_change, + false, + ); + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + &mut layout_cx, + ) + }); + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 3); + // moves cursor back one line + assert_eq!( + local_selections[0].range, + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 6) + ); + // moves cursor back one column + assert_eq!( + local_selections[1].range, + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 2) + ); + // leaves cursor on the max point + assert_eq!( + local_selections[2].range, + DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) + ); + + // active lines does not include 1 + assert_eq!( + state.active_rows.keys().cloned().collect::>(), + vec![0, 3, 5, 6] + ); + } + #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); From f1957b1737648429d2272f001abc995067027dce Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 13:31:09 -0700 Subject: [PATCH 101/326] Push focus and fix keybindings --- assets/keymaps/default.json | 8 +++- .../src/collab_panel/channel_modal.rs | 45 ++++++++++--------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index d99a660850..11cc50a03e 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -553,8 +553,12 @@ { "context": "ChannelModal", "bindings": { - "left": "channel_modal::SelectNextControl", - "right": "channel_modal::SelectNextControl", + "tab": "channel_modal::ToggleMode" + } + }, + { + "context": "ChannelModal > Picker > Editor", + "bindings": { "tab": "channel_modal::ToggleMode" } }, diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 1b1a50dbe4..f1775eb084 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -16,7 +16,7 @@ actions!(channel_modal, [SelectNextControl, ToggleMode]); pub fn init(cx: &mut AppContext) { Picker::::init(cx); cx.add_action(ChannelModal::toggle_mode); - cx.add_action(ChannelModal::select_next_control); + // cx.add_action(ChannelModal::select_next_control); } pub struct ChannelModal { @@ -64,6 +64,7 @@ impl ChannelModal { }); cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); + let has_focus = picker.read(cx).has_focus(); Self { @@ -105,29 +106,30 @@ impl ChannelModal { picker.update_matches(picker.query(cx), cx); cx.notify() }); + cx.notify() }) }) .detach(); } - fn select_next_control(&mut self, _: &SelectNextControl, cx: &mut ViewContext) { - self.picker.update(cx, |picker, cx| { - let delegate = picker.delegate_mut(); - match delegate.mode { - Mode::ManageMembers => match delegate.selected_column { - Some(UserColumn::Remove) => { - delegate.selected_column = Some(UserColumn::ToggleAdmin) - } - Some(UserColumn::ToggleAdmin) => { - delegate.selected_column = Some(UserColumn::Remove) - } - None => todo!(), - }, - Mode::InviteMembers => {} - } - cx.notify() - }); - } + // fn select_next_control(&mut self, _: &SelectNextControl, cx: &mut ViewContext) { + // self.picker.update(cx, |picker, cx| { + // let delegate = picker.delegate_mut(); + // match delegate.mode { + // Mode::ManageMembers => match delegate.selected_column { + // Some(UserColumn::Remove) => { + // delegate.selected_column = Some(UserColumn::ToggleAdmin) + // } + // Some(UserColumn::ToggleAdmin) => { + // delegate.selected_column = Some(UserColumn::Remove) + // } + // None => todo!(), + // }, + // Mode::InviteMembers => {} + // } + // cx.notify() + // }); + // } } impl Entity for ChannelModal { @@ -209,8 +211,11 @@ impl View for ChannelModal { .into_any() } - fn focus_in(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { self.has_focus = true; + if cx.is_self_focused() { + cx.focus(&self.picker) + } } fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { From 90cdbe8bf37c3683856ca17efa82a4308e0bec28 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 13:39:05 -0700 Subject: [PATCH 102/326] Fix modal click throughs and adjust height for channel modal --- .../src/collab_panel/channel_modal.rs | 2 +- crates/workspace/src/workspace.rs | 19 ++++++++++++++----- styles/src/style_tree/channel_modal.ts | 3 ++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index f1775eb084..0671eee8af 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -205,7 +205,7 @@ impl View for ChannelModal { ])) .with_child(ChildView::new(&self.picker, cx)) .constrained() - .with_height(theme.height) + .with_max_height(theme.height) .contained() .with_style(theme.container) .into_any() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cec9904eac..e01f81c29e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3755,11 +3755,20 @@ impl View for Workspace { ) })) .with_children(self.modal.as_ref().map(|modal| { - ChildView::new(modal.view.as_any(), cx) - .contained() - .with_style(theme.workspace.modal) - .aligned() - .top() + enum ModalBackground {} + MouseEventHandler::::new( + 0, + cx, + |_, cx| { + ChildView::new(modal.view.as_any(), cx) + .contained() + .with_style(theme.workspace.modal) + .aligned() + .top() + }, + ) + .on_click(MouseButton::Left, |_, _, _| {}) + // Consume click events to stop focus dropping through })) .with_children(self.render_notifications(&theme.workspace, cx)), )) diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index a097bc561f..8dc9e79967 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -29,7 +29,7 @@ export default function channel_modal(): any { selection: theme.players[0], border: border(theme.middle), padding: { - bottom: 4, + bottom: 8, left: 8, right: 8, top: 4, @@ -37,6 +37,7 @@ export default function channel_modal(): any { margin: { left: side_margin, right: side_margin, + bottom: 8, }, } From 9913067e51933287ca0c74bce33d916fdf3b5113 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 14:32:09 -0700 Subject: [PATCH 103/326] Remove admin and member button Fix bug with invites in the member list Fix bug when there are network errors in the member related RPC calls co-authored-by: Max --- crates/client/src/channel_store.rs | 21 +++- .../src/collab_panel/channel_modal.rs | 119 +++++++++--------- crates/theme/src/theme.rs | 3 +- styles/src/style_tree/channel_modal.ts | 15 +-- 4 files changed, 76 insertions(+), 82 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 13510a1e1c..317fbd1189 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -127,17 +127,21 @@ impl ChannelStore { cx.notify(); let client = self.client.clone(); cx.spawn(|this, mut cx| async move { - client + let result = client .request(proto::InviteChannelMember { channel_id, user_id, admin, }) - .await?; + .await; + this.update(&mut cx, |this, cx| { this.outgoing_invites.remove(&(channel_id, user_id)); cx.notify(); }); + + result?; + Ok(()) }) } @@ -155,16 +159,18 @@ impl ChannelStore { cx.notify(); let client = self.client.clone(); cx.spawn(|this, mut cx| async move { - client + let result = client .request(proto::RemoveChannelMember { channel_id, user_id, }) - .await?; + .await; + this.update(&mut cx, |this, cx| { this.outgoing_invites.remove(&(channel_id, user_id)); cx.notify(); }); + result?; Ok(()) }) } @@ -183,17 +189,20 @@ impl ChannelStore { cx.notify(); let client = self.client.clone(); cx.spawn(|this, mut cx| async move { - client + let result = client .request(proto::SetChannelMemberAdmin { channel_id, user_id, admin, }) - .await?; + .await; + this.update(&mut cx, |this, cx| { this.outgoing_invites.remove(&(channel_id, user_id)); cx.notify(); }); + + result?; Ok(()) }) } diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 0671eee8af..fc1b86354f 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -45,15 +45,7 @@ impl ChannelModal { user_store: user_store.clone(), channel_store: channel_store.clone(), channel_id, - match_candidates: members - .iter() - .enumerate() - .map(|(id, member)| StringMatchCandidate { - id, - string: member.user.github_login.clone(), - char_bag: member.user.github_login.chars().collect(), - }) - .collect(), + match_candidates: Vec::new(), members, mode, selected_column: None, @@ -256,7 +248,7 @@ pub struct ChannelModalDelegate { selected_index: usize, mode: Mode, selected_column: Option, - match_candidates: Arc<[StringMatchCandidate]>, + match_candidates: Vec, members: Vec, } @@ -287,30 +279,36 @@ impl PickerDelegate for ChannelModalDelegate { fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { match self.mode { Mode::ManageMembers => { - let match_candidates = self.match_candidates.clone(); + self.match_candidates.clear(); + self.match_candidates + .extend(self.members.iter().enumerate().map(|(id, member)| { + StringMatchCandidate { + id, + string: member.user.github_login.clone(), + char_bag: member.user.github_login.chars().collect(), + } + })); + + let matches = cx.background().block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + cx.background().clone(), + )); + cx.spawn(|picker, mut cx| async move { - async move { - let matches = match_strings( - &match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - cx.background().clone(), - ) - .await; - picker.update(&mut cx, |picker, cx| { + picker + .update(&mut cx, |picker, cx| { let delegate = picker.delegate_mut(); delegate.matching_member_indices.clear(); delegate .matching_member_indices .extend(matches.into_iter().map(|m| m.candidate_id)); cx.notify(); - })?; - anyhow::Ok(()) - } - .log_err() - .await; + }) + .ok(); }) } Mode::InviteMembers => { @@ -346,11 +344,7 @@ impl PickerDelegate for ChannelModalDelegate { } } Some(proto::channel_member::Kind::AncestorMember) | None => { - self.channel_store - .update(cx, |store, cx| { - store.invite_member(self.channel_id, selected_user.id, false, cx) - }) - .detach(); + self.invite_member(selected_user, cx) } } } @@ -386,41 +380,24 @@ impl PickerDelegate for ChannelModalDelegate { .aligned() .left(), ) - .with_children(admin.map(|admin| { - let member_style = theme.admin_toggle_part.in_state(!admin); - let admin_style = theme.admin_toggle_part.in_state(admin); - Flex::row() - .with_child( - Label::new("member", member_style.text.clone()) - .contained() - .with_style(member_style.container), - ) - .with_child( - Label::new("admin", admin_style.text.clone()) - .contained() - .with_style(admin_style.container), - ) + .with_children(admin.map(|_| { + Label::new("admin", theme.admin_toggle.text.clone()) .contained() - .with_style(theme.admin_toggle) + .with_style(theme.admin_toggle.container) .aligned() - .flex_float() })) .with_children({ match self.mode { Mode::ManageMembers => match request_status { - Some(proto::channel_member::Kind::Member) => Some( - Label::new("remove member", theme.remove_member_button.text.clone()) - .contained() - .with_style(theme.remove_member_button.container) - .into_any(), - ), Some(proto::channel_member::Kind::Invitee) => Some( Label::new("cancel invite", theme.cancel_invite_button.text.clone()) .contained() .with_style(theme.cancel_invite_button.container) .into_any(), ), - Some(proto::channel_member::Kind::AncestorMember) | None => None, + Some(proto::channel_member::Kind::Member) + | Some(proto::channel_member::Kind::AncestorMember) + | None => None, }, Mode::InviteMembers => { let svg = match request_status { @@ -466,11 +443,12 @@ impl ChannelModalDelegate { self.members .iter() .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind)) - .or(self - .channel_store - .read(cx) - .has_pending_channel_invite(self.channel_id, user_id) - .then_some(proto::channel_member::Kind::Invitee)) + .or_else(|| { + self.channel_store + .read(cx) + .has_pending_channel_invite(self.channel_id, user_id) + .then_some(proto::channel_member::Kind::Invitee) + }) } fn user_at_index(&self, ix: usize) -> Option<(Arc, Option)> { @@ -517,4 +495,25 @@ impl ChannelModalDelegate { }) .detach_and_log_err(cx); } + + 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) + }); + + cx.spawn(|this, mut cx| async move { + invite_member.await?; + + this.update(&mut cx, |this, cx| { + let delegate_mut = this.delegate_mut(); + delegate_mut.members.push(ChannelMembership { + user, + kind: proto::channel_member::Kind::Invitee, + admin: false, + }); + cx.notify(); + }) + }) + .detach_and_log_err(cx); + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8bd673d1b3..c778b5fc88 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -261,8 +261,7 @@ pub struct ChannelModal { pub cancel_invite_button: ContainedText, pub member_icon: Icon, pub invitee_icon: Icon, - pub admin_toggle: ContainerStyle, - pub admin_toggle_part: Toggleable, + pub admin_toggle: ContainedText, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 8dc9e79967..40fd497458 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -76,21 +76,8 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", { size: "xs" }), background: background(theme.middle), }, - admin_toggle_part: toggleable({ - base: { - ...text(theme.middle, "sans", { size: "xs" }), - padding: { - left: 7, - right: 7, - }, - }, - state: { - active: { - background: background(theme.middle, "on"), - } - } - }), admin_toggle: { + ...text(theme.middle, "sans", { size: "xs" }), border: border(theme.middle, "active"), background: background(theme.middle), margin: { From e37e76fc0bdb1f690162d6f055d48c4585f7f9b5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 15:29:30 -0700 Subject: [PATCH 104/326] Add context menu controls to the channel member management co-authored-by: Max --- .../src/collab_panel/channel_modal.rs | 244 +++++++++++------- crates/theme/src/theme.rs | 2 +- styles/src/style_tree/channel_modal.ts | 8 +- 3 files changed, 161 insertions(+), 93 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index fc1b86354f..8747d9a0af 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,5 @@ use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; +use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, @@ -11,12 +12,21 @@ use std::sync::Arc; use util::TryFutureExt; use workspace::Modal; -actions!(channel_modal, [SelectNextControl, ToggleMode]); +actions!( + channel_modal, + [ + SelectNextControl, + ToggleMode, + ToggleMemberAdmin, + RemoveMember + ] +); pub fn init(cx: &mut AppContext) { Picker::::init(cx); cx.add_action(ChannelModal::toggle_mode); - // cx.add_action(ChannelModal::select_next_control); + cx.add_action(ChannelModal::toggle_member_admin); + cx.add_action(ChannelModal::remove_member); } pub struct ChannelModal { @@ -48,7 +58,11 @@ impl ChannelModal { match_candidates: Vec::new(), members, mode, - selected_column: None, + context_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx.view_id(), cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), }, cx, ) @@ -95,6 +109,8 @@ impl ChannelModal { this.picker.update(cx, |picker, cx| { let delegate = picker.delegate_mut(); delegate.mode = mode; + delegate.selected_index = 0; + picker.set_query("", cx); picker.update_matches(picker.query(cx), cx); cx.notify() }); @@ -104,24 +120,17 @@ impl ChannelModal { .detach(); } - // fn select_next_control(&mut self, _: &SelectNextControl, cx: &mut ViewContext) { - // self.picker.update(cx, |picker, cx| { - // let delegate = picker.delegate_mut(); - // match delegate.mode { - // Mode::ManageMembers => match delegate.selected_column { - // Some(UserColumn::Remove) => { - // delegate.selected_column = Some(UserColumn::ToggleAdmin) - // } - // Some(UserColumn::ToggleAdmin) => { - // delegate.selected_column = Some(UserColumn::Remove) - // } - // None => todo!(), - // }, - // Mode::InviteMembers => {} - // } - // cx.notify() - // }); - // } + fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + picker.delegate_mut().toggle_selected_member_admin(cx); + }) + } + + fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + picker.delegate_mut().remove_selected_member(cx); + }); + } } impl Entity for ChannelModal { @@ -233,12 +242,6 @@ pub enum Mode { InviteMembers, } -#[derive(Copy, Clone, PartialEq)] -pub enum UserColumn { - ToggleAdmin, - Remove, -} - pub struct ChannelModalDelegate { matching_users: Vec>, matching_member_indices: Vec, @@ -247,9 +250,9 @@ pub struct ChannelModalDelegate { channel_id: ChannelId, selected_index: usize, mode: Mode, - selected_column: Option, match_candidates: Vec, members: Vec, + context_menu: ViewHandle, } impl PickerDelegate for ChannelModalDelegate { @@ -270,10 +273,6 @@ impl PickerDelegate for ChannelModalDelegate { fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { self.selected_index = ix; - self.selected_column = match self.mode { - Mode::ManageMembers => Some(UserColumn::ToggleAdmin), - Mode::InviteMembers => None, - }; } fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { @@ -334,18 +333,17 @@ 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) { - match self.member_status(selected_user.id, cx) { - Some(proto::channel_member::Kind::Member) - | Some(proto::channel_member::Kind::Invitee) => { - if self.selected_column == Some(UserColumn::ToggleAdmin) { - self.set_member_admin(selected_user.id, !admin.unwrap_or(false), cx); - } else { + match self.mode { + Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx), + Mode::InviteMembers => match self.member_status(selected_user.id, cx) { + Some(proto::channel_member::Kind::Invitee) => { self.remove_member(selected_user.id, cx); } - } - Some(proto::channel_member::Kind::AncestorMember) | None => { - self.invite_member(selected_user, cx) - } + Some(proto::channel_member::Kind::AncestorMember) | None => { + self.invite_member(selected_user, cx) + } + Some(proto::channel_member::Kind::Member) => {} + }, } } } @@ -366,7 +364,10 @@ impl PickerDelegate for ChannelModalDelegate { let request_status = self.member_status(user.id, cx); let style = theme.picker.item.in_state(selected).style_for(mouse_state); - Flex::row() + + let in_manage = matches!(self.mode, Mode::ManageMembers); + + let mut result = Flex::row() .with_children(user.avatar.clone().map(|avatar| { Image::from_data(avatar) .with_style(theme.contact_avatar) @@ -380,57 +381,81 @@ impl PickerDelegate for ChannelModalDelegate { .aligned() .left(), ) - .with_children(admin.map(|_| { - Label::new("admin", theme.admin_toggle.text.clone()) - .contained() - .with_style(theme.admin_toggle.container) - .aligned() + .with_children({ + (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then( + || { + Label::new("Invited", theme.member_tag.text.clone()) + .contained() + .with_style(theme.member_tag.container) + .aligned() + .left() + }, + ) + }) + .with_children(admin.and_then(|admin| { + (in_manage && admin).then(|| { + Label::new("Admin", theme.member_tag.text.clone()) + .contained() + .with_style(theme.member_tag.container) + .aligned() + .left() + }) })) .with_children({ - match self.mode { - Mode::ManageMembers => match request_status { - Some(proto::channel_member::Kind::Invitee) => Some( - Label::new("cancel invite", theme.cancel_invite_button.text.clone()) + let svg = match self.mode { + Mode::ManageMembers => Some( + Svg::new("icons/ellipsis_14.svg") + .with_color(theme.member_icon.color) + .constrained() + .with_width(theme.member_icon.width) + .aligned() + .contained() + .with_style(theme.member_icon.container), + ), + Mode::InviteMembers => match request_status { + Some(proto::channel_member::Kind::Member) => Some( + Svg::new("icons/check_8.svg") + .with_color(theme.member_icon.color) + .constrained() + .with_width(theme.member_icon.width) + .aligned() .contained() - .with_style(theme.cancel_invite_button.container) - .into_any(), + .with_style(theme.member_icon.container), ), - Some(proto::channel_member::Kind::Member) - | Some(proto::channel_member::Kind::AncestorMember) - | None => None, + Some(proto::channel_member::Kind::Invitee) => Some( + Svg::new("icons/check_8.svg") + .with_color(theme.invitee_icon.color) + .constrained() + .with_width(theme.invitee_icon.width) + .aligned() + .contained() + .with_style(theme.invitee_icon.container), + ), + Some(proto::channel_member::Kind::AncestorMember) | None => None, }, - Mode::InviteMembers => { - let svg = match request_status { - Some(proto::channel_member::Kind::Member) => Some( - Svg::new("icons/check_8.svg") - .with_color(theme.member_icon.color) - .constrained() - .with_width(theme.member_icon.width) - .aligned() - .contained() - .with_style(theme.member_icon.container), - ), - Some(proto::channel_member::Kind::Invitee) => Some( - Svg::new("icons/check_8.svg") - .with_color(theme.invitee_icon.color) - .constrained() - .with_width(theme.invitee_icon.width) - .aligned() - .contained() - .with_style(theme.invitee_icon.container), - ), - Some(proto::channel_member::Kind::AncestorMember) | None => None, - }; + }; - svg.map(|svg| svg.aligned().flex_float().into_any()) - } - } + svg.map(|svg| svg.aligned().flex_float().into_any()) }) .contained() .with_style(style.container) .constrained() .with_height(theme.row_height) - .into_any() + .into_any(); + + if selected { + result = Stack::new() + .with_child(result) + .with_child( + ChildView::new(&self.context_menu, cx) + .aligned() + .top() + .right(), + ) + .into_any(); + } + + result } } @@ -464,20 +489,30 @@ impl ChannelModalDelegate { } } - fn set_member_admin(&mut self, user_id: u64, admin: bool, cx: &mut ViewContext>) { + 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 update = self.channel_store.update(cx, |store, cx| { - store.set_member_admin(self.channel_id, user_id, admin, cx) + store.set_member_admin(self.channel_id, user.id, admin, cx) }); cx.spawn(|picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, _| { + 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) { + if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) { member.admin = admin; } + cx.notify(); }) }) .detach_and_log_err(cx); + Some(()) + } + + fn remove_selected_member(&mut self, cx: &mut ViewContext>) -> Option<()> { + let (user, _) = self.user_at_index(self.selected_index)?; + self.remove_member(user.id, cx); + Some(()) } fn remove_member(&mut self, user_id: u64, cx: &mut ViewContext>) { @@ -486,11 +521,20 @@ impl ChannelModalDelegate { }); cx.spawn(|picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, _| { + picker.update(&mut cx, |picker, cx| { let this = picker.delegate_mut(); if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) { this.members.remove(ix); + this.matching_member_indices.retain_mut(|member_ix| { + if *member_ix == ix { + return false; + } else if *member_ix > ix { + *member_ix -= 1; + } + true + }) } + cx.notify(); }) }) .detach_and_log_err(cx); @@ -505,8 +549,7 @@ impl ChannelModalDelegate { invite_member.await?; this.update(&mut cx, |this, cx| { - let delegate_mut = this.delegate_mut(); - delegate_mut.members.push(ChannelMembership { + this.delegate_mut().members.push(ChannelMembership { user, kind: proto::channel_member::Kind::Invitee, admin: false, @@ -516,4 +559,25 @@ impl ChannelModalDelegate { }) .detach_and_log_err(cx); } + + fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext>) { + self.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + Default::default(), + AnchorCorner::TopRight, + vec![ + ContextMenuItem::action("Remove", RemoveMember), + ContextMenuItem::action( + if user_is_admin { + "Make non-admin" + } else { + "Make admin" + }, + ToggleMemberAdmin, + ), + ], + cx, + ) + }) + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c778b5fc88..b3640f538f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -261,7 +261,7 @@ pub struct ChannelModal { pub cancel_invite_button: ContainedText, pub member_icon: Icon, pub invitee_icon: Icon, - pub admin_toggle: ContainedText, + pub member_tag: ContainedText, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 40fd497458..447522070b 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -76,12 +76,16 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", { size: "xs" }), background: background(theme.middle), }, - admin_toggle: { + member_tag: { ...text(theme.middle, "sans", { size: "xs" }), border: border(theme.middle, "active"), background: background(theme.middle), margin: { - right: 8, + left: 8, + }, + padding: { + left: 4, + right: 4, } }, container: { From 8980a9f1c1b33a8661fb8c48da3e6a8418176c0d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 16:27:47 -0700 Subject: [PATCH 105/326] Add settings for removing the assistant and collaboration panel buttons Add a not-logged-in state to the collaboration panel co-authored-by: max --- assets/settings/default.json | 10 +- crates/ai/src/assistant.rs | 7 +- crates/ai/src/assistant_settings.rs | 2 + crates/collab_ui/src/collab_panel.rs | 54 ++++-- .../src/collab_panel/panel_settings.rs | 18 +- crates/project_panel/src/project_panel.rs | 4 +- crates/terminal_view/src/terminal_panel.rs | 4 +- crates/theme/src/theme.rs | 1 + crates/workspace/src/dock.rs | 179 +++++++++--------- styles/src/style_tree/collab_panel.ts | 31 +++ 10 files changed, 195 insertions(+), 115 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c40ed4e8da..08faedbed6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -122,13 +122,17 @@ // Amount of indentation for nested items. "indent_size": 20 }, - "channels_panel": { + "collaboration_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": "left", // Default width of the channels panel. "default_width": 240 }, "assistant": { + // Whether to show the assistant panel button in the status bar. + "button": true, // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. "dock": "right", // Default width when the assistant is docked to the left or right. @@ -220,7 +224,9 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [".env"] + "disabled_globs": [ + ".env" + ] }, // Settings specific to journaling "journal": { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 957c5e1c06..35d3c9f7ef 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -192,6 +192,7 @@ impl AssistantPanel { old_dock_position = new_dock_position; cx.emit(AssistantPanelEvent::DockPositionChanged); } + cx.notify(); })]; this @@ -790,8 +791,10 @@ impl Panel for AssistantPanel { } } - fn icon_path(&self) -> &'static str { - "icons/robot_14.svg" + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { + settings::get::(cx) + .button + .then(|| "icons/robot_14.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs index eb92e0f6e8..04ba8fb946 100644 --- a/crates/ai/src/assistant_settings.rs +++ b/crates/ai/src/assistant_settings.rs @@ -13,6 +13,7 @@ pub enum AssistantDockPosition { #[derive(Deserialize, Debug)] pub struct AssistantSettings { + pub button: bool, pub dock: AssistantDockPosition, pub default_width: f32, pub default_height: f32, @@ -20,6 +21,7 @@ pub struct AssistantSettings { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct AssistantSettingsContent { + pub button: Option, pub dock: Option, pub default_width: Option, pub default_height: Option, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 382381dba1..d8e2682316 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -27,7 +27,7 @@ use gpui::{ Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; -use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}; +use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings}; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; @@ -65,7 +65,7 @@ impl_actions!(collab_panel, [RemoveChannel, NewChannel, AddMember]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { - settings::register::(cx); + settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); @@ -95,6 +95,7 @@ pub struct CollabPanel { entries: Vec, selection: Option, user_store: ModelHandle, + client: Arc, channel_store: ModelHandle, project: ModelHandle, match_candidates: Vec, @@ -320,6 +321,7 @@ impl CollabPanel { match_candidates: Vec::default(), collapsed_sections: Vec::default(), workspace: workspace.weak_handle(), + client: workspace.app_state().client.clone(), list_state, }; this.update_entries(cx); @@ -334,6 +336,7 @@ impl CollabPanel { old_dock_position = new_dock_position; cx.emit(Event::DockPositionChanged); } + cx.notify(); }), ); @@ -1862,6 +1865,31 @@ impl View for CollabPanel { fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let theme = &theme::current(cx).collab_panel; + if self.user_store.read(cx).current_user().is_none() { + enum LogInButton {} + + return Flex::column() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let button = theme.log_in_button.style_for(state); + Label::new("Sign in to collaborate", button.text.clone()) + .contained() + .with_style(button.container) + }) + .on_click(MouseButton::Left, |_, this, cx| { + let client = this.client.clone(); + cx.spawn(|_, cx| async move { + client.authenticate_and_connect(true, &cx).await.log_err() + }) + .detach(); + }) + .with_cursor_style(CursorStyle::PointingHand), + ) + .contained() + .with_style(theme.container) + .into_any(); + } + enum PanelFocus {} MouseEventHandler::::new(0, cx, |_, cx| { Stack::new() @@ -1901,9 +1929,9 @@ impl View for CollabPanel { impl Panel for CollabPanel { fn position(&self, cx: &gpui::WindowContext) -> DockPosition { - match settings::get::(cx).dock { - ChannelsPanelDockPosition::Left => DockPosition::Left, - ChannelsPanelDockPosition::Right => DockPosition::Right, + match settings::get::(cx).dock { + CollaborationPanelDockPosition::Left => DockPosition::Left, + CollaborationPanelDockPosition::Right => DockPosition::Right, } } @@ -1912,13 +1940,15 @@ impl Panel for CollabPanel { } fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::( + settings::update_settings_file::( self.fs.clone(), cx, move |settings| { let dock = match position { - DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left, - DockPosition::Right => ChannelsPanelDockPosition::Right, + DockPosition::Left | DockPosition::Bottom => { + CollaborationPanelDockPosition::Left + } + DockPosition::Right => CollaborationPanelDockPosition::Right, }; settings.dock = Some(dock); }, @@ -1927,7 +1957,7 @@ impl Panel for CollabPanel { fn size(&self, cx: &gpui::WindowContext) -> f32 { self.width - .unwrap_or_else(|| settings::get::(cx).default_width) + .unwrap_or_else(|| settings::get::(cx).default_width) } fn set_size(&mut self, size: f32, cx: &mut ViewContext) { @@ -1936,8 +1966,10 @@ impl Panel for CollabPanel { cx.notify(); } - fn icon_path(&self) -> &'static str { - "icons/radix/person.svg" + fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { + settings::get::(cx) + .button + .then(|| "icons/radix/person.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/collab_ui/src/collab_panel/panel_settings.rs b/crates/collab_ui/src/collab_panel/panel_settings.rs index fe3484b782..5e2954b915 100644 --- a/crates/collab_ui/src/collab_panel/panel_settings.rs +++ b/crates/collab_ui/src/collab_panel/panel_settings.rs @@ -5,27 +5,29 @@ use settings::Setting; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum ChannelsPanelDockPosition { +pub enum CollaborationPanelDockPosition { Left, Right, } #[derive(Deserialize, Debug)] -pub struct ChannelsPanelSettings { - pub dock: ChannelsPanelDockPosition, +pub struct CollaborationPanelSettings { + pub button: bool, + pub dock: CollaborationPanelDockPosition, pub default_width: f32, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -pub struct ChannelsPanelSettingsContent { - pub dock: Option, +pub struct CollaborationPanelSettingsContent { + pub button: Option, + pub dock: Option, pub default_width: Option, } -impl Setting for ChannelsPanelSettings { - const KEY: Option<&'static str> = Some("channels_panel"); +impl Setting for CollaborationPanelSettings { + const KEY: Option<&'static str> = Some("collaboration_panel"); - type FileContent = ChannelsPanelSettingsContent; + type FileContent = CollaborationPanelSettingsContent; fn load( default_value: &Self::FileContent, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0383117de8..4d84a1c638 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1657,8 +1657,8 @@ impl workspace::dock::Panel for ProjectPanel { cx.notify(); } - fn icon_path(&self) -> &'static str { - "icons/folder_tree_16.svg" + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/folder_tree_16.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6ad321c735..34752ad3c4 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -396,8 +396,8 @@ impl Panel for TerminalPanel { } } - fn icon_path(&self) -> &'static str { - "icons/terminal_12.svg" + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/terminal_12.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b3640f538f..c554f77fe4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,6 +220,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub log_in_button: Interactive, pub channel_hash: Icon, pub channel_modal: ChannelModal, pub user_query_editor: FieldEditor, diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 6c88e5032c..e447a43d55 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -14,7 +14,7 @@ pub trait Panel: View { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); fn size(&self, cx: &WindowContext) -> f32; fn set_size(&mut self, size: f32, cx: &mut ViewContext); - fn icon_path(&self) -> &'static str; + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; fn icon_tooltip(&self) -> (String, Option>); fn icon_label(&self, _: &WindowContext) -> Option { None @@ -51,7 +51,7 @@ pub trait PanelHandle { fn set_active(&self, active: bool, cx: &mut WindowContext); fn size(&self, cx: &WindowContext) -> f32; fn set_size(&self, size: f32, cx: &mut WindowContext); - fn icon_path(&self, cx: &WindowContext) -> &'static str; + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); fn icon_label(&self, cx: &WindowContext) -> Option; fn has_focus(&self, cx: &WindowContext) -> bool; @@ -98,8 +98,8 @@ where self.update(cx, |this, cx| this.set_active(active, cx)) } - fn icon_path(&self, cx: &WindowContext) -> &'static str { - self.read(cx).icon_path() + fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { + self.read(cx).icon_path(cx) } fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>) { @@ -490,8 +490,9 @@ impl View for PanelButtons { .map(|item| (item.panel.clone(), item.context_menu.clone())) .collect::>(); Flex::row() - .with_children(panels.into_iter().enumerate().map( + .with_children(panels.into_iter().enumerate().filter_map( |(panel_ix, (view, context_menu))| { + let icon_path = view.icon_path(cx)?; let is_active = is_open && panel_ix == active_ix; let (tooltip, tooltip_action) = if is_active { ( @@ -505,94 +506,96 @@ impl View for PanelButtons { } else { view.icon_tooltip(cx) }; - Stack::new() - .with_child( - MouseEventHandler::::new(panel_ix, cx, |state, cx| { - let style = button_style.in_state(is_active); + Some( + Stack::new() + .with_child( + MouseEventHandler::::new(panel_ix, cx, |state, cx| { + let style = button_style.in_state(is_active); - let style = style.style_for(state); - Flex::row() - .with_child( - Svg::new(view.icon_path(cx)) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .aligned(), - ) - .with_children(if let Some(label) = view.icon_label(cx) { - Some( - Label::new(label, style.label.text.clone()) - .contained() - .with_style(style.label.container) + let style = style.style_for(state); + Flex::row() + .with_child( + Svg::new(icon_path) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) .aligned(), ) - } else { - None - }) - .constrained() - .with_height(style.icon_size) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, { - let tooltip_action = - tooltip_action.as_ref().map(|action| action.boxed_clone()); - move |_, this, cx| { - if let Some(tooltip_action) = &tooltip_action { - let window_id = cx.window_id(); - let view_id = this.workspace.id(); - let tooltip_action = tooltip_action.boxed_clone(); - cx.spawn(|_, mut cx| async move { - cx.dispatch_action( - window_id, - view_id, - &*tooltip_action, + .with_children(if let Some(label) = view.icon_label(cx) { + Some( + Label::new(label, style.label.text.clone()) + .contained() + .with_style(style.label.container) + .aligned(), ) - .ok(); + } else { + None }) - .detach(); - } - } - }) - .on_click(MouseButton::Right, { - let view = view.clone(); - let menu = context_menu.clone(); - move |_, _, cx| { - const POSITIONS: [DockPosition; 3] = [ - DockPosition::Left, - DockPosition::Right, - DockPosition::Bottom, - ]; - - menu.update(cx, |menu, cx| { - let items = POSITIONS - .into_iter() - .filter(|position| { - *position != dock_position - && view.position_is_valid(*position, cx) - }) - .map(|position| { - let view = view.clone(); - ContextMenuItem::handler( - format!("Dock {}", position.to_label()), - move |cx| view.set_position(position, cx), + .constrained() + .with_height(style.icon_size) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let tooltip_action = + tooltip_action.as_ref().map(|action| action.boxed_clone()); + move |_, this, cx| { + if let Some(tooltip_action) = &tooltip_action { + let window_id = cx.window_id(); + let view_id = this.workspace.id(); + let tooltip_action = tooltip_action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + cx.dispatch_action( + window_id, + view_id, + &*tooltip_action, ) + .ok(); }) - .collect(); - menu.show(Default::default(), menu_corner, items, cx); - }) - } - }) - .with_tooltip::( - panel_ix, - tooltip, - tooltip_action, - tooltip_style.clone(), - cx, - ), - ) - .with_child(ChildView::new(&context_menu, cx)) + .detach(); + } + } + }) + .on_click(MouseButton::Right, { + let view = view.clone(); + let menu = context_menu.clone(); + move |_, _, cx| { + const POSITIONS: [DockPosition; 3] = [ + DockPosition::Left, + DockPosition::Right, + DockPosition::Bottom, + ]; + + menu.update(cx, |menu, cx| { + let items = POSITIONS + .into_iter() + .filter(|position| { + *position != dock_position + && view.position_is_valid(*position, cx) + }) + .map(|position| { + let view = view.clone(); + ContextMenuItem::handler( + format!("Dock {}", position.to_label()), + move |cx| view.set_position(position, cx), + ) + }) + .collect(); + menu.show(Default::default(), menu_corner, items, cx); + }) + } + }) + .with_tooltip::( + panel_ix, + tooltip, + tooltip_action, + tooltip_style.clone(), + cx, + ), + ) + .with_child(ChildView::new(&context_menu, cx)), + ) }, )) .contained() @@ -702,8 +705,8 @@ pub mod test { self.size = size; } - fn icon_path(&self) -> &'static str { - "icons/test_panel.svg" + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/test_panel.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index f24468dca6..2c543356b0 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -67,6 +67,37 @@ export default function contacts_panel(): any { return { channel_modal: channel_modal(), + log_in_button: interactive({ + base: { + background: background(theme.middle), + border: border(theme.middle, "active"), + corner_radius: 4, + margin: { + top: 16, + left: 16, + right: 16, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(theme.middle, "sans", "default", { size: "sm" }), + }, + state: { + hovered: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "hovered"), + border: border(theme.middle, "active"), + }, + clicked: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "pressed"), + border: border(theme.middle, "active"), + }, + }, + }), background: background(layer), padding: { top: 12, From bedf60b6b28d7aeae35ba4583ee8420911a77134 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 16:45:13 -0700 Subject: [PATCH 106/326] Improve local collaboration script to accept a zed impersonate Gate channels UI behind a flag co-authored-by: max --- Cargo.lock | 1 + crates/collab_ui/Cargo.toml | 2 +- crates/collab_ui/src/collab_panel.rs | 149 ++++++++++++++------------- script/start-local-collaboration | 2 +- 4 files changed, 80 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cbd7c8d304..26f71da741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1568,6 +1568,7 @@ dependencies = [ "serde", "serde_derive", "settings", + "staff_mode", "theme", "theme_selector", "util", diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 2ceac649ec..471608c43e 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -38,6 +38,7 @@ picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } +staff_mode = {path = "../staff_mode"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } vcs_menu = { path = "../vcs_menu" } @@ -45,7 +46,6 @@ util = { path = "../util" } workspace = { path = "../workspace" } zed-actions = {path = "../zed-actions"} - anyhow.workspace = true futures.workspace = true log.workspace = true diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d8e2682316..13d14f51ac 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -31,6 +31,7 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings} use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; +use staff_mode::StaffMode; use std::{mem, sync::Arc}; use theme::IconButton; use util::{ResultExt, TryFutureExt}; @@ -347,6 +348,8 @@ impl CollabPanel { .push(cx.observe(&this.channel_store, |this, _, cx| this.update_entries(cx))); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); + this.subscriptions + .push(cx.observe_global::(move |this, cx| this.update_entries(cx))); this }) @@ -516,79 +519,76 @@ impl CollabPanel { } } - self.entries.push(ListEntry::Header(Section::Channels, 0)); + let mut request_entries = Vec::new(); + if self.include_channels_section(cx) { + self.entries.push(ListEntry::Header(Section::Channels, 0)); - let channels = channel_store.channels(); - if !(channels.is_empty() && self.channel_editing_state.is_none()) { - self.match_candidates.clear(); - self.match_candidates - .extend( - channels - .iter() - .enumerate() - .map(|(ix, channel)| StringMatchCandidate { + let channels = channel_store.channels(); + if !(channels.is_empty() && self.channel_editing_state.is_none()) { + self.match_candidates.clear(); + self.match_candidates + .extend(channels.iter().enumerate().map(|(ix, channel)| { + StringMatchCandidate { id: ix, string: channel.name.clone(), char_bag: channel.name.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - if let Some(state) = &self.channel_editing_state { - if state.parent_id.is_none() { - self.entries.push(ListEntry::ChannelEditor { depth: 0 }); - } - } - for mat in matches { - let channel = &channels[mat.candidate_id]; - self.entries.push(ListEntry::Channel(channel.clone())); + } + })); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); if let Some(state) = &self.channel_editing_state { - if state.parent_id == Some(channel.id) { - self.entries.push(ListEntry::ChannelEditor { - depth: channel.depth + 1, - }); + if state.parent_id.is_none() { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); + } + } + for mat in matches { + let channel = &channels[mat.candidate_id]; + self.entries.push(ListEntry::Channel(channel.clone())); + if let Some(state) = &self.channel_editing_state { + if state.parent_id == Some(channel.id) { + self.entries.push(ListEntry::ChannelEditor { + depth: channel.depth + 1, + }); + } } } } - } - let mut request_entries = Vec::new(); - let channel_invites = channel_store.channel_invitations(); - if !channel_invites.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { - StringMatchCandidate { - id: ix, - string: channel.name.clone(), - char_bag: channel.name.chars().collect(), - } + let channel_invites = channel_store.channel_invitations(); + if !channel_invites.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { + StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + } + })); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend(matches.iter().map(|mat| { + ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) })); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())), - ); - if !request_entries.is_empty() { - self.entries - .push(ListEntry::Header(Section::ChannelInvites, 1)); - if !self.collapsed_sections.contains(&Section::ChannelInvites) { - self.entries.append(&mut request_entries); + if !request_entries.is_empty() { + self.entries + .push(ListEntry::Header(Section::ChannelInvites, 1)); + if !self.collapsed_sections.contains(&Section::ChannelInvites) { + self.entries.append(&mut request_entries); + } } } } @@ -686,16 +686,9 @@ impl CollabPanel { executor.clone(), )); - let (mut online_contacts, offline_contacts) = matches + let (online_contacts, offline_contacts) = matches .iter() .partition::, _>(|mat| contacts[mat.candidate_id].online); - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - online_contacts.retain(|contact| { - let contact = &contacts[contact.candidate_id]; - !room.contains_participant(contact.user.id) - }); - } for (matches, section) in [ (online_contacts, Section::Online), @@ -1534,6 +1527,14 @@ impl CollabPanel { .into_any() } + fn include_channels_section(&self, cx: &AppContext) -> bool { + if cx.has_global::() { + cx.global::().0 + } else { + false + } + } + fn deploy_channel_context_menu( &mut self, position: Vector2F, @@ -1878,8 +1879,12 @@ impl View for CollabPanel { }) .on_click(MouseButton::Left, |_, this, cx| { let client = this.client.clone(); - cx.spawn(|_, cx| async move { - client.authenticate_and_connect(true, &cx).await.log_err() + cx.spawn(|this, mut cx| async move { + client.authenticate_and_connect(true, &cx).await.log_err(); + + this.update(&mut cx, |_, cx| { + cx.notify(); + }) }) .detach(); }) diff --git a/script/start-local-collaboration b/script/start-local-collaboration index b702fb4e02..a5836ff776 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -53,6 +53,6 @@ sleep 0.5 # Start the two Zed child processes. Open the given paths with the first instance. trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT -ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & +ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & wait From fa71de8842c512dfb797b3f87886acbd2c9ba9eb Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 17:14:09 -0700 Subject: [PATCH 107/326] Tune UX for context menus Co-authored-by: max --- crates/client/src/user.rs | 16 ++++++- crates/collab_ui/src/collab_panel.rs | 45 +++++++++++++------ .../src/collab_panel/channel_modal.rs | 16 ++++--- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 4c2721ffeb..be11d1fb44 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -165,17 +165,29 @@ impl UserStore { }); current_user_tx.send(user).await.ok(); + + this.update(&mut cx, |_, cx| { + cx.notify(); + }); } } Status::SignedOut => { current_user_tx.send(None).await.ok(); if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| this.clear_contacts()).await; + this.update(&mut cx, |this, cx| { + cx.notify(); + this.clear_contacts() + }) + .await; } } Status::ConnectionLost => { if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| this.clear_contacts()).await; + this.update(&mut cx, |this, cx| { + cx.notify(); + this.clear_contacts() + }) + .await; } } _ => {} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 13d14f51ac..e457f8c750 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -4,7 +4,7 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{proto::PeerId, Channel, ChannelStore, Client, Contact, User, UserStore}; +use client::{proto::PeerId, Channel, ChannelId, ChannelStore, Client, Contact, User, UserStore}; use contact_finder::build_contact_finder; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; @@ -55,13 +55,21 @@ struct NewChannel { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct AddMember { +struct InviteMembers { + channel_id: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ManageMembers { channel_id: u64, } actions!(collab_panel, [ToggleFocus]); -impl_actions!(collab_panel, [RemoveChannel, NewChannel, AddMember]); +impl_actions!( + collab_panel, + [RemoveChannel, NewChannel, InviteMembers, ManageMembers] +); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -76,7 +84,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::confirm); cx.add_action(CollabPanel::remove_channel); cx.add_action(CollabPanel::new_subchannel); - cx.add_action(CollabPanel::add_member); + cx.add_action(CollabPanel::invite_members); + cx.add_action(CollabPanel::manage_members); } #[derive(Debug, Default)] @@ -325,6 +334,7 @@ impl CollabPanel { client: workspace.app_state().client.clone(), list_state, }; + this.update_entries(cx); // Update the dock position when the setting changes. @@ -1549,7 +1559,8 @@ impl CollabPanel { vec![ ContextMenuItem::action("New Channel", NewChannel { channel_id }), ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), - ContextMenuItem::action("Add member", AddMember { channel_id }), + ContextMenuItem::action("Manage members", ManageMembers { channel_id }), + ContextMenuItem::action("Invite members", InviteMembers { channel_id }), ], cx, ); @@ -1710,8 +1721,20 @@ impl CollabPanel { cx.notify(); } - fn add_member(&mut self, action: &AddMember, cx: &mut ViewContext) { - let channel_id = action.channel_id; + fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext) { + self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); + } + + fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { + self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); + } + + fn show_channel_modal( + &mut self, + channel_id: ChannelId, + mode: channel_modal::Mode, + cx: &mut ViewContext, + ) { let workspace = self.workspace.clone(); let user_store = self.user_store.clone(); let channel_store = self.channel_store.clone(); @@ -1728,7 +1751,7 @@ impl CollabPanel { user_store.clone(), channel_store.clone(), channel_id, - channel_modal::Mode::InviteMembers, + mode, members, cx, ) @@ -1879,12 +1902,8 @@ impl View for CollabPanel { }) .on_click(MouseButton::Left, |_, this, cx| { let client = this.client.clone(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|_, cx| async move { client.authenticate_and_connect(true, &cx).await.log_err(); - - this.update(&mut cx, |_, cx| { - cx.notify(); - }) }) .detach(); }) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 8747d9a0af..7ce830b22f 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -337,7 +337,7 @@ impl PickerDelegate for ChannelModalDelegate { Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx), Mode::InviteMembers => match self.member_status(selected_user.id, cx) { Some(proto::channel_member::Kind::Invitee) => { - self.remove_member(selected_user.id, cx); + self.remove_selected_member(cx); } Some(proto::channel_member::Kind::AncestorMember) | None => { self.invite_member(selected_user, cx) @@ -502,6 +502,7 @@ impl ChannelModalDelegate { if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) { member.admin = admin; } + cx.focus_self(); cx.notify(); }) }) @@ -511,11 +512,7 @@ impl ChannelModalDelegate { fn remove_selected_member(&mut self, cx: &mut ViewContext>) -> Option<()> { let (user, _) = self.user_at_index(self.selected_index)?; - self.remove_member(user.id, cx); - Some(()) - } - - fn remove_member(&mut self, user_id: u64, cx: &mut ViewContext>) { + let user_id = user.id; let update = self.channel_store.update(cx, |store, cx| { store.remove_member(self.channel_id, user_id, cx) }); @@ -534,10 +531,17 @@ impl ChannelModalDelegate { true }) } + + this.selected_index = this + .selected_index + .min(this.matching_member_indices.len() - 1); + + cx.focus_self(); cx.notify(); }) }) .detach_and_log_err(cx); + Some(()) } fn invite_member(&mut self, user: Arc, cx: &mut ViewContext>) { From 299906346e0268deca783b415ea5d35c7bc0b0a0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 7 Aug 2023 18:04:41 -0700 Subject: [PATCH 108/326] Change collab panel icon --- crates/collab_ui/src/collab_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index e457f8c750..f745420eeb 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1088,7 +1088,7 @@ impl CollabPanel { MouseEventHandler::::new(0, cx, |_, _| { render_icon_button( theme.collab_panel.add_contact_button.in_state(is_selected), - "icons/user_plus_16.svg", + "icons/plus_16.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) @@ -1993,7 +1993,7 @@ impl Panel for CollabPanel { fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { settings::get::(cx) .button - .then(|| "icons/radix/person.svg") + .then(|| "icons/speech_bubble_12.svg") } fn icon_tooltip(&self) -> (String, Option>) { From 95891d28d8ab51c8f7da4b33da104e869654e81c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:14:39 +0200 Subject: [PATCH 109/326] Move SearchMode and SearchHistory to separate modules --- crates/search/src/buffer_search.rs | 5 +- crates/search/src/history.rs | 184 ++++++++++++++++++++++++++ crates/search/src/mode.rs | 73 +++++++++++ crates/search/src/project_search.rs | 86 +----------- crates/search/src/search.rs | 194 +--------------------------- 5 files changed, 269 insertions(+), 273 deletions(-) create mode 100644 crates/search/src/history.rs create mode 100644 crates/search/src/mode.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index baba45e0f6..52dd943625 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ - NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, + history::SearchHistory, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, + SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -50,7 +50,6 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::previous_history_query); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); - add_toggle_option_action::(SearchOptions::REGEX, cx); } fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { diff --git a/crates/search/src/history.rs b/crates/search/src/history.rs new file mode 100644 index 0000000000..6b06c60293 --- /dev/null +++ b/crates/search/src/history.rs @@ -0,0 +1,184 @@ +use smallvec::SmallVec; +const SEARCH_HISTORY_LIMIT: usize = 20; + +#[derive(Default, Debug, Clone)] +pub struct SearchHistory { + history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, + selected: Option, +} + +impl SearchHistory { + pub fn add(&mut self, search_string: String) { + if let Some(i) = self.selected { + if search_string == self.history[i] { + return; + } + } + + if let Some(previously_searched) = self.history.last_mut() { + if search_string.find(previously_searched.as_str()).is_some() { + *previously_searched = search_string; + self.selected = Some(self.history.len() - 1); + return; + } + } + + self.history.push(search_string); + if self.history.len() > SEARCH_HISTORY_LIMIT { + self.history.remove(0); + } + self.selected = Some(self.history.len() - 1); + } + + pub fn next(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let selected = self.selected?; + if selected == history_size - 1 { + return None; + } + let next_index = selected + 1; + self.selected = Some(next_index); + Some(&self.history[next_index]) + } + + pub fn current(&self) -> Option<&str> { + Some(&self.history[self.selected?]) + } + + pub fn previous(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let prev_index = match self.selected { + Some(selected_index) => { + if selected_index == 0 { + return None; + } else { + selected_index - 1 + } + } + None => history_size - 1, + }; + + self.selected = Some(prev_index); + Some(&self.history[prev_index]) + } + + pub fn reset_selection(&mut self) { + self.selected = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.current(), + None, + "No current selection should be set fo the default search history" + ); + + search_history.add("rust".to_string()); + assert_eq!( + search_history.current(), + Some("rust"), + "Newly added item should be selected" + ); + + // check if duplicates are not added + search_history.add("rust".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should not add a duplicate" + ); + assert_eq!(search_history.current(), Some("rust")); + + // check if new string containing the previous string replaces it + search_history.add("rustlang".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should replace previous item if it's a substring" + ); + assert_eq!(search_history.current(), Some("rustlang")); + + // push enough items to test SEARCH_HISTORY_LIMIT + for i in 0..SEARCH_HISTORY_LIMIT * 2 { + search_history.add(format!("item{i}")); + } + assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); + } + + #[test] + fn test_next_and_previous() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.next(), + None, + "Default search history should not have a next item" + ); + + search_history.add("Rust".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("JavaScript".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("TypeScript".to_string()); + assert_eq!(search_history.next(), None); + + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.previous(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.previous(), Some("Rust")); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.previous(), None); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.next(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.next(), Some("TypeScript")); + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.next(), None); + assert_eq!(search_history.current(), Some("TypeScript")); + } + + #[test] + fn test_reset_selection() { + let mut search_history = SearchHistory::default(); + search_history.add("Rust".to_string()); + search_history.add("JavaScript".to_string()); + search_history.add("TypeScript".to_string()); + + assert_eq!(search_history.current(), Some("TypeScript")); + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + assert_eq!( + search_history.previous(), + Some("TypeScript"), + "Should start from the end after reset on previous item query" + ); + + search_history.previous(); + assert_eq!(search_history.current(), Some("JavaScript")); + search_history.previous(); + assert_eq!(search_history.current(), Some("Rust")); + + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + } +} diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs new file mode 100644 index 0000000000..b27365b8ed --- /dev/null +++ b/crates/search/src/mode.rs @@ -0,0 +1,73 @@ +use gpui::Action; + +use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; +// TODO: Update the default search mode to get from config +#[derive(Copy, Clone, Default, PartialEq)] +pub(crate) enum SearchMode { + #[default] + Text, + Semantic, + Regex, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) enum Side { + Left, + Right, +} + +impl SearchMode { + pub(crate) fn label(&self) -> &'static str { + match self { + SearchMode::Text => "Text", + SearchMode::Semantic => "Semantic", + SearchMode::Regex => "Regex", + } + } + + pub(crate) fn region_id(&self) -> usize { + match self { + SearchMode::Text => 3, + SearchMode::Semantic => 4, + SearchMode::Regex => 5, + } + } + + pub(crate) fn tooltip_text(&self) -> &'static str { + match self { + SearchMode::Text => "Activate Text Search", + SearchMode::Semantic => "Activate Semantic Search", + SearchMode::Regex => "Activate Regex Search", + } + } + + pub(crate) fn activate_action(&self) -> Box { + match self { + SearchMode::Text => Box::new(ActivateTextMode), + SearchMode::Semantic => Box::new(ActivateSemanticMode), + SearchMode::Regex => Box::new(ActivateRegexMode), + } + } + + pub(crate) fn border_left(&self) -> bool { + match self { + SearchMode::Text => false, + _ => true, + } + } + + pub(crate) fn border_right(&self) -> bool { + match self { + SearchMode::Regex => false, + _ => true, + } + } + + pub(crate) fn button_side(&self) -> Option { + match self { + SearchMode::Text => Some(Side::Left), + SearchMode::Semantic => None, + SearchMode::Regex => Some(Side::Right), + } + } +} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 53b54192da..c3e1a44afa 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,5 +1,7 @@ use crate::{ - NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, + history::SearchHistory, + mode::{SearchMode, Side}, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use anyhow::{Context, Result}; @@ -49,16 +51,7 @@ use workspace::{ actions!( project_search, - [ - SearchInNew, - ToggleFocus, - NextField, - CycleMode, - ToggleFilters, - ActivateTextMode, - ActivateSemanticMode, - ActivateRegexMode - ] + [SearchInNew, ToggleFocus, NextField, ToggleFilters,] ); #[derive(Default)] @@ -147,77 +140,6 @@ struct SemanticSearchState { _progress_task: Task<()>, } -// TODO: Update the default search mode to get from config -#[derive(Copy, Clone, Default, PartialEq)] -enum SearchMode { - #[default] - Text, - Semantic, - Regex, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -enum Side { - Left, - Right, -} - -impl SearchMode { - fn label(&self) -> &'static str { - match self { - SearchMode::Text => "Text", - SearchMode::Semantic => "Semantic", - SearchMode::Regex => "Regex", - } - } - - fn region_id(&self) -> usize { - match self { - SearchMode::Text => 3, - SearchMode::Semantic => 4, - SearchMode::Regex => 5, - } - } - - fn tooltip_text(&self) -> &'static str { - match self { - SearchMode::Text => "Activate Text Search", - SearchMode::Semantic => "Activate Semantic Search", - SearchMode::Regex => "Activate Regex Search", - } - } - - fn activate_action(&self) -> Box { - match self { - SearchMode::Text => Box::new(ActivateTextMode), - SearchMode::Semantic => Box::new(ActivateSemanticMode), - SearchMode::Regex => Box::new(ActivateRegexMode), - } - } - - fn border_left(&self) -> bool { - match self { - SearchMode::Text => false, - _ => true, - } - } - - fn border_right(&self) -> bool { - match self { - SearchMode::Regex => false, - _ => true, - } - } - - fn button_side(&self) -> Option { - match self { - SearchMode::Text => Some(Side::Left), - SearchMode::Semantic => None, - SearchMode::Regex => Some(Side::Right), - } - } -} - pub struct ProjectSearchBar { active_project_search: Option>, subscription: Option, diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 7940490de9..8aa03bdc35 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -3,9 +3,10 @@ pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext}; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use smallvec::SmallVec; pub mod buffer_search; +mod history; +mod mode; pub mod project_search; pub(crate) mod search_bar; @@ -17,14 +18,17 @@ pub fn init(cx: &mut AppContext) { actions!( search, [ + CycleMode, ToggleWholeWord, ToggleCaseSensitive, - ToggleRegex, SelectNextMatch, SelectPrevMatch, SelectAllMatches, NextHistoryQuery, PreviousHistoryQuery, + ActivateTextMode, + ActivateSemanticMode, + ActivateRegexMode ] ); @@ -43,7 +47,6 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => "Match Whole Word", SearchOptions::CASE_SENSITIVE => "Match Case", - SearchOptions::REGEX => "Use Regular Expression", _ => panic!("{:?} is not a named SearchOption", self), } } @@ -52,7 +55,6 @@ impl SearchOptions { match *self { SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), - SearchOptions::REGEX => Box::new(ToggleRegex), _ => panic!("{:?} is not a named SearchOption", self), } } @@ -69,187 +71,3 @@ impl SearchOptions { options } } - -const SEARCH_HISTORY_LIMIT: usize = 20; - -#[derive(Default, Debug, Clone)] -pub struct SearchHistory { - history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, - selected: Option, -} - -impl SearchHistory { - pub fn add(&mut self, search_string: String) { - if let Some(i) = self.selected { - if search_string == self.history[i] { - return; - } - } - - if let Some(previously_searched) = self.history.last_mut() { - if search_string.find(previously_searched.as_str()).is_some() { - *previously_searched = search_string; - self.selected = Some(self.history.len() - 1); - return; - } - } - - self.history.push(search_string); - if self.history.len() > SEARCH_HISTORY_LIMIT { - self.history.remove(0); - } - self.selected = Some(self.history.len() - 1); - } - - pub fn next(&mut self) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - - let selected = self.selected?; - if selected == history_size - 1 { - return None; - } - let next_index = selected + 1; - self.selected = Some(next_index); - Some(&self.history[next_index]) - } - - pub fn current(&self) -> Option<&str> { - Some(&self.history[self.selected?]) - } - - pub fn previous(&mut self) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - - let prev_index = match self.selected { - Some(selected_index) => { - if selected_index == 0 { - return None; - } else { - selected_index - 1 - } - } - None => history_size - 1, - }; - - self.selected = Some(prev_index); - Some(&self.history[prev_index]) - } - - pub fn reset_selection(&mut self) { - self.selected = None; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_add() { - let mut search_history = SearchHistory::default(); - assert_eq!( - search_history.current(), - None, - "No current selection should be set fo the default search history" - ); - - search_history.add("rust".to_string()); - assert_eq!( - search_history.current(), - Some("rust"), - "Newly added item should be selected" - ); - - // check if duplicates are not added - search_history.add("rust".to_string()); - assert_eq!( - search_history.history.len(), - 1, - "Should not add a duplicate" - ); - assert_eq!(search_history.current(), Some("rust")); - - // check if new string containing the previous string replaces it - search_history.add("rustlang".to_string()); - assert_eq!( - search_history.history.len(), - 1, - "Should replace previous item if it's a substring" - ); - assert_eq!(search_history.current(), Some("rustlang")); - - // push enough items to test SEARCH_HISTORY_LIMIT - for i in 0..SEARCH_HISTORY_LIMIT * 2 { - search_history.add(format!("item{i}")); - } - assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); - } - - #[test] - fn test_next_and_previous() { - let mut search_history = SearchHistory::default(); - assert_eq!( - search_history.next(), - None, - "Default search history should not have a next item" - ); - - search_history.add("Rust".to_string()); - assert_eq!(search_history.next(), None); - search_history.add("JavaScript".to_string()); - assert_eq!(search_history.next(), None); - search_history.add("TypeScript".to_string()); - assert_eq!(search_history.next(), None); - - assert_eq!(search_history.current(), Some("TypeScript")); - - assert_eq!(search_history.previous(), Some("JavaScript")); - assert_eq!(search_history.current(), Some("JavaScript")); - - assert_eq!(search_history.previous(), Some("Rust")); - assert_eq!(search_history.current(), Some("Rust")); - - assert_eq!(search_history.previous(), None); - assert_eq!(search_history.current(), Some("Rust")); - - assert_eq!(search_history.next(), Some("JavaScript")); - assert_eq!(search_history.current(), Some("JavaScript")); - - assert_eq!(search_history.next(), Some("TypeScript")); - assert_eq!(search_history.current(), Some("TypeScript")); - - assert_eq!(search_history.next(), None); - assert_eq!(search_history.current(), Some("TypeScript")); - } - - #[test] - fn test_reset_selection() { - let mut search_history = SearchHistory::default(); - search_history.add("Rust".to_string()); - search_history.add("JavaScript".to_string()); - search_history.add("TypeScript".to_string()); - - assert_eq!(search_history.current(), Some("TypeScript")); - search_history.reset_selection(); - assert_eq!(search_history.current(), None); - assert_eq!( - search_history.previous(), - Some("TypeScript"), - "Should start from the end after reset on previous item query" - ); - - search_history.previous(); - assert_eq!(search_history.current(), Some("JavaScript")); - search_history.previous(); - assert_eq!(search_history.current(), Some("Rust")); - - search_history.reset_selection(); - assert_eq!(search_history.current(), None); - } -} From d497f279f09e6bb84dd1c3d516bb4da65a877061 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:24:26 +0200 Subject: [PATCH 110/326] Move ButtonSide to new module --- crates/search/src/elements.rs | 146 ++++++++++++++++++++++++++++ crates/search/src/project_search.rs | 146 ++-------------------------- crates/search/src/search.rs | 1 + 3 files changed, 153 insertions(+), 140 deletions(-) create mode 100644 crates/search/src/elements.rs diff --git a/crates/search/src/elements.rs b/crates/search/src/elements.rs new file mode 100644 index 0000000000..cdc203983f --- /dev/null +++ b/crates/search/src/elements.rs @@ -0,0 +1,146 @@ +use std::ops::Range; + +use gpui::color::Color; +use gpui::geometry::rect::RectF; +use gpui::geometry::vector::IntoVector2F; +use gpui::json::{self, ToJson}; +use gpui::{scene::Path, LayoutContext}; +use gpui::{Element, SceneBuilder, View, ViewContext}; + +type CreatePath = fn(RectF, Color) -> Path; +type AdjustBorder = fn(RectF, f32) -> RectF; +type BorderThickness = f32; + +pub(crate) struct ButtonSide { + color: Color, + factory: CreatePath, + /// After the outline is drawn with border color, + /// the drawing bounds have to be adjusted by different factors in different dimensions. + border_adjustment: AdjustBorder, + border: Option<(BorderThickness, Color)>, +} + +impl ButtonSide { + fn new(color: Color, factory: CreatePath, border_adjustment: AdjustBorder) -> Self { + Self { + color, + factory, + border_adjustment, + border: None, + } + } + pub fn with_border(mut self, width: f32, color: Color) -> Self { + self.border = Some((width, color)); + self + } + pub fn left(color: Color) -> Self { + Self::new(color, left_button_side, left_button_border_adjust) + } + pub fn right(color: Color) -> Self { + Self::new(color, right_button_side, right_button_border_adjust) + } +} + +fn left_button_border_adjust(bounds: RectF, width: f32) -> RectF { + let width = width.into_vector_2f(); + let mut lower_right = bounds.clone().lower_right(); + lower_right.set_x(lower_right.x() + width.x()); + RectF::from_points(bounds.origin() + width, lower_right) +} +fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { + let width = width.into_vector_2f(); + let mut origin = bounds.clone().origin(); + origin.set_x(origin.x() - width.x()); + RectF::from_points(origin, bounds.lower_right() - width) +} +fn left_button_side(bounds: RectF, color: Color) -> Path { + use gpui::geometry::PathBuilder; + let mut path = PathBuilder::new(); + path.reset(bounds.lower_right()); + path.line_to(bounds.upper_right()); + let mut middle_point = bounds.origin(); + let distance_to_line = (middle_point.y() - bounds.lower_left().y()) / 4.; + middle_point.set_y(middle_point.y() - distance_to_line); + path.curve_to(middle_point, bounds.origin()); + let mut target = bounds.lower_left(); + target.set_y(target.y() + distance_to_line); + path.line_to(target); + path.curve_to(bounds.lower_right(), bounds.lower_left()); + path.build(color, None) +} + +fn right_button_side(bounds: RectF, color: Color) -> Path { + use gpui::geometry::PathBuilder; + let mut path = PathBuilder::new(); + path.reset(bounds.lower_left()); + path.line_to(bounds.origin()); + let mut middle_point = bounds.upper_right(); + let distance_to_line = (middle_point.y() - bounds.lower_right().y()) / 4.; + middle_point.set_y(middle_point.y() - distance_to_line); + path.curve_to(middle_point, bounds.upper_right()); + let mut target = bounds.lower_right(); + target.set_y(target.y() + distance_to_line); + path.line_to(target); + path.curve_to(bounds.lower_left(), bounds.lower_right()); + path.build(color, None) +} + +impl Element for ButtonSide { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + _: &mut V, + _: &mut LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + scene: &mut SceneBuilder, + bounds: RectF, + _: RectF, + _: &mut Self::LayoutState, + _: &mut V, + _: &mut ViewContext, + ) -> Self::PaintState { + let mut bounds = bounds; + if let Some((border_width, border_color)) = self.border.as_ref() { + scene.push_path((self.factory)(bounds, border_color.clone())); + bounds = (self.border_adjustment)(bounds, *border_width); + }; + scene.push_path((self.factory)(bounds, self.color)); + } + + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &V, + _: &ViewContext, + ) -> Option { + None + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &V, + _: &ViewContext, + ) -> gpui::json::Value { + json::json!({ + "type": "ButtonSide", + "bounds": bounds.to_json(), + "color": self.color.to_json(), + }) + } +} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c3e1a44afa..fa2630f969 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,4 +1,5 @@ use crate::{ + elements::ButtonSide, history::SearchHistory, mode::{SearchMode, Side}, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, @@ -11,12 +12,9 @@ use editor::{ SelectAll, MAX_TAB_TITLE_LEN, }; use futures::StreamExt; -use gpui::color::Color; -use gpui::geometry::rect::RectF; -use gpui::geometry::vector::IntoVector2F; -use gpui::json::{self, ToJson}; + use gpui::platform::PromptLevel; -use gpui::SceneBuilder; + use gpui::{ actions, elements::*, @@ -24,7 +22,7 @@ use gpui::{ Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; -use gpui::{scene::Path, LayoutContext}; + use menu::Confirm; use postage::stream::Stream; use project::{ @@ -717,7 +715,7 @@ impl ProjectSearchView { })?; if answer.next().await == Some(0) { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, _| { this.semantic_permissioned = Some(true); })?; } else { @@ -1150,138 +1148,6 @@ impl Default for ProjectSearchBar { Self::new() } } -type CreatePath = fn(RectF, Color) -> Path; -type AdjustBorder = fn(RectF, f32) -> RectF; -pub struct ButtonSide { - color: Color, - factory: CreatePath, - border_adjustment: AdjustBorder, - border: Option<(f32, Color)>, -} - -impl ButtonSide { - fn new(color: Color, factory: CreatePath, border_adjustment: AdjustBorder) -> Self { - Self { - color, - factory, - border_adjustment, - border: None, - } - } - pub fn with_border(mut self, width: f32, color: Color) -> Self { - self.border = Some((width, color)); - self - } - pub fn left(color: Color) -> Self { - Self::new(color, left_button_side, left_button_border_adjust) - } - pub fn right(color: Color) -> Self { - Self::new(color, right_button_side, right_button_border_adjust) - } -} -fn left_button_border_adjust(bounds: RectF, width: f32) -> RectF { - let width = width.into_vector_2f(); - let mut lower_right = bounds.clone().lower_right(); - lower_right.set_x(lower_right.x() + width.x()); - RectF::from_points(bounds.origin() + width, lower_right) -} -fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { - let width = width.into_vector_2f(); - let mut origin = bounds.clone().origin(); - origin.set_x(origin.x() - width.x()); - RectF::from_points(origin, bounds.lower_right() - width) -} -fn left_button_side(bounds: RectF, color: Color) -> Path { - use gpui::geometry::PathBuilder; - let mut path = PathBuilder::new(); - path.reset(bounds.lower_right()); - path.line_to(bounds.upper_right()); - let mut middle_point = bounds.origin(); - let distance_to_line = (middle_point.y() - bounds.lower_left().y()) / 4.; - middle_point.set_y(middle_point.y() - distance_to_line); - path.curve_to(middle_point, bounds.origin()); - let mut target = bounds.lower_left(); - target.set_y(target.y() + distance_to_line); - path.line_to(target); - path.curve_to(bounds.lower_right(), bounds.lower_left()); - path.build(color, None) -} - -fn right_button_side(bounds: RectF, color: Color) -> Path { - use gpui::geometry::PathBuilder; - let mut path = PathBuilder::new(); - path.reset(bounds.lower_left()); - path.line_to(bounds.origin()); - let mut middle_point = bounds.upper_right(); - let distance_to_line = (middle_point.y() - bounds.lower_right().y()) / 4.; - middle_point.set_y(middle_point.y() - distance_to_line); - path.curve_to(middle_point, bounds.upper_right()); - let mut target = bounds.lower_right(); - target.set_y(target.y() + distance_to_line); - path.line_to(target); - path.curve_to(bounds.lower_left(), bounds.lower_right()); - path.build(color, None) -} - -impl Element for ButtonSide { - type LayoutState = (); - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - _: &mut ProjectSearchBar, - _: &mut LayoutContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - (constraint.max, ()) - } - - fn paint( - &mut self, - scene: &mut SceneBuilder, - bounds: RectF, - _: RectF, - _: &mut Self::LayoutState, - _: &mut ProjectSearchBar, - _: &mut ViewContext, - ) -> Self::PaintState { - let mut bounds = bounds; - if let Some((border_width, border_color)) = self.border.as_ref() { - scene.push_path((self.factory)(bounds, border_color.clone())); - bounds = (self.border_adjustment)(bounds, *border_width); - }; - scene.push_path((self.factory)(bounds, self.color)); - } - - fn rect_for_text_range( - &self, - _: Range, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &ProjectSearchBar, - _: &ViewContext, - ) -> Option { - None - } - - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &ProjectSearchBar, - _: &ViewContext, - ) -> gpui::json::Value { - json::json!({ - "type": "ButtonSide", - "bounds": bounds.to_json(), - "color": self.color.to_json(), - }) - } -} impl ProjectSearchBar { pub fn new() -> Self { @@ -1480,7 +1346,7 @@ impl ProjectSearchBar { Direction::Prev => style.container.border.left = false, Direction::Next => style.container.border.right = false, }; - let mut label = Label::new(icon, style.label.clone()) + let label = Label::new(icon, style.label.clone()) .contained() .with_style(style.container.clone()); match direction { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 8aa03bdc35..3a985bbe8d 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -5,6 +5,7 @@ use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; pub mod buffer_search; +mod elements; mod history; mod mode; pub mod project_search; From 0ca29e56c26152c84d2efe16b8c8c6ae7fc03825 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:33:51 +0200 Subject: [PATCH 111/326] Update keybinds to use new names --- assets/keymaps/default.json | 6 +++--- crates/search/src/project_search.rs | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2f13ee6d03..97dad7a831 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -238,7 +238,7 @@ "context": "ProjectSearchBar", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "project_search::CycleMode" + "alt-tab": "search::CycleMode" } }, { @@ -252,7 +252,7 @@ "context": "ProjectSearchView", "bindings": { "escape": "project_search::ToggleFocus", - "alt-tab": "project_search::CycleMode" + "alt-tab": "search::CycleMode" } }, { @@ -264,7 +264,7 @@ "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex", + "alt-cmd-r": "search::ActivateRegexMode", "alt-cmd-f": "project_search::ToggleFilters" } }, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index fa2630f969..5fed36994a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,8 +2,8 @@ use crate::{ elements::ButtonSide, history::SearchHistory, mode::{SearchMode, Side}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, - SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use anyhow::{Context, Result}; use collections::HashMap; @@ -66,6 +66,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::cycle_mode); cx.add_action(ProjectSearchBar::next_history_query); cx.add_action(ProjectSearchBar::previous_history_query); + cx.add_action(ProjectSearchBar::activate_regex_mode); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); @@ -1296,6 +1297,19 @@ impl ProjectSearchBar { } } + fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Regex, cx) + }); + } else { + cx.propagate_action(); + } + } + fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { From c53554ead3cd8843ac316e1f5fb4c1c201330dd8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:42:11 +0200 Subject: [PATCH 112/326] Remove SearchOptions::REGEX. A bit WIP as it awaits migration of buffer search to modes --- crates/search/src/buffer_search.rs | 6 +++--- crates/search/src/mode.rs | 2 +- crates/search/src/project_search.rs | 12 ++---------- crates/search/src/search.rs | 2 -- crates/vim/src/normal/search.rs | 2 +- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 52dd943625..d7b8ab1410 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -209,12 +209,12 @@ impl View for BufferSearchBar { SearchOptions::WHOLE_WORD, cx, )) - .with_children(self.render_search_option( + /*.with_children(self.render_search_option( supported_options.regex, "Regex", SearchOptions::REGEX, cx, - )) + ))*/ .contained() .with_style(theme.search.option_button_group) .aligned(), @@ -697,7 +697,7 @@ impl BufferSearchBar { active_searchable_item.clear_matches(cx); let _ = done_tx.send(()); } else { - let query = if self.search_options.contains(SearchOptions::REGEX) { + let query = if true { match SearchQuery::regex( query, self.search_options.contains(SearchOptions::WHOLE_WORD), diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index b27365b8ed..aaaebc6e86 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -2,7 +2,7 @@ use gpui::Action; use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; // TODO: Update the default search mode to get from config -#[derive(Copy, Clone, Default, PartialEq)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub(crate) enum SearchMode { #[default] Text, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5fed36994a..ef905fa8e7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -736,15 +736,9 @@ impl ProjectSearchView { }).detach_and_log_err(cx); } SearchMode::Regex => { - if !self.is_option_enabled(SearchOptions::REGEX) { - self.toggle_search_option(SearchOptions::REGEX); - } self.semantic_state = None; } SearchMode::Text => { - if self.is_option_enabled(SearchOptions::REGEX) { - self.toggle_search_option(SearchOptions::REGEX); - } self.semantic_state = None; } } @@ -992,7 +986,7 @@ impl ProjectSearchView { return None; } }; - if self.search_options.contains(SearchOptions::REGEX) { + if self.current_mode == SearchMode::Regex { match SearchQuery::regex( text, self.search_options.contains(SearchOptions::WHOLE_WORD), @@ -1011,6 +1005,7 @@ impl ProjectSearchView { } } } else { + debug_assert_ne!(self.current_mode, SearchMode::Semantic); Some(SearchQuery::text( text, self.search_options.contains(SearchOptions::WHOLE_WORD), @@ -1139,9 +1134,6 @@ impl ProjectSearchView { cx.propagate_action(); } - fn is_option_enabled(&self, option: SearchOptions) -> bool { - self.search_options.contains(option) - } } impl Default for ProjectSearchBar { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 3a985bbe8d..b2cc43c7c1 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -39,7 +39,6 @@ bitflags! { const NONE = 0b000; const WHOLE_WORD = 0b001; const CASE_SENSITIVE = 0b010; - const REGEX = 0b100; } } @@ -68,7 +67,6 @@ impl SearchOptions { let mut options = SearchOptions::NONE; options.set(SearchOptions::WHOLE_WORD, query.whole_word()); options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); - options.set(SearchOptions::REGEX, query.is_regex()); options } } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 9375c4e78d..2ec4162e1e 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -66,7 +66,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext Date: Tue, 8 Aug 2023 14:56:21 +0200 Subject: [PATCH 113/326] Move nav buttons generation to search_bar --- crates/search/src/project_search.rs | 110 ++++------------------------ crates/search/src/search_bar.rs | 93 ++++++++++++++++++++++- 2 files changed, 107 insertions(+), 96 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ef905fa8e7..30bb76197b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,6 +2,7 @@ use crate::{ elements::ButtonSide, history::SearchHistory, mode::{SearchMode, Side}, + search_bar::render_nav_button, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; @@ -1322,91 +1323,6 @@ impl ProjectSearchBar { } } - fn render_nav_button( - &self, - icon: &'static str, - direction: Direction, - cx: &mut ViewContext, - ) -> AnyElement { - let action: Box; - let tooltip; - - match direction { - Direction::Prev => { - action = Box::new(SelectPrevMatch); - tooltip = "Select Previous Match"; - } - Direction::Next => { - action = Box::new(SelectNextMatch); - tooltip = "Select Next Match"; - } - }; - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { - let theme = theme::current(cx); - let mut style = theme.search.nav_button.style_for(state).clone(); - - match direction { - Direction::Prev => style.container.border.left = false, - Direction::Next => style.container.border.right = false, - }; - let label = Label::new(icon, style.label.clone()) - .contained() - .with_style(style.container.clone()); - match direction { - Direction::Prev => Flex::row() - .with_child( - ButtonSide::left( - style - .clone() - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(theme.search.mode_filling_width), - ) - .with_child(label) - .constrained() - .with_height(theme.workspace.toolbar.height), - Direction::Next => Flex::row() - .with_child(label) - .with_child( - ButtonSide::right( - style - .clone() - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(theme.search.mode_filling_width), - ) - .constrained() - .with_height(theme.workspace.toolbar.height), - } - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - direction as usize, - tooltip.to_string(), - Some(action), - tooltip_style, - cx, - ) - .into_any() - } fn render_option_button_icon( &self, icon: &'static str, @@ -1747,6 +1663,18 @@ impl View for ProjectSearchBar { let semantic_index = SemanticIndex::enabled(cx) .then(|| self.render_search_mode_button(SearchMode::Semantic, cx)); + let mut nav_button_for_direction = |label, direction| { + render_nav_button( + label, + direction, + move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + } + }, + cx, + ) + }; Flex::row() .with_child( Flex::column() @@ -1755,16 +1683,8 @@ impl View for ProjectSearchBar { .align_children_center() .with_child( Flex::row() - .with_child(self.render_nav_button( - "<", - Direction::Prev, - cx, - )) - .with_child(self.render_nav_button( - ">", - Direction::Next, - cx, - )) + .with_child(nav_button_for_direction("<", Direction::Prev)) + .with_child(nav_button_for_direction(">", Direction::Next)) .aligned(), ) .with_children(matches) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 5119e7866e..236509b86f 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,9 +1,12 @@ use gpui::{ - elements::{MouseEventHandler, Svg}, + elements::{Flex, Label, MouseEventHandler, ParentElement, Svg}, platform::{CursorStyle, MouseButton}, scene::MouseClick, Action, AnyElement, Element, EventContext, View, ViewContext, }; +use workspace::searchable::Direction; + +use crate::{elements::ButtonSide, SelectNextMatch, SelectPrevMatch}; pub(super) fn render_close_button( theme: &theme::Search, @@ -32,3 +35,91 @@ pub(super) fn render_close_button( .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) .into_any() } + +pub(super) fn render_nav_button( + icon: &'static str, + direction: Direction, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> AnyElement { + let action: Box; + let tooltip; + + match direction { + Direction::Prev => { + action = Box::new(SelectPrevMatch); + tooltip = "Select Previous Match"; + } + Direction::Next => { + action = Box::new(SelectNextMatch); + tooltip = "Select Next Match"; + } + }; + let tooltip_style = theme::current(cx).tooltip.clone(); + + enum NavButton {} + MouseEventHandler::::new(direction as usize, cx, |state, cx| { + let theme = theme::current(cx); + let mut style = theme.search.nav_button.style_for(state).clone(); + + match direction { + Direction::Prev => style.container.border.left = false, + Direction::Next => style.container.border.right = false, + }; + let label = Label::new(icon, style.label.clone()) + .contained() + .with_style(style.container.clone()); + match direction { + Direction::Prev => Flex::row() + .with_child( + ButtonSide::left( + style + .clone() + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .with_child(label) + .constrained() + .with_height(theme.workspace.toolbar.height), + Direction::Next => Flex::row() + .with_child(label) + .with_child( + ButtonSide::right( + style + .clone() + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .constrained() + .with_height(theme.workspace.toolbar.height), + } + }) + .on_click( + MouseButton::Left, + on_click, /*move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + }*/ + ) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + direction as usize, + tooltip.to_string(), + Some(action), + tooltip_style, + cx, + ) + .into_any() +} From 7547fa2679673e7e5d3c5387f98fe99037194f3d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 15:11:32 +0200 Subject: [PATCH 114/326] Move mode rendering to a search_bar module --- crates/search/src/project_search.rs | 142 ++++++++-------------------- crates/search/src/search_bar.rs | 89 ++++++++++++++++- 2 files changed, 129 insertions(+), 102 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 30bb76197b..4852e77004 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,8 +1,7 @@ use crate::{ - elements::ButtonSide, history::SearchHistory, - mode::{SearchMode, Side}, - search_bar::render_nav_button, + mode::SearchMode, + search_bar::{render_nav_button, render_search_mode_button}, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; @@ -1358,90 +1357,6 @@ impl ProjectSearchBar { .into_any() } - fn render_search_mode_button( - &self, - mode: SearchMode, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - - enum SearchModeButton {} - MouseEventHandler::::new(mode.region_id(), cx, |state, cx| { - let theme = theme::current(cx); - let mut style = theme - .search - .mode_button - .in_state(is_active) - .style_for(state) - .clone(); - - let label = Label::new(mode.label(), style.text.clone()) - .contained() - .with_style(style.container); - - if let Some(button_side) = mode.button_side() { - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - - if button_side == Side::Left { - Flex::row() - .align_children_center() - .with_child( - ButtonSide::left( - style - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(theme.search.mode_filling_width), - ) - .with_child(label) - .into_any() - } else { - Flex::row() - .align_children_center() - .with_child(label) - .with_child( - ButtonSide::right( - style - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(theme.search.mode_filling_width), - ) - .into_any() - } - } else { - label.into_any() - } - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.activate_search_mode(mode, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - mode.region_id(), - mode.tooltip_text().to_owned(), - Some(mode.activate_action()), - tooltip_style, - cx, - ) - .into_any() - } - fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { // Update Current Mode if let Some(search_view) = self.active_project_search.as_ref() { @@ -1660,21 +1575,38 @@ impl View for ProjectSearchBar { .flex(1., false), ) }); - - let semantic_index = SemanticIndex::enabled(cx) - .then(|| self.render_search_mode_button(SearchMode::Semantic, cx)); - let mut nav_button_for_direction = |label, direction| { - render_nav_button( - label, - direction, + let search_button_for_mode = |mode, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + is_active, move |_, this, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); - } + this.activate_search_mode(mode, cx); }, cx, ) }; + let semantic_index = SemanticIndex::enabled(cx) + .then(|| search_button_for_mode(SearchMode::Semantic, cx)); + let nav_button_for_direction = + |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + } + }, + cx, + ) + }; + Flex::row() .with_child( Flex::column() @@ -1683,8 +1615,16 @@ impl View for ProjectSearchBar { .align_children_center() .with_child( Flex::row() - .with_child(nav_button_for_direction("<", Direction::Prev)) - .with_child(nav_button_for_direction(">", Direction::Next)) + .with_child(nav_button_for_direction( + "<", + Direction::Prev, + cx, + )) + .with_child(nav_button_for_direction( + ">", + Direction::Next, + cx, + )) .aligned(), ) .with_children(matches) @@ -1726,9 +1666,9 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .align_children_center() - .with_child(self.render_search_mode_button(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Text, cx)) .with_children(semantic_index) - .with_child(self.render_search_mode_button(SearchMode::Regex, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) .with_child(super::search_bar::render_close_button( &theme.search, cx, diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 236509b86f..cd640ee4a8 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -6,7 +6,11 @@ use gpui::{ }; use workspace::searchable::Direction; -use crate::{elements::ButtonSide, SelectNextMatch, SelectPrevMatch}; +use crate::{ + elements::ButtonSide, + mode::{SearchMode, Side}, + SelectNextMatch, SelectPrevMatch, +}; pub(super) fn render_close_button( theme: &theme::Search, @@ -123,3 +127,86 @@ pub(super) fn render_nav_button( ) .into_any() } + +pub(crate) fn render_search_mode_button( + mode: SearchMode, + is_active: bool, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + // let is_active = if let Some(search) = self.active_project_search.as_ref() { + // let search = search.read(cx); + // search.current_mode == mode + // } else { + // false + // }; + + enum SearchModeButton {} + MouseEventHandler::::new(mode.region_id(), cx, |state, cx| { + let theme = theme::current(cx); + let mut style = theme + .search + .mode_button + .in_state(is_active) + .style_for(state) + .clone(); + + let label = Label::new(mode.label(), style.text.clone()) + .contained() + .with_style(style.container); + + if let Some(button_side) = mode.button_side() { + style.container.border.left = mode.border_left(); + style.container.border.right = mode.border_right(); + + if button_side == Side::Left { + Flex::row() + .align_children_center() + .with_child( + ButtonSide::left( + style + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .with_child(label) + .into_any() + } else { + Flex::row() + .align_children_center() + .with_child(label) + .with_child( + ButtonSide::right( + style + .container + .background_color + .unwrap_or_else(gpui::color::Color::transparent_black), + ) + .with_border(style.container.border.width, style.container.border.color) + .contained() + .constrained() + .with_max_width(theme.search.mode_filling_width), + ) + .into_any() + } + } else { + label.into_any() + } + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + mode.region_id(), + mode.tooltip_text().to_owned(), + Some(mode.activate_action()), + tooltip_style, + cx, + ) + .into_any() +} From 0374fdfd062708a8edf1c2e42692381d6ac79cbb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 15:20:34 +0200 Subject: [PATCH 115/326] Move mode cycling to mode module --- crates/search/src/mode.rs | 14 ++++++++++++++ crates/search/src/project_search.rs | 14 ++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index aaaebc6e86..072b9ffd80 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -71,3 +71,17 @@ impl SearchMode { } } } + +pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { + let next_text_state = if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Regex + }; + + match mode { + SearchMode::Text => next_text_state, + SearchMode::Semantic => SearchMode::Regex, + SearchMode::Regex => SearchMode::Text, + } +} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4852e77004..97b9f7b75b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1155,18 +1155,8 @@ impl ProjectSearchBar { .and_then(|item| item.downcast::()) { search_view.update(cx, |this, cx| { - let mode = &this.current_mode; - let next_text_state = if SemanticIndex::enabled(cx) { - SearchMode::Semantic - } else { - SearchMode::Regex - }; - - let new_mode = match mode { - &SearchMode::Text => next_text_state, - &SearchMode::Semantic => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, - }; + let new_mode = + crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); this.activate_search_mode(new_mode, cx); }) } From fecea03c90a1160c883834b59ac7625185d9cf65 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 15:25:32 +0200 Subject: [PATCH 116/326] Add modes to buffer search --- crates/search/src/buffer_search.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d7b8ab1410..8823f8236e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,7 @@ use crate::{ - history::SearchHistory, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, - SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + history::SearchHistory, mode::SearchMode, search_bar::render_search_mode_button, + NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, + SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -78,6 +79,7 @@ pub struct BufferSearchBar { query_contains_error: bool, dismissed: bool, search_history: SearchHistory, + current_mode: SearchMode, } impl Entity for BufferSearchBar { @@ -149,7 +151,18 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); + let search_button_for_mode = |mode, cx: &mut ViewContext| { + let is_active = self.current_mode == mode; + render_search_mode_button( + mode, + is_active, + move |_, this, cx| { + //this.activate_search_mode(mode, cx); + }, + cx, + ) + }; Flex::row() .with_child( Flex::row() @@ -221,6 +234,8 @@ impl View for BufferSearchBar { ) .flex(1., true), ) + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) .with_child(super::search_bar::render_close_button( &theme.search, cx, @@ -308,6 +323,7 @@ impl BufferSearchBar { query_contains_error: false, dismissed: true, search_history: SearchHistory::default(), + current_mode: SearchMode::default(), } } From 343e9d080ed1b35bd156ec11ca4f352aaf12dcdd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 15:57:07 +0200 Subject: [PATCH 117/326] Finish migration to modes for buffer search --- crates/search/src/buffer_search.rs | 24 +++++++++++++++--------- crates/search/src/mode.rs | 2 +- crates/search/src/search.rs | 1 + crates/vim/src/normal/search.rs | 8 +++----- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8823f8236e..758a0b17cf 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -158,7 +158,7 @@ impl View for BufferSearchBar { mode, is_active, move |_, this, cx| { - //this.activate_search_mode(mode, cx); + this.activate_search_mode(mode, cx); }, cx, ) @@ -222,12 +222,6 @@ impl View for BufferSearchBar { SearchOptions::WHOLE_WORD, cx, )) - /*.with_children(self.render_search_option( - supported_options.regex, - "Regex", - SearchOptions::REGEX, - cx, - ))*/ .contained() .with_style(theme.search.option_button_group) .aligned(), @@ -537,7 +531,19 @@ impl BufferSearchBar { ) .into_any() } - + pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + assert_ne!( + mode, + SearchMode::Semantic, + "Semantic search is not supported in buffer search" + ); + if mode == self.current_mode { + return; + } + self.current_mode = mode; + let _ = self.update_matches(cx); + cx.notify(); + } fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { let mut propagate_action = true; if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { @@ -713,7 +719,7 @@ impl BufferSearchBar { active_searchable_item.clear_matches(cx); let _ = done_tx.send(()); } else { - let query = if true { + let query = if self.current_mode == SearchMode::Regex { match SearchQuery::regex( query, self.search_options.contains(SearchOptions::WHOLE_WORD), diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 072b9ffd80..bb620f0670 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -3,7 +3,7 @@ use gpui::Action; use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; // TODO: Update the default search mode to get from config #[derive(Copy, Clone, Debug, Default, PartialEq)] -pub(crate) enum SearchMode { +pub enum SearchMode { #[default] Text, Semantic, diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index b2cc43c7c1..33905f83ec 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,6 +1,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext}; +pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 2ec4162e1e..5f1a68cfe9 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,5 +1,5 @@ use gpui::{actions, impl_actions, AppContext, ViewContext}; -use search::{buffer_search, BufferSearchBar, SearchOptions}; +use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions}; use serde_derive::Deserialize; use workspace::{searchable::Direction, Pane, Workspace}; @@ -65,10 +65,8 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext Date: Tue, 8 Aug 2023 16:26:19 +0200 Subject: [PATCH 118/326] Move option button rendering to search_bar --- crates/search/src/project_search.rs | 69 ++++++++--------------------- crates/search/src/search_bar.rs | 35 ++++++++++++++- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 97b9f7b75b..611fd02846 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1312,41 +1312,6 @@ impl ProjectSearchBar { } } - fn render_option_button_icon( - &self, - icon: &'static str, - option: SearchOptions, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.is_option_enabled(option, cx); - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Svg::new(icon) - .with_color(style.text.color.clone()) - .contained() - .with_style(style.container) - .constrained() - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_search_option(option, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - option.bits as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, - ) - .into_any() - } - fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { // Update Current Mode if let Some(search_view) = self.active_project_search.as_ref() { @@ -1478,26 +1443,28 @@ impl View for ProjectSearchBar { }; let search = _search.read(cx); let is_semantic_disabled = search.semantic_state.is_none(); - - let case_sensitive = if is_semantic_disabled { - Some(self.render_option_button_icon( + let render_option_button_icon = |path, option, cx: &mut ViewContext| { + crate::search_bar::render_option_button_icon( + self.is_option_enabled(option, cx), + path, + option, + move |_, this, cx| { + this.toggle_search_option(option, cx); + }, + cx, + ) + }; + let case_sensitive = is_semantic_disabled.then(|| { + render_option_button_icon( "icons/case_insensitive_12.svg", SearchOptions::CASE_SENSITIVE, cx, - )) - } else { - None - }; + ) + }); - let whole_word = if is_semantic_disabled { - Some(self.render_option_button_icon( - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - )) - } else { - None - }; + let whole_word = is_semantic_disabled.then(|| { + render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) + }); let search = _search.read(cx); let icon_style = theme.search.editor_icon.clone(); diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index cd640ee4a8..446039e960 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -9,7 +9,7 @@ use workspace::searchable::Direction; use crate::{ elements::ButtonSide, mode::{SearchMode, Side}, - SelectNextMatch, SelectPrevMatch, + SearchOptions, SelectNextMatch, SelectPrevMatch, }; pub(super) fn render_close_button( @@ -210,3 +210,36 @@ pub(crate) fn render_search_mode_button( ) .into_any() } + +pub(crate) fn render_option_button_icon( + is_active: bool, + icon: &'static str, + option: SearchOptions, + on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); + Svg::new(icon) + .with_color(style.text.color.clone()) + .contained() + .with_style(style.container) + .constrained() + }) + .on_click(MouseButton::Left, on_click) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + option.bits as usize, + format!("Toggle {}", option.label()), + Some(option.to_toggle_action()), + tooltip_style, + cx, + ) + .into_any() +} From 17c9b4ca968c8605e69c5840fe838feb2735d894 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 8 Aug 2023 10:03:55 -0700 Subject: [PATCH 119/326] Fix tests --- crates/client/src/channel_store_tests.rs | 11 ++++++----- crates/zed/src/zed.rs | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs index 7f31243dad..69d5fed70d 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/client/src/channel_store_tests.rs @@ -35,8 +35,8 @@ fn test_update_channels(cx: &mut AppContext) { &channel_store, &[ // - (0, "a", true), - (0, "b", false), + (0, "a", false), + (0, "b", true), ], cx, ); @@ -65,9 +65,9 @@ fn test_update_channels(cx: &mut AppContext) { assert_channels( &channel_store, &[ - (0, "a", true), - (1, "y", true), - (0, "b", false), + (0, "a", false), + (1, "y", false), + (0, "b", true), (1, "x", false), ], cx, @@ -82,6 +82,7 @@ fn update_channels( channel_store.update(cx, |store, cx| store.update_channels(message, cx)); } +#[track_caller] fn assert_channels( channel_store: &ModelHandle, expected_channels: &[(usize, &str, bool)], diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 655f0ec84c..f435d9a721 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2401,6 +2401,7 @@ mod tests { language::init(cx); editor::init(cx); project_panel::init_settings(cx); + collab_ui::init(&app_state, cx); pane::init(cx); project_panel::init((), cx); terminal_view::init(cx); From 6a7245b92bdf5ad2be361569953fe8ed56d0d53f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 8 Aug 2023 10:44:44 -0700 Subject: [PATCH 120/326] Fix positioning on face piles, fix panic on member invite removal --- crates/collab_ui/src/collab_panel/channel_modal.rs | 2 +- crates/collab_ui/src/face_pile.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 7ce830b22f..09be3798a6 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -534,7 +534,7 @@ impl ChannelModalDelegate { this.selected_index = this .selected_index - .min(this.matching_member_indices.len() - 1); + .min(this.matching_member_indices.len().saturating_sub(1)); cx.focus_self(); cx.notify(); diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 30fcb97506..b604761488 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -37,12 +37,18 @@ impl Element for FacePile { debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); let mut width = 0.; + let mut max_height = 0.; for face in &mut self.faces { - width += face.layout(constraint, view, cx).x(); + let layout = face.layout(constraint, view, cx); + width += layout.x(); + max_height = f32::max(max_height, layout.y()); } width -= self.overlap * self.faces.len().saturating_sub(1) as f32; - (Vector2F::new(width, constraint.max.y()), ()) + ( + Vector2F::new(width, max_height.clamp(1., constraint.max.y())), + (), + ) } fn paint( From d00f6a490c4a089a249fcd23ddd498eb8c79f71c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 8 Aug 2023 11:47:13 -0700 Subject: [PATCH 121/326] Fix a bug where channel invitations would show up in the channels section Block non-members from reading channel information WIP: Make sure Arc::make_mut() works --- crates/client/src/channel_store.rs | 7 ++- crates/collab/src/db.rs | 62 +++++++++++++++------- crates/collab/src/db/tests.rs | 3 ++ crates/collab/src/rpc.rs | 32 +++++++---- crates/collab/src/tests/channel_tests.rs | 67 ++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 33 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 317fbd1189..93b96fc629 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -301,8 +301,10 @@ impl ChannelStore { .iter_mut() .find(|c| c.id == channel.id) { - let existing_channel = Arc::make_mut(existing_channel); + let existing_channel = Arc::get_mut(existing_channel) + .expect("channel is shared, update would have been lost"); existing_channel.name = channel.name; + existing_channel.user_is_admin = channel.user_is_admin; continue; } @@ -320,7 +322,8 @@ impl ChannelStore { for channel in payload.channels { if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { - let existing_channel = Arc::make_mut(existing_channel); + let existing_channel = Arc::get_mut(existing_channel) + .expect("channel is shared, update would have been lost"); existing_channel.name = channel.name; existing_channel.user_is_admin = channel.user_is_admin; continue; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index c3ffc12634..eb40587ea7 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3601,7 +3601,7 @@ impl Database { ) .one(&*tx) .await? - .ok_or_else(|| anyhow!("user is not a channel member"))?; + .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?; Ok(()) } @@ -3621,7 +3621,7 @@ impl Database { ) .one(&*tx) .await? - .ok_or_else(|| anyhow!("user is not a channel admin"))?; + .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; Ok(()) } @@ -3723,31 +3723,53 @@ impl Database { 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 pub async fn get_channel( &self, channel_id: ChannelId, user_id: UserId, - ) -> Result> { + ) -> Result> { self.transaction(|tx| async move { let tx = tx; - let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; - let user_is_admin = channel_member::Entity::find() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(user_id)) - .and(channel_member::Column::Admin.eq(true)), - ) - .count(&*tx) - .await? - > 0; - Ok(channel.map(|channel| Channel { - id: channel.id, - name: channel.name, - user_is_admin, - parent_id: None, - })) + let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + + 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 (user_is_admin, is_accepted) = channel_membership + .map(|membership| (membership.admin, membership.accepted)) + .unwrap_or((false, false)); + + Ok(Some(( + Channel { + id: channel.id, + name: channel.name, + user_is_admin, + parent_id: None, + }, + is_accepted, + ))) + } else { + Ok(None) + } }) .await } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index efc35a5c24..cdcde3332c 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -915,6 +915,9 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { let zed_id = db.create_root_channel("zed", "1", 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()); + db.invite_channel_member(zed_id, b_id, a_id, false) .await .unwrap(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f1fd97db41..a24db6be81 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2210,14 +2210,15 @@ async fn invite_channel_member( ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let channel = db - .get_channel(channel_id, session.user_id) - .await? - .ok_or_else(|| anyhow!("channel not found"))?; let invitee_id = UserId::from_proto(request.user_id); db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin) .await?; + let (channel, _) = db + .get_channel(channel_id, session.user_id) + .await? + .ok_or_else(|| anyhow!("channel not found"))?; + let mut update = proto::UpdateChannels::default(); update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), @@ -2275,18 +2276,27 @@ async fn set_channel_member_admin( db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin) .await?; - let channel = db + let (channel, has_accepted) = db .get_channel(channel_id, member_id) .await? .ok_or_else(|| anyhow!("channel not found"))?; let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - parent_id: None, - user_is_admin: request.admin, - }); + if has_accepted { + update.channels.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + user_is_admin: request.admin, + }); + } else { + update.channel_invitations.push(proto::Channel { + id: channel.id.to_proto(), + name: channel.name, + parent_id: None, + user_is_admin: request.admin, + }); + } for connection_id in session .connection_pool() diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 88d88a40fd..9723b18394 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -584,3 +584,70 @@ async fn test_channel_jumping(deterministic: Arc, cx_a: &mut Test ); }); } + +#[gpui::test] +async fn test_permissions_update_while_invited( + 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 rust_id = server + .make_channel("rust", (&client_a, cx_a), &mut []) + .await; + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channel_invitations(), + &[Arc::new(Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + user_is_admin: false, + depth: 0, + })], + ); + + assert_eq!(channels.channels(), &[],); + }); + + // Update B's invite before they've accepted it + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channel_invitations(), + &[Arc::new(Channel { + id: rust_id, + name: "rust".to_string(), + parent_id: None, + user_is_admin: true, + depth: 0, + })], + ); + + assert_eq!(channels.channels(), &[],); + }); +} From b708824d3796306dc7de4734cd0f8440e83de4af Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 8 Aug 2023 12:46:13 -0700 Subject: [PATCH 122/326] Position and style the channel editor correctly Fix a bug where some channel updates would be lost Add channel name sanitization before storing in the database --- crates/client/src/channel_store.rs | 34 +++++++++++++++++++------ crates/collab/src/db.rs | 1 + crates/collab_ui/src/collab_panel.rs | 36 +++++++++++++++++++++++---- crates/theme/src/theme.rs | 2 ++ crates/util/src/util.rs | 15 +++++++++++ styles/src/style_tree/collab_panel.ts | 7 +++++- 6 files changed, 81 insertions(+), 14 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 93b96fc629..1beb1bc8ea 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -4,8 +4,10 @@ use anyhow::Result; use collections::HashMap; use collections::HashSet; use futures::Future; +use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, TypedEnvelope}; +use std::mem; use std::sync::Arc; pub type ChannelId = u64; @@ -19,6 +21,7 @@ pub struct ChannelStore { client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, + _maintain_user: Task<()>, } #[derive(Clone, Debug, PartialEq)] @@ -55,6 +58,20 @@ impl ChannelStore { let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_channels); + let mut current_user = user_store.read(cx).watch_current_user(); + let maintain_user = cx.spawn(|this, mut cx| async move { + while let Some(current_user) = current_user.next().await { + if current_user.is_none() { + this.update(&mut cx, |this, cx| { + this.channels.clear(); + this.channel_invitations.clear(); + this.channel_participants.clear(); + this.outgoing_invites.clear(); + cx.notify(); + }); + } + } + }); Self { channels: vec![], channel_invitations: vec![], @@ -63,6 +80,7 @@ impl ChannelStore { client, user_store, _rpc_subscription: rpc_subscription, + _maintain_user: maintain_user, } } @@ -301,10 +319,10 @@ impl ChannelStore { .iter_mut() .find(|c| c.id == channel.id) { - let existing_channel = Arc::get_mut(existing_channel) - .expect("channel is shared, update would have been lost"); - existing_channel.name = channel.name; - existing_channel.user_is_admin = channel.user_is_admin; + util::make_arc_mut(existing_channel, |new_existing_channel| { + new_existing_channel.name = channel.name; + new_existing_channel.user_is_admin = channel.user_is_admin; + }); continue; } @@ -322,10 +340,10 @@ impl ChannelStore { for channel in payload.channels { if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { - let existing_channel = Arc::get_mut(existing_channel) - .expect("channel is shared, update would have been lost"); - existing_channel.name = channel.name; - existing_channel.user_is_admin = channel.user_is_admin; + util::make_arc_mut(existing_channel, |new_existing_channel| { + new_existing_channel.name = channel.name; + new_existing_channel.user_is_admin = channel.user_is_admin; + }); continue; } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index eb40587ea7..ed5e7e8e3d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3155,6 +3155,7 @@ impl Database { live_kit_room: &str, creator_id: UserId, ) -> Result { + let name = name.trim().trim_start_matches('#'); self.transaction(move |tx| async move { if let Some(parent) = parent { self.check_user_is_channel_admin(parent, creator_id, &*tx) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f745420eeb..2b39678f5e 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -308,7 +308,7 @@ impl CollabPanel { cx, ), ListEntry::ChannelEditor { depth } => { - this.render_channel_editor(&theme.collab_panel, *depth, cx) + this.render_channel_editor(&theme, *depth, cx) } } }); @@ -1280,11 +1280,37 @@ impl CollabPanel { fn render_channel_editor( &self, - _theme: &theme::CollabPanel, - _depth: usize, + theme: &theme::Theme, + depth: usize, cx: &AppContext, ) -> AnyElement { - ChildView::new(&self.channel_name_editor, cx).into_any() + Flex::row() + .with_child( + Svg::new("icons/channel_hash.svg") + .with_color(theme.collab_panel.channel_hash.color) + .constrained() + .with_width(theme.collab_panel.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + ChildView::new(&self.channel_name_editor, cx) + .contained() + .with_style(theme.collab_panel.channel_editor) + .flex(1.0, true), + ) + .align_children_center() + .contained() + .with_padding_left( + theme.collab_panel.contact_row.default_style().padding.left + + theme.collab_panel.channel_indent * depth as f32, + ) + .contained() + .with_style(gpui::elements::ContainerStyle { + background_color: Some(theme.editor.background), + ..Default::default() + }) + .into_any() } fn render_channel( @@ -1331,7 +1357,7 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style(*theme.contact_row.in_state(is_selected).style_for(state)) + .with_style(*theme.contact_row.style_for(is_selected, state)) .with_padding_left( theme.contact_row.default_style().padding.left + theme.channel_indent * channel.depth as f32, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c554f77fe4..cf8da6233a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -221,6 +221,7 @@ pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, pub log_in_button: Interactive, + pub channel_editor: ContainerStyle, pub channel_hash: Icon, pub channel_modal: ChannelModal, pub user_query_editor: FieldEditor, @@ -885,6 +886,7 @@ impl Toggleable { pub fn active_state(&self) -> &T { self.in_state(true) } + pub fn inactive_state(&self) -> &T { self.in_state(false) } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index c8beb86aef..2766cee295 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -9,9 +9,11 @@ pub mod test; use std::{ borrow::Cow, cmp::{self, Ordering}, + mem, ops::{AddAssign, Range, RangeInclusive}, panic::Location, pin::Pin, + sync::Arc, task::{Context, Poll}, }; @@ -118,6 +120,19 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se } } +/// Mutates through the arc if no other references exist, +/// otherwise clones the value and swaps out the reference with a new Arc +/// Useful for mutating the elements of a list while using iter_mut() +pub fn make_arc_mut(arc: &mut Arc, mutate: impl FnOnce(&mut T)) { + if let Some(t) = Arc::get_mut(arc) { + mutate(t); + return; + } + let mut new_t = (**arc).clone(); + mutate(&mut new_t); + mem::swap(&mut Arc::new(new_t), arc); +} + pub trait ResultExt { type Ok; diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 2c543356b0..a859f6d670 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -316,6 +316,11 @@ export default function contacts_panel(): any { }, }, }), - face_overlap: 8 + face_overlap: 8, + channel_editor: { + padding: { + left: 8, + } + } } } From bbe4a9b38881824433453e654da5092258c02024 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 8 Aug 2023 12:46:13 -0700 Subject: [PATCH 123/326] Position and style the channel editor correctly Fix a bug where some channel updates would be lost Add channel name sanitization before storing in the database --- assets/keymaps/default.json | 8 +++ crates/collab_ui/src/collab_panel.rs | 96 ++++++++++++++++++++++++---- crates/menu/src/menu.rs | 3 +- 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 11cc50a03e..f4d36ee95b 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -13,6 +13,7 @@ "cmd-up": "menu::SelectFirst", "cmd-down": "menu::SelectLast", "enter": "menu::Confirm", + "ctrl-enter": "menu::ShowContextMenu", "cmd-enter": "menu::SecondaryConfirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", @@ -550,6 +551,13 @@ "alt-shift-f": "project_panel::NewSearchInDirectory" } }, + { + "context": "CollabPanel", + "bindings": { + "ctrl-backspace": "collab_panel::Remove", + "space": "menu::Confirm" + } + }, { "context": "ChannelModal", "bindings": { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 2b39678f5e..85e0d80cce 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -15,7 +15,7 @@ use gpui::{ actions, elements::{ Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, }, geometry::{ rect::RectF, @@ -64,7 +64,7 @@ struct ManageMembers { channel_id: u64, } -actions!(collab_panel, [ToggleFocus]); +actions!(collab_panel, [ToggleFocus, Remove, Secondary]); impl_actions!( collab_panel, @@ -82,7 +82,9 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::select_next); cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); - cx.add_action(CollabPanel::remove_channel); + cx.add_action(CollabPanel::remove); + cx.add_action(CollabPanel::remove_channel_action); + cx.add_action(CollabPanel::show_inline_context_menu); cx.add_action(CollabPanel::new_subchannel); cx.add_action(CollabPanel::invite_members); cx.add_action(CollabPanel::manage_members); @@ -113,6 +115,7 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, workspace: WeakViewHandle, + context_menu_on_selected: bool, } #[derive(Serialize, Deserialize)] @@ -274,7 +277,26 @@ impl CollabPanel { ) } ListEntry::Channel(channel) => { - this.render_channel(&*channel, &theme.collab_panel, is_selected, cx) + let channel_row = this.render_channel( + &*channel, + &theme.collab_panel, + is_selected, + cx, + ); + + if is_selected && this.context_menu_on_selected { + Stack::new() + .with_child(channel_row) + .with_child( + ChildView::new(&this.context_menu, cx) + .aligned() + .bottom() + .right(), + ) + .into_any() + } else { + return channel_row; + } } ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), @@ -332,6 +354,7 @@ impl CollabPanel { collapsed_sections: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), + context_menu_on_selected: true, list_state, }; @@ -1321,6 +1344,7 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() .with_child( @@ -1367,7 +1391,7 @@ impl CollabPanel { this.join_channel(channel_id, cx); }) .on_click(MouseButton::Right, move |e, this, cx| { - this.deploy_channel_context_menu(e.position, channel_id, cx); + this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .into_any() } @@ -1573,15 +1597,27 @@ impl CollabPanel { fn deploy_channel_context_menu( &mut self, - position: Vector2F, + position: Option, channel_id: u64, cx: &mut ViewContext, ) { if self.channel_store.read(cx).is_user_admin(channel_id) { + self.context_menu_on_selected = position.is_none(); + self.context_menu.update(cx, |context_menu, cx| { + context_menu.set_position_mode(if self.context_menu_on_selected { + OverlayPositionMode::Local + } else { + OverlayPositionMode::Window + }); + context_menu.show( - position, - gpui::elements::AnchorCorner::BottomLeft, + position.unwrap_or_default(), + if self.context_menu_on_selected { + gpui::elements::AnchorCorner::TopRight + } else { + gpui::elements::AnchorCorner::BottomLeft + }, vec![ ContextMenuItem::action("New Channel", NewChannel { channel_id }), ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), @@ -1591,6 +1627,8 @@ impl CollabPanel { cx, ); }); + + cx.notify(); } } @@ -1755,6 +1793,33 @@ impl CollabPanel { self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); } + // TODO: Make join into a toggle + // TODO: Make enter work on channel editor + fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.remove_channel(channel.id, cx) + } + } + + fn rename(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) {} + + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { + let Some(channel) = self.selected_channel() else { + return; + }; + + self.deploy_channel_context_menu(None, channel.id, cx); + } + + fn selected_channel(&self) -> Option<&Arc> { + self.selection + .and_then(|ix| self.entries.get(ix)) + .and_then(|entry| match entry { + ListEntry::Channel(channel) => Some(channel), + _ => None, + }) + } + fn show_channel_modal( &mut self, channel_id: ChannelId, @@ -1788,8 +1853,11 @@ impl CollabPanel { .detach(); } - fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { - let channel_id = action.channel_id; + fn remove_channel_action(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { + self.remove_channel(action.channel_id, cx) + } + + fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let channel_store = self.channel_store.clone(); if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { let prompt_message = format!( @@ -1818,6 +1886,9 @@ impl CollabPanel { } } + // Should move to the filter editor if clicking on it + // Should move selection to the channel editor if activating it + fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { let user_store = self.user_store.clone(); let prompt_message = format!( @@ -1969,7 +2040,10 @@ impl View for CollabPanel { .with_width(self.size(cx)) .into_any(), ) - .with_child(ChildView::new(&self.context_menu, cx)) + .with_children( + (!self.context_menu_on_selected) + .then(|| ChildView::new(&self.context_menu, cx)), + ) .into_any() }) .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs index b0f1a9c6c8..519ad1ecd0 100644 --- a/crates/menu/src/menu.rs +++ b/crates/menu/src/menu.rs @@ -7,6 +7,7 @@ gpui::actions!( SelectPrev, SelectNext, SelectFirst, - SelectLast + SelectLast, + ShowContextMenu ] ); From a7bd05ec0ac1b19a0336edf86dd3b2411d436c1c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:16:12 +0200 Subject: [PATCH 124/326] buffer: use icons for search options --- crates/search/src/buffer_search.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 758a0b17cf..e02720712a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -163,6 +163,21 @@ impl View for BufferSearchBar { cx, ) }; + let render_search_option = + |options: bool, icon, option, cx: &mut ViewContext| { + options.then(|| { + let is_active = self.search_options.contains(option); + crate::search_bar::render_option_button_icon::( + is_active, + icon, + option, + move |_, this, cx| { + this.toggle_search_option(option, cx); + }, + cx, + ) + }) + }; Flex::row() .with_child( Flex::row() @@ -210,15 +225,15 @@ impl View for BufferSearchBar { ) .with_child( Flex::row() - .with_children(self.render_search_option( + .with_children(render_search_option( supported_options.case, - "Case", + "icons/case_insensitive_12.svg", SearchOptions::CASE_SENSITIVE, cx, )) - .with_children(self.render_search_option( + .with_children(render_search_option( supported_options.word, - "Word", + "icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx, )) From e1724daf35285c19ff5147f066bfd12911f2851f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:38:50 +0200 Subject: [PATCH 125/326] buffer: WIP style sides of buffer search --- crates/search/src/buffer_search.rs | 78 +++++++++++++++++++----------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e02720712a..a4658c9428 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -180,14 +180,15 @@ impl View for BufferSearchBar { }; Flex::row() .with_child( - Flex::row() + Flex::column() .with_child( Flex::row() + .align_children_center() .with_child( - ChildView::new(&self.query_editor, cx) - .aligned() - .left() - .flex(1., true), + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned(), ) .with_children(self.active_searchable_item.as_ref().and_then( |searchable_item| { @@ -208,23 +209,24 @@ impl View for BufferSearchBar { ) }, )) - .contained() - .with_style(editor_container) .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .with_child(self.render_action_button("Select All", cx)) - .aligned(), + .left() + .top(), ) + .contained() + .flex(1., true), + ) + .with_child( + Flex::row() + .align_children_center() .with_child( Flex::row() + .with_child( + ChildView::new(&self.query_editor, cx) + .aligned() + .left() + .flex(1., true), + ) .with_children(render_search_option( supported_options.case, "icons/case_insensitive_12.svg", @@ -238,19 +240,39 @@ impl View for BufferSearchBar { cx, )) .contained() - .with_style(theme.search.option_button_group) + .with_style(editor_container) + .aligned() + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false), + ) + .with_child( + Flex::row() + .with_child(self.render_action_button("Select All", cx)) .aligned(), ) - .flex(1., true), + .flex(1., false), + ) + .with_child( + Flex::column().with_child( + Flex::row() + .align_children_center() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .with_child(super::search_bar::render_close_button( + &theme.search, + cx, + |_, this, cx| this.dismiss(&Default::default(), cx), + Some(Box::new(Dismiss)), + )) + .contained() + .aligned() + .right() + .top() + .flex(1., true), + ), ) - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .with_child(super::search_bar::render_close_button( - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - )) .contained() .with_style(theme.search.container) .into_any_named("search bar") From b282bdfe5a3e02bceba2464b9c9d22ed1094f5cd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:09:07 +0200 Subject: [PATCH 126/326] buffer: finish up moving things around --- crates/search/src/buffer_search.rs | 70 +++++++++++++++++------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a4658c9428..915ede6cb5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -211,47 +211,56 @@ impl View for BufferSearchBar { )) .aligned() .left() - .top(), + .top() + .flex(1., true) + .constrained(), ) - .contained() - .flex(1., true), + .contained(), ) .with_child( - Flex::row() + Flex::column() .align_children_center() .with_child( Flex::row() .with_child( - ChildView::new(&self.query_editor, cx) + Flex::row() + .with_child( + ChildView::new(&self.query_editor, cx) + .aligned() + .left() + .flex(1., true), + ) + .with_children(render_search_option( + supported_options.case, + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + )) + .with_children(render_search_option( + supported_options.word, + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + )) + .contained() + .with_style(editor_container) .aligned() - .left() - .flex(1., true), + .top() + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false), + ) + .with_child( + Flex::row() + .with_child(self.render_action_button("Select All", cx)) + .aligned(), ) - .with_children(render_search_option( - supported_options.case, - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_children(render_search_option( - supported_options.word, - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - )) - .contained() - .with_style(editor_container) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) .flex(1., false), ) - .with_child( - Flex::row() - .with_child(self.render_action_button("Select All", cx)) - .aligned(), - ) + .contained() + .aligned() + .top() .flex(1., false), ) .with_child( @@ -275,6 +284,7 @@ impl View for BufferSearchBar { ) .contained() .with_style(theme.search.container) + .flex_float() .into_any_named("search bar") } } From a583d1c6ccfa8ccaf16bc8bf4d13e275150cc50c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:15:17 +0200 Subject: [PATCH 127/326] Do not display match count when query is empty --- crates/search/src/buffer_search.rs | 44 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 915ede6cb5..b9b09523cc 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -178,6 +178,29 @@ impl View for BufferSearchBar { ) }) }; + let match_count = self + .active_searchable_item + .as_ref() + .and_then(|searchable_item| { + if self.query(cx).is_empty() { + return None; + } + let matches = self + .searchable_items_with_matches + .get(&searchable_item.downgrade())?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, matches.len()) + } else { + "No matches".to_string() + }; + + Some( + Label::new(message, theme.search.match_index.text.clone()) + .contained() + .with_style(theme.search.match_index.container) + .aligned(), + ) + }); Flex::row() .with_child( Flex::column() @@ -190,25 +213,7 @@ impl View for BufferSearchBar { .with_child(self.render_nav_button(">", Direction::Next, cx)) .aligned(), ) - .with_children(self.active_searchable_item.as_ref().and_then( - |searchable_item| { - let matches = self - .searchable_items_with_matches - .get(&searchable_item.downgrade())?; - let message = if let Some(match_ix) = self.active_match_index { - format!("{}/{}", match_ix + 1, matches.len()) - } else { - "No matches".to_string() - }; - - Some( - Label::new(message, theme.search.match_index.text.clone()) - .contained() - .with_style(theme.search.match_index.container) - .aligned(), - ) - }, - )) + .with_children(match_count) .aligned() .left() .top() @@ -765,6 +770,7 @@ impl BufferSearchBar { self.active_match_index.take(); active_searchable_item.clear_matches(cx); let _ = done_tx.send(()); + cx.notify(); } else { let query = if self.current_mode == SearchMode::Regex { match SearchQuery::regex( From e2b8e6ce63ad5a2ac70cd0661860c1e9664b4b88 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:16:24 +0200 Subject: [PATCH 128/326] chore: fix compiler warnings --- crates/search/src/buffer_search.rs | 40 --------------------- crates/semantic_index/src/semantic_index.rs | 2 +- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b9b09523cc..d7add91599 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -468,46 +468,6 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_search_option( - &self, - option_supported: bool, - icon: &'static str, - option: SearchOptions, - cx: &mut ViewContext, - ) -> Option> { - if !option_supported { - return None; - } - - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.search_options.contains(option); - Some( - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_search_option(option, cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - option.bits as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, - ) - .into_any(), - ) - } - fn render_nav_button( &self, icon: &'static str, diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 50b871d454..8c9877b9d3 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -16,7 +16,7 @@ use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; use postage::watch; -use project::{project_settings, search::PathMatcher, Fs, Project, WorktreeId}; +use project::{search::PathMatcher, Fs, Project, WorktreeId}; use smol::channel; use std::{ cmp::Ordering, From f978acdd34085bb0238c6e3307edd1651f3808b8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:29:22 +0200 Subject: [PATCH 129/326] buffer: use shared func for nav button rendering --- crates/search/src/buffer_search.rs | 19 ++++++++++++++++--- crates/search/src/project_search.rs | 25 ++++++++++++------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d7add91599..25149012c7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,5 +1,7 @@ use crate::{ - history::SearchHistory, mode::SearchMode, search_bar::render_search_mode_button, + history::SearchHistory, + mode::SearchMode, + search_bar::{render_nav_button, render_search_mode_button}, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; @@ -201,6 +203,17 @@ impl View for BufferSearchBar { .aligned(), ) }); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + move |_, this, cx| match direction { + Direction::Prev => this.select_prev_match(&Default::default(), cx), + Direction::Next => this.select_next_match(&Default::default(), cx), + }, + cx, + ) + }; Flex::row() .with_child( Flex::column() @@ -209,8 +222,8 @@ impl View for BufferSearchBar { .align_children_center() .with_child( Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) .aligned(), ) .with_children(match_count) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 611fd02846..b607848e21 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1550,19 +1550,18 @@ impl View for ProjectSearchBar { }; let semantic_index = SemanticIndex::enabled(cx) .then(|| search_button_for_mode(SearchMode::Semantic, cx)); - let nav_button_for_direction = - |label, direction, cx: &mut ViewContext| { - render_nav_button( - label, - direction, - move |_, this, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); - } - }, - cx, - ) - }; + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + } + }, + cx, + ) + }; Flex::row() .with_child( From 22f630e985a5b10827de1c61c53bf1671062f376 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:30:45 +0200 Subject: [PATCH 130/326] chore: remove unused function --- crates/search/src/buffer_search.rs | 45 ------------------------------ 1 file changed, 45 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 25149012c7..845d943f4e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -481,51 +481,6 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_nav_button( - &self, - icon: &'static str, - direction: Direction, - cx: &mut ViewContext, - ) -> AnyElement { - let action: Box; - let tooltip; - match direction { - Direction::Prev => { - action = Box::new(SelectPrevMatch); - tooltip = "Select Previous Match"; - } - Direction::Next => { - action = Box::new(SelectNextMatch); - tooltip = "Select Next Match"; - } - }; - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme.search.option_button.inactive_state().style_for(state); - Label::new(icon, style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, { - move |_, this, cx| match direction { - Direction::Prev => this.select_prev_match(&Default::default(), cx), - Direction::Next => this.select_next_match(&Default::default(), cx), - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - direction as usize, - tooltip.to_string(), - Some(action), - tooltip_style, - cx, - ) - .into_any() - } - fn render_action_button( &self, icon: &'static str, From d34da2db697baeb6044530ca4ee8a5c32141e8d4 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 01:37:17 +0200 Subject: [PATCH 131/326] fix dismiss tooltip for project search --- crates/search/src/buffer_search.rs | 1 + crates/search/src/project_search.rs | 1 + crates/search/src/search_bar.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 845d943f4e..36dbe77bb4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -288,6 +288,7 @@ impl View for BufferSearchBar { .with_child(search_button_for_mode(SearchMode::Text, cx)) .with_child(search_button_for_mode(SearchMode::Regex, cx)) .with_child(super::search_bar::render_close_button( + "Dismiss Buffer Search", &theme.search, cx, |_, this, cx| this.dismiss(&Default::default(), cx), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b607848e21..8939279f3b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1626,6 +1626,7 @@ impl View for ProjectSearchBar { .with_children(semantic_index) .with_child(search_button_for_mode(SearchMode::Regex, cx)) .with_child(super::search_bar::render_close_button( + "Dismiss Project Search", &theme.search, cx, |_, this, cx| { diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 446039e960..19d8fa6b98 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -13,12 +13,12 @@ use crate::{ }; pub(super) fn render_close_button( + tooltip: &'static str, theme: &theme::Search, cx: &mut ViewContext, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, dismiss_action: Option>, ) -> AnyElement { - let tooltip = "Dismiss Buffer Search"; let tooltip_style = theme::current(cx).tooltip.clone(); enum CloseButton {} From 2605ae1ef52f38d23405a9469faaae46e5c6196b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Aug 2023 17:49:29 -0700 Subject: [PATCH 132/326] Use Arc::make_mut in ChannelStore Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 15 ++++++--------- crates/util/src/util.rs | 15 --------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 1beb1bc8ea..ec945ce036 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -7,7 +7,6 @@ use futures::Future; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, TypedEnvelope}; -use std::mem; use std::sync::Arc; pub type ChannelId = u64; @@ -319,10 +318,9 @@ impl ChannelStore { .iter_mut() .find(|c| c.id == channel.id) { - util::make_arc_mut(existing_channel, |new_existing_channel| { - new_existing_channel.name = channel.name; - new_existing_channel.user_is_admin = channel.user_is_admin; - }); + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.name = channel.name; + existing_channel.user_is_admin = channel.user_is_admin; continue; } @@ -340,10 +338,9 @@ impl ChannelStore { for channel in payload.channels { if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { - util::make_arc_mut(existing_channel, |new_existing_channel| { - new_existing_channel.name = channel.name; - new_existing_channel.user_is_admin = channel.user_is_admin; - }); + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.name = channel.name; + existing_channel.user_is_admin = channel.user_is_admin; continue; } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 2766cee295..c8beb86aef 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -9,11 +9,9 @@ pub mod test; use std::{ borrow::Cow, cmp::{self, Ordering}, - mem, ops::{AddAssign, Range, RangeInclusive}, panic::Location, pin::Pin, - sync::Arc, task::{Context, Poll}, }; @@ -120,19 +118,6 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se } } -/// Mutates through the arc if no other references exist, -/// otherwise clones the value and swaps out the reference with a new Arc -/// Useful for mutating the elements of a list while using iter_mut() -pub fn make_arc_mut(arc: &mut Arc, mutate: impl FnOnce(&mut T)) { - if let Some(t) = Arc::get_mut(arc) { - mutate(t); - return; - } - let mut new_t = (**arc).clone(); - mutate(&mut new_t); - mem::swap(&mut Arc::new(new_t), arc); -} - pub trait ResultExt { type Ok; From 8b033223fb407de5b3a96fea77d76e62de9bc54a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:28:15 +0200 Subject: [PATCH 133/326] Go back to previous mode on rejection of semantic indexing --- crates/search/src/project_search.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8939279f3b..be40f6ca15 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -685,6 +685,10 @@ impl ProjectSearchView { } fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + let previous_mode = self.current_mode; + if previous_mode == mode { + return; + } self.model.update(cx, |model, _| model.kill_search()); self.current_mode = mode; @@ -722,7 +726,8 @@ impl ProjectSearchView { } else { this.update(&mut cx, |this, cx| { this.semantic_permissioned = Some(false); - this.activate_search_mode(SearchMode::Regex, cx); + debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected"); + this.activate_search_mode(previous_mode, cx); })?; return anyhow::Ok(()); } From 6fbef9f3c7ee1972e8c2b269b0c048fd635c737a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:33:14 +0200 Subject: [PATCH 134/326] chore: update fn paint --- crates/search/src/elements.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/search/src/elements.rs b/crates/search/src/elements.rs index cdc203983f..bd81f4f6a2 100644 --- a/crates/search/src/elements.rs +++ b/crates/search/src/elements.rs @@ -5,7 +5,7 @@ use gpui::geometry::rect::RectF; use gpui::geometry::vector::IntoVector2F; use gpui::json::{self, ToJson}; use gpui::{scene::Path, LayoutContext}; -use gpui::{Element, SceneBuilder, View, ViewContext}; +use gpui::{Element, PaintContext, SceneBuilder, View, ViewContext}; type CreatePath = fn(RectF, Color) -> Path; type AdjustBorder = fn(RectF, f32) -> RectF; @@ -106,7 +106,7 @@ impl Element for ButtonSide { _: RectF, _: &mut Self::LayoutState, _: &mut V, - _: &mut ViewContext, + _: &mut PaintContext, ) -> Self::PaintState { let mut bounds = bounds; if let Some((border_width, border_color)) = self.border.as_ref() { From 28a57662af44989e7e9761ee4bb73eb7f00bfc75 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:47:48 +0200 Subject: [PATCH 135/326] buffer: Add magnifying glass icon --- crates/search/src/buffer_search.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 29cd32c6e5..cffbe91fbe 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -214,6 +214,7 @@ impl View for BufferSearchBar { cx, ) }; + let icon_style = theme.search.editor_icon.clone(); Flex::row() .with_child( Flex::column() @@ -242,6 +243,13 @@ impl View for BufferSearchBar { Flex::row() .with_child( Flex::row() + .align_children_center() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container) + .constrained(), + ) .with_child( ChildView::new(&self.query_editor, cx) .aligned() From 17d505bb7b0dcce4147b4015eba48590a96f518a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 14:25:56 +0200 Subject: [PATCH 136/326] Reset search index on mode change --- crates/search/src/project_search.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 45b50dbe34..52797aa8db 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -695,6 +695,7 @@ impl ProjectSearchView { match mode { SearchMode::Semantic => { let has_permission = self.semantic_permissioned(cx); + self.active_match_index = None; cx.spawn(|this, mut cx| async move { let has_permission = has_permission.await?; @@ -740,11 +741,9 @@ impl ProjectSearchView { anyhow::Ok(()) }).detach_and_log_err(cx); } - SearchMode::Regex => { - self.semantic_state = None; - } - SearchMode::Text => { + SearchMode::Regex | SearchMode::Text => { self.semantic_state = None; + self.active_match_index = None; } } cx.notify(); From a5cb4c6d52dd61efe47925ad6cb2eb299420eee4 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 9 Aug 2023 08:54:24 -0700 Subject: [PATCH 137/326] Fix selections and enter-to-create-file --- crates/collab_ui/src/collab_panel.rs | 61 ++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 85e0d80cce..b3b43dbabe 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -197,7 +197,7 @@ impl CollabPanel { if !query.is_empty() { this.selection.take(); } - this.update_entries(cx); + this.update_entries(false, cx); if !query.is_empty() { this.selection = this .entries @@ -220,7 +220,7 @@ impl CollabPanel { cx.subscribe(&channel_name_editor, |this, _, event, cx| { if let editor::Event::Blurred = event { this.take_editing_state(cx); - this.update_entries(cx); + this.update_entries(false, cx); cx.notify(); } }) @@ -358,7 +358,7 @@ impl CollabPanel { list_state, }; - this.update_entries(cx); + this.update_entries(false, cx); // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); @@ -376,13 +376,18 @@ impl CollabPanel { let active_call = ActiveCall::global(cx); this.subscriptions - .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx))); + .push(cx.observe(&this.user_store, |this, _, cx| { + this.update_entries(false, cx) + })); this.subscriptions - .push(cx.observe(&this.channel_store, |this, _, cx| this.update_entries(cx))); + .push(cx.observe(&this.channel_store, |this, _, cx| { + this.update_entries(false, cx) + })); this.subscriptions - .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); - this.subscriptions - .push(cx.observe_global::(move |this, cx| this.update_entries(cx))); + .push(cx.observe(&active_call, |this, _, cx| this.update_entries(false, cx))); + this.subscriptions.push( + cx.observe_global::(move |this, cx| this.update_entries(false, cx)), + ); this }) @@ -434,7 +439,7 @@ impl CollabPanel { ); } - fn update_entries(&mut self, cx: &mut ViewContext) { + fn update_entries(&mut self, select_editor: bool, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); let query = self.filter_editor.read(cx).text(cx); @@ -743,14 +748,23 @@ impl CollabPanel { } } - if let Some(prev_selected_entry) = prev_selected_entry { - self.selection.take(); + if select_editor { for (ix, entry) in self.entries.iter().enumerate() { - if *entry == prev_selected_entry { + if matches!(*entry, ListEntry::ChannelEditor { .. }) { self.selection = Some(ix); break; } } + } else { + if let Some(prev_selected_entry) = prev_selected_entry { + self.selection.take(); + for (ix, entry) in self.entries.iter().enumerate() { + if *entry == prev_selected_entry { + self.selection = Some(ix); + break; + } + } + } } let old_scroll_top = self.list_state.logical_scroll_top(); @@ -1643,7 +1657,7 @@ impl CollabPanel { }); } - self.update_entries(cx); + self.update_entries(false, cx); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -1724,17 +1738,28 @@ impl CollabPanel { ListEntry::Channel(channel) => { self.join_channel(channel.id, cx); } + ListEntry::ChannelEditor { .. } => { + self.confirm_channel_edit(cx); + } _ => {} } } - } else if let Some((editing_state, channel_name)) = self.take_editing_state(cx) { + } else { + self.confirm_channel_edit(cx); + } + } + + fn confirm_channel_edit(&mut self, cx: &mut ViewContext<'_, '_, CollabPanel>) { + if let Some((editing_state, channel_name)) = self.take_editing_state(cx) { let create_channel = self.channel_store.update(cx, |channel_store, _| { channel_store.create_channel(&channel_name, editing_state.parent_id) }); + self.update_entries(false, cx); + cx.foreground() .spawn(async move { - create_channel.await.ok(); + create_channel.await.log_err(); }) .detach(); } @@ -1746,7 +1771,7 @@ impl CollabPanel { } else { self.collapsed_sections.push(section); } - self.update_entries(cx); + self.update_entries(false, cx); } fn leave_call(cx: &mut ViewContext) { @@ -1771,7 +1796,7 @@ impl CollabPanel { fn new_root_channel(&mut self, cx: &mut ViewContext) { self.channel_editing_state = Some(ChannelEditingState { parent_id: None }); - self.update_entries(cx); + self.update_entries(true, cx); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } @@ -1780,7 +1805,7 @@ impl CollabPanel { self.channel_editing_state = Some(ChannelEditingState { parent_id: Some(action.channel_id), }); - self.update_entries(cx); + self.update_entries(true, cx); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } From c58cf396d3c1b9fe58ace18a66051a54d2ee3115 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:02:21 +0200 Subject: [PATCH 138/326] Add cycle mode command for buffer search --- assets/keymaps/default.json | 4 +++- crates/search/src/buffer_search.rs | 28 +++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 97dad7a831..3b894de723 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -224,7 +224,8 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", - "alt-enter": "search::SelectAllMatches" + "alt-enter": "search::SelectAllMatches", + "alt-tab": "search::CycleMode" } }, { @@ -265,6 +266,7 @@ "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-r": "search::ActivateRegexMode", + "alt-tab": "search::CycleMode", "alt-cmd-f": "project_search::ToggleFilters" } }, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index cffbe91fbe..ec2f1c6855 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,9 +1,9 @@ use crate::{ history::SearchHistory, - mode::SearchMode, + mode::{next_mode, SearchMode}, search_bar::{render_nav_button, render_search_mode_button}, - NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, - SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -51,6 +51,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::handle_editor_cancel); cx.add_action(BufferSearchBar::next_history_query); cx.add_action(BufferSearchBar::previous_history_query); + cx.add_action(BufferSearchBar::cycle_mode); + cx.add_action(BufferSearchBar::cycle_mode_on_pane); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); } @@ -804,6 +806,26 @@ impl BufferSearchBar { let _ = self.search(&new_query, Some(self.search_options), cx); } } + fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { + self.activate_search_mode(next_mode(&self.current_mode, false), cx); + } + fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| { + if bar.show(cx) { + should_propagate = false; + bar.cycle_mode(action, cx); + false + } else { + true + } + }); + } + if should_propagate { + cx.propagate_action(); + } + } } #[cfg(test)] From beffe6f6a9c1bcef9565cc7bbe3eb8eb871c94b0 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 9 Aug 2023 12:44:34 -0400 Subject: [PATCH 139/326] WIP BROKEN --- crates/collab_ui/src/collab_panel.rs | 21 +++++++++++++++------ crates/theme/src/theme.rs | 6 +++--- styles/src/style_tree/collab_panel.ts | 15 ++------------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b3b43dbabe..c934313621 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1103,9 +1103,12 @@ impl CollabPanel { enum AddContact {} let button = match section { Section::ActiveCall => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::::new(0, cx, |state, _| { render_icon_button( - theme.collab_panel.leave_call_button.in_state(is_selected), + theme + .collab_panel + .leave_call_button + .style_for(is_selected, state), "icons/radix/exit.svg", ) }) @@ -1122,9 +1125,12 @@ impl CollabPanel { ), ), Section::Contacts => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::::new(0, cx, |state, _| { render_icon_button( - theme.collab_panel.add_contact_button.in_state(is_selected), + theme + .collab_panel + .add_contact_button + .style_for(is_selected, state), "icons/plus_16.svg", ) }) @@ -1141,9 +1147,12 @@ impl CollabPanel { ), ), Section::Channels => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::::new(0, cx, |state, _| { render_icon_button( - theme.collab_panel.add_contact_button.in_state(is_selected), + theme + .collab_panel + .add_contact_button + .style_for(is_selected, state), "icons/plus_16.svg", ) }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cf8da6233a..1756f91fb8 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -226,9 +226,9 @@ pub struct CollabPanel { pub channel_modal: ChannelModal, pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, - pub leave_call_button: Toggleable, - pub add_contact_button: Toggleable, - pub add_channel_button: Toggleable, + pub leave_call_button: Toggleable>, + pub add_contact_button: Toggleable>, + pub add_channel_button: Toggleable>, pub header_row: ContainedText, pub subheader_row: Toggleable>, pub leave_call: Interactive, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index a859f6d670..fd6e75d9ec 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -8,6 +8,7 @@ import { import { interactive, toggleable } from "../element" import { useTheme } from "../theme" import channel_modal from "./channel_modal" +import { icon_button, toggleable_icon_button } from "../component/icon_button" export default function contacts_panel(): any { @@ -51,19 +52,7 @@ export default function contacts_panel(): any { }, } - const headerButton = toggleable({ - base: { - color: foreground(layer, "on"), - button_width: 28, - icon_width: 16, - }, - state: { - active: { - background: background(layer, "active"), - corner_radius: 8, - } - } - }) + const headerButton = toggleable_icon_button(theme, {}) return { channel_modal: channel_modal(), From 498d043a0af2829431beee59683d2316d47108a0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Aug 2023 10:23:52 -0700 Subject: [PATCH 140/326] Avoid leak of channel store Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index ec945ce036..8fb005a262 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -58,16 +58,20 @@ impl ChannelStore { client.add_message_handler(cx.handle(), Self::handle_update_channels); let mut current_user = user_store.read(cx).watch_current_user(); - let maintain_user = cx.spawn(|this, mut cx| async move { + let maintain_user = cx.spawn_weak(|this, mut cx| async move { while let Some(current_user) = current_user.next().await { if current_user.is_none() { - this.update(&mut cx, |this, cx| { - this.channels.clear(); - this.channel_invitations.clear(); - this.channel_participants.clear(); - this.outgoing_invites.clear(); - cx.notify(); - }); + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.channels.clear(); + this.channel_invitations.clear(); + this.channel_participants.clear(); + this.outgoing_invites.clear(); + cx.notify(); + }); + } else { + break; + } } } }); From 778fd6b0a95e6a44a026d1d0bc3f217170e11c67 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Aug 2023 10:36:27 -0700 Subject: [PATCH 141/326] Represent channel relationships using paths table Co-authored-by: Mikayla --- .../20221109000000_test_schema.sql | 8 +- .../20230727150500_add_channels.sql | 8 +- crates/collab/src/db.rs | 135 +++++++++--------- .../db/{channel_parent.rs => channel_path.rs} | 7 +- 4 files changed, 80 insertions(+), 78 deletions(-) rename crates/collab/src/db/{channel_parent.rs => channel_path.rs} (69%) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 6703f98df2..3dceaecef4 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -192,11 +192,11 @@ CREATE TABLE "channels" ( "created_at" TIMESTAMP NOT NULL DEFAULT now ); -CREATE TABLE "channel_parents" ( - "child_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "parent_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - PRIMARY KEY(child_id, parent_id) +CREATE TABLE "channel_paths" ( + "id_path" TEXT NOT NULL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE ); +CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id"); CREATE TABLE "channel_members" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/crates/collab/migrations/20230727150500_add_channels.sql b/crates/collab/migrations/20230727150500_add_channels.sql index 2d94cb6d97..df981838bf 100644 --- a/crates/collab/migrations/20230727150500_add_channels.sql +++ b/crates/collab/migrations/20230727150500_add_channels.sql @@ -10,11 +10,11 @@ CREATE TABLE "channels" ( "created_at" TIMESTAMP NOT NULL DEFAULT now() ); -CREATE TABLE "channel_parents" ( - "child_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - "parent_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, - PRIMARY KEY(child_id, parent_id) +CREATE TABLE "channel_paths" ( + "id_path" VARCHAR NOT NULL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE ); +CREATE INDEX "index_channel_paths_on_channel_id" ON "channel_paths" ("channel_id"); CREATE TABLE "channel_members" ( "id" SERIAL PRIMARY KEY, diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ed5e7e8e3d..d830938497 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,7 +1,7 @@ mod access_token; mod channel; mod channel_member; -mod channel_parent; +mod channel_path; mod contact; mod follower; mod language_server; @@ -3169,12 +3169,34 @@ impl Database { .insert(&*tx) .await?; + let channel_paths_stmt; if let Some(parent) = parent { - channel_parent::ActiveModel { - child_id: ActiveValue::Set(channel.id), - parent_id: ActiveValue::Set(parent), - } - .insert(&*tx) + let sql = r#" + INSERT INTO channel_paths + (id_path, channel_id) + SELECT + id_path || $1 || '/', $2 + FROM + channel_paths + WHERE + channel_id = $3 + "#; + channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + channel.id.to_proto().into(), + channel.id.to_proto().into(), + parent.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + } else { + channel_path::Entity::insert(channel_path::ActiveModel { + channel_id: ActiveValue::Set(channel.id), + id_path: ActiveValue::Set(format!("/{}/", channel.id)), + }) + .exec(&*tx) .await?; } @@ -3213,9 +3235,9 @@ impl Database { // 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_keep = channel_parent::Entity::find() + let mut channels_to_keep = channel_path::Entity::find() .filter( - channel_parent::Column::ChildId + channel_path::Column::ChannelId .is_in( channels_to_remove .keys() @@ -3223,15 +3245,15 @@ impl Database { .filter(|&id| id != channel_id), ) .and( - channel_parent::Column::ParentId - .is_not_in(channels_to_remove.keys().copied()), + channel_path::Column::IdPath + .not_like(&format!("%/{}/%", channel_id)), ), ) .stream(&*tx) .await?; while let Some(row) = channels_to_keep.next().await { let row = row?; - channels_to_remove.remove(&row.child_id); + channels_to_remove.remove(&row.channel_id); } } @@ -3631,40 +3653,21 @@ impl Database { channel_id: ChannelId, tx: &DatabaseTransaction, ) -> Result> { - let sql = format!( - r#" - WITH RECURSIVE channel_tree(child_id, parent_id) AS ( - SELECT CAST(NULL as INTEGER) as child_id, root_ids.column1 as parent_id - FROM (VALUES ({})) as root_ids - UNION - SELECT channel_parents.child_id, channel_parents.parent_id - FROM channel_parents, channel_tree - WHERE channel_parents.child_id = channel_tree.parent_id - ) - SELECT DISTINCT channel_tree.parent_id - FROM channel_tree - "#, - channel_id - ); - - #[derive(FromQueryResult, Debug, PartialEq)] - pub struct ChannelParent { - pub parent_id: ChannelId, - } - - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); - - let mut channel_ids_stream = channel_parent::Entity::find() - .from_raw_sql(stmt) - .into_model::() - .stream(&*tx) + let paths = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel_id)) + .all(tx) .await?; - - let mut channel_ids = vec![]; - while let Some(channel_id) = channel_ids_stream.next().await { - channel_ids.push(channel_id?.parent_id); + let mut channel_ids = Vec::new(); + for path in paths { + for id in path.id_path.trim_matches('/').split('/') { + if let Ok(id) = id.parse() { + let id = ChannelId::from_proto(id); + if let Err(ix) = channel_ids.binary_search(&id) { + channel_ids.insert(ix, id); + } + } + } } - Ok(channel_ids) } @@ -3687,38 +3690,38 @@ impl Database { let sql = format!( r#" - WITH RECURSIVE channel_tree(child_id, parent_id) AS ( - SELECT root_ids.column1 as child_id, CAST(NULL as INTEGER) as parent_id - FROM (VALUES {values}) as root_ids - UNION - SELECT channel_parents.child_id, channel_parents.parent_id - FROM channel_parents, channel_tree - WHERE channel_parents.parent_id = channel_tree.child_id - ) - SELECT channel_tree.child_id, channel_tree.parent_id - FROM channel_tree - ORDER BY child_id, parent_id IS NOT NULL - "#, + 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 || '%') + "# ); - #[derive(FromQueryResult, Debug, PartialEq)] - pub struct ChannelParent { - pub child_id: ChannelId, - pub parent_id: Option, - } - let stmt = Statement::from_string(self.pool.get_database_backend(), sql); let mut parents_by_child_id = HashMap::default(); - let mut parents = channel_parent::Entity::find() + let mut paths = channel_path::Entity::find() .from_raw_sql(stmt) - .into_model::() .stream(tx) .await?; - while let Some(parent) = parents.next().await { - let parent = parent?; - parents_by_child_id.insert(parent.child_id, parent.parent_id); + 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); + } + } + parents_by_child_id.insert(path.channel_id, parent_id); } Ok(parents_by_child_id) diff --git a/crates/collab/src/db/channel_parent.rs b/crates/collab/src/db/channel_path.rs similarity index 69% rename from crates/collab/src/db/channel_parent.rs rename to crates/collab/src/db/channel_path.rs index b0072155a3..08ecbddb56 100644 --- a/crates/collab/src/db/channel_parent.rs +++ b/crates/collab/src/db/channel_path.rs @@ -2,12 +2,11 @@ use super::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "channel_parents")] +#[sea_orm(table_name = "channel_paths")] pub struct Model { #[sea_orm(primary_key)] - pub child_id: ChannelId, - #[sea_orm(primary_key)] - pub parent_id: ChannelId, + pub id_path: String, + pub channel_id: ChannelId, } impl ActiveModelBehavior for ActiveModel {} From eed49a88bd9933bf61c100f3da46a0abe0285e0c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 9 Aug 2023 11:04:09 -0700 Subject: [PATCH 142/326] Fix bad merge --- crates/collab/src/tests.rs | 49 ++------------------------------------ 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index c669e1da40..31d7b629f8 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -13,10 +13,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{ - elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, Task, TestAppContext, - View, ViewContext, ViewHandle, WeakViewHandle, -}; +use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; use language::LanguageRegistry; use parking_lot::Mutex; use project::{Project, WorktreeId}; @@ -541,50 +538,8 @@ impl TestClient { &self, project: &ModelHandle, cx: &mut TestAppContext, - // <<<<<<< HEAD - // ) -> ViewHandle { - // struct WorkspaceContainer { - // workspace: Option>, - // } - - // impl Entity for WorkspaceContainer { - // type Event = (); - // } - - // impl View for WorkspaceContainer { - // fn ui_name() -> &'static str { - // "WorkspaceContainer" - // } - - // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - // if let Some(workspace) = self - // .workspace - // .as_ref() - // .and_then(|workspace| workspace.upgrade(cx)) - // { - // ChildView::new(&workspace, cx).into_any() - // } else { - // Empty::new().into_any() - // } - // } - // } - - // // We use a workspace container so that we don't need to remove the window in order to - // // drop the workspace and we can use a ViewHandle instead. - // let window = cx.add_window(|_| WorkspaceContainer { workspace: None }); - // let container = window.root(cx); - // let workspace = window.add_view(cx, |cx| { - // Workspace::new(0, project.clone(), self.app_state.clone(), cx) - // }); - // container.update(cx, |container, cx| { - // container.workspace = Some(workspace.downgrade()); - // cx.notify(); - // }); - // workspace - // ======= ) -> WindowHandle { - cx.add_window(|cx| Workspace::test_new(project.clone(), cx)) - // >>>>>>> main + cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } } From a3623ec2b84ea09f7901faf1d52f599c29884618 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 9 Aug 2023 12:20:48 -0700 Subject: [PATCH 143/326] Add renames co-authored-by: max --- crates/client/src/channel_store.rs | 51 +++++--- crates/client/src/channel_store_tests.rs | 6 +- crates/collab/src/db.rs | 49 ++++++-- crates/collab/src/db/tests.rs | 75 +++++++++--- crates/collab/src/rpc.rs | 58 ++++++--- crates/collab/src/tests/channel_tests.rs | 78 ++++++++++--- crates/collab_ui/src/collab_panel.rs | 142 +++++++++++++++++------ crates/rpc/proto/zed.proto | 15 ++- crates/rpc/src/proto.rs | 2 + 9 files changed, 356 insertions(+), 120 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 8fb005a262..b9aa4268cd 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -16,6 +16,7 @@ pub struct ChannelStore { channels: Vec>, channel_invitations: Vec>, channel_participants: HashMap>>, + channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, client: Arc, user_store: ModelHandle, @@ -28,7 +29,6 @@ pub struct Channel { pub id: ChannelId, pub name: String, pub parent_id: Option, - pub user_is_admin: bool, pub depth: usize, } @@ -79,6 +79,7 @@ impl ChannelStore { channels: vec![], channel_invitations: vec![], channel_participants: Default::default(), + channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), client, user_store, @@ -100,17 +101,18 @@ impl ChannelStore { } pub fn is_user_admin(&self, mut channel_id: ChannelId) -> bool { - while let Some(channel) = self.channel_for_id(channel_id) { - if channel.user_is_admin { + loop { + if self.channels_with_admin_privileges.contains(&channel_id) { return true; } - if let Some(parent_id) = channel.parent_id { - channel_id = parent_id; - } else { - break; + if let Some(channel) = self.channel_for_id(channel_id) { + if let Some(parent_id) = channel.parent_id { + channel_id = parent_id; + continue; + } } + return false; } - false } pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { @@ -228,6 +230,22 @@ impl ChannelStore { }) } + pub fn rename( + &mut self, + channel_id: ChannelId, + new_name: &str, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + let name = new_name.to_string(); + cx.spawn(|_this, _cx| async move { + client + .request(proto::RenameChannel { channel_id, name }) + .await?; + Ok(()) + }) + } + pub fn respond_to_channel_invite( &mut self, channel_id: ChannelId, @@ -315,6 +333,8 @@ impl ChannelStore { .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); self.channel_participants .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); + self.channels_with_admin_privileges + .retain(|channel_id| !payload.remove_channels.contains(channel_id)); for channel in payload.channel_invitations { if let Some(existing_channel) = self @@ -324,7 +344,6 @@ impl ChannelStore { { let existing_channel = Arc::make_mut(existing_channel); existing_channel.name = channel.name; - existing_channel.user_is_admin = channel.user_is_admin; continue; } @@ -333,7 +352,6 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, - user_is_admin: false, parent_id: None, depth: 0, }), @@ -344,7 +362,6 @@ impl ChannelStore { if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { let existing_channel = Arc::make_mut(existing_channel); existing_channel.name = channel.name; - existing_channel.user_is_admin = channel.user_is_admin; continue; } @@ -357,7 +374,6 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, - user_is_admin: channel.user_is_admin, parent_id: Some(parent_id), depth, }), @@ -369,7 +385,6 @@ impl ChannelStore { Arc::new(Channel { id: channel.id, name: channel.name, - user_is_admin: channel.user_is_admin, parent_id: None, depth: 0, }), @@ -377,6 +392,16 @@ impl ChannelStore { } } + for permission in payload.channel_permissions { + if permission.is_admin { + self.channels_with_admin_privileges + .insert(permission.channel_id); + } else { + self.channels_with_admin_privileges + .remove(&permission.channel_id); + } + } + let mut all_user_ids = Vec::new(); let channel_participants = payload.channel_participants; for entry in &channel_participants { diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs index 69d5fed70d..4ee54d3eca 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/client/src/channel_store_tests.rs @@ -18,13 +18,11 @@ fn test_update_channels(cx: &mut AppContext) { id: 1, name: "b".to_string(), parent_id: None, - user_is_admin: true, }, proto::Channel { id: 2, name: "a".to_string(), parent_id: None, - user_is_admin: false, }, ], ..Default::default() @@ -49,13 +47,11 @@ fn test_update_channels(cx: &mut AppContext) { id: 3, name: "x".to_string(), parent_id: Some(1), - user_is_admin: false, }, proto::Channel { id: 4, name: "y".to_string(), parent_id: Some(2), - user_is_admin: false, }, ], ..Default::default() @@ -92,7 +88,7 @@ fn assert_channels( let actual = store .channels() .iter() - .map(|c| (c.depth, c.name.as_str(), c.user_is_admin)) + .map(|c| (c.depth, c.name.as_str(), store.is_user_admin(c.id))) .collect::>(); assert_eq!(actual, expected_channels); }); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d830938497..8faea0e402 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3155,7 +3155,7 @@ impl Database { live_kit_room: &str, creator_id: UserId, ) -> Result { - let name = name.trim().trim_start_matches('#'); + let name = Self::sanitize_channel_name(name)?; self.transaction(move |tx| async move { if let Some(parent) = parent { self.check_user_is_channel_admin(parent, creator_id, &*tx) @@ -3303,6 +3303,39 @@ impl Database { .await } + fn sanitize_channel_name(name: &str) -> Result<&str> { + let new_name = name.trim().trim_start_matches('#'); + if new_name == "" { + Err(anyhow!("channel name can't be blank"))?; + } + Ok(new_name) + } + + pub async fn rename_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + new_name: &str, + ) -> 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 { + id: ActiveValue::Unchanged(channel_id), + name: ActiveValue::Set(new_name.clone()), + ..Default::default() + } + .update(&*tx) + .await?; + + Ok(new_name) + }) + .await + } + pub async fn respond_to_channel_invite( &self, channel_id: ChannelId, @@ -3400,7 +3433,6 @@ impl Database { .map(|channel| Channel { id: channel.id, name: channel.name, - user_is_admin: false, parent_id: None, }) .collect(); @@ -3426,10 +3458,6 @@ impl Database { .all(&*tx) .await?; - let admin_channel_ids = channel_memberships - .iter() - .filter_map(|m| m.admin.then_some(m.channel_id)) - .collect::>(); let parents_by_child_id = self .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; @@ -3445,7 +3473,6 @@ impl Database { channels.push(Channel { id: row.id, name: row.name, - user_is_admin: admin_channel_ids.contains(&row.id), parent_id: parents_by_child_id.get(&row.id).copied().flatten(), }); } @@ -3758,15 +3785,14 @@ impl Database { .one(&*tx) .await?; - let (user_is_admin, is_accepted) = channel_membership - .map(|membership| (membership.admin, membership.accepted)) - .unwrap_or((false, false)); + let is_accepted = channel_membership + .map(|membership| membership.accepted) + .unwrap_or(false); Ok(Some(( Channel { id: channel.id, name: channel.name, - user_is_admin, parent_id: None, }, is_accepted, @@ -4043,7 +4069,6 @@ pub struct NewUserResult { pub struct Channel { pub id: ChannelId, pub name: String, - pub user_is_admin: bool, pub parent_id: Option, } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index cdcde3332c..a659f3d164 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -962,43 +962,36 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { id: zed_id, name: "zed".to_string(), parent_id: None, - user_is_admin: true, }, Channel { id: crdb_id, name: "crdb".to_string(), parent_id: Some(zed_id), - user_is_admin: true, }, Channel { id: livestreaming_id, name: "livestreaming".to_string(), parent_id: Some(zed_id), - user_is_admin: true, }, Channel { id: replace_id, name: "replace".to_string(), parent_id: Some(zed_id), - user_is_admin: true, }, Channel { id: rust_id, name: "rust".to_string(), parent_id: None, - user_is_admin: true, }, Channel { id: cargo_id, name: "cargo".to_string(), parent_id: Some(rust_id), - user_is_admin: true, }, Channel { id: cargo_ra_id, name: "cargo-ra".to_string(), parent_id: Some(cargo_id), - user_is_admin: true, } ] ); @@ -1011,25 +1004,21 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { id: zed_id, name: "zed".to_string(), parent_id: None, - user_is_admin: false, }, Channel { id: crdb_id, name: "crdb".to_string(), parent_id: Some(zed_id), - user_is_admin: false, }, Channel { id: livestreaming_id, name: "livestreaming".to_string(), parent_id: Some(zed_id), - user_is_admin: false, }, Channel { id: replace_id, name: "replace".to_string(), parent_id: Some(zed_id), - user_is_admin: false, }, ] ); @@ -1048,25 +1037,21 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { id: zed_id, name: "zed".to_string(), parent_id: None, - user_is_admin: true, }, Channel { id: crdb_id, name: "crdb".to_string(), parent_id: Some(zed_id), - user_is_admin: false, }, Channel { id: livestreaming_id, name: "livestreaming".to_string(), parent_id: Some(zed_id), - user_is_admin: false, }, Channel { id: replace_id, name: "replace".to_string(), parent_id: Some(zed_id), - user_is_admin: false, }, ] ); @@ -1296,6 +1281,66 @@ test_both_dbs!( } ); +test_both_dbs!( + test_channel_renames_postgres, + test_channel_renames_sqlite, + db, + { + 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 zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + + db.rename_channel(zed_id, user_1, "#zed-archive") + .await + .unwrap(); + + let zed_archive_id = zed_id; + + let (channel, _) = db + .get_channel(zed_archive_id, user_1) + .await + .unwrap() + .unwrap(); + assert_eq!(channel.name, "zed-archive"); + + let non_permissioned_rename = db + .rename_channel(zed_archive_id, user_2, "hacked-lol") + .await; + assert!(non_permissioned_rename.is_err()); + + let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; + assert!(bad_name_rename.is_err()) + } +); + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index a24db6be81..0f52c8c03a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -247,6 +247,7 @@ impl Server { .add_request_handler(invite_channel_member) .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) + .add_request_handler(rename_channel) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -2151,7 +2152,6 @@ async fn create_channel( id: id.to_proto(), name: request.name, parent_id: request.parent_id, - user_is_admin: false, }); let user_ids_to_notify = if let Some(parent_id) = parent_id { @@ -2165,7 +2165,10 @@ async fn create_channel( for connection_id in connection_pool.user_connection_ids(user_id) { let mut update = update.clone(); if user_id == session.user_id { - update.channels[0].user_is_admin = true; + update.channel_permissions.push(proto::ChannelPermission { + channel_id: id.to_proto(), + is_admin: true, + }); } session.peer.send(connection_id, update)?; } @@ -2224,7 +2227,6 @@ async fn invite_channel_member( id: channel.id.to_proto(), name: channel.name, parent_id: None, - user_is_admin: false, }); for connection_id in session .connection_pool() @@ -2283,18 +2285,9 @@ async fn set_channel_member_admin( let mut update = proto::UpdateChannels::default(); if has_accepted { - update.channels.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - parent_id: None, - user_is_admin: request.admin, - }); - } else { - update.channel_invitations.push(proto::Channel { - id: channel.id.to_proto(), - name: channel.name, - parent_id: None, - user_is_admin: request.admin, + update.channel_permissions.push(proto::ChannelPermission { + channel_id: channel.id.to_proto(), + is_admin: request.admin, }); } @@ -2310,6 +2303,38 @@ async fn set_channel_member_admin( Ok(()) } +async fn rename_channel( + request: proto::RenameChannel, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + let new_name = db + .rename_channel(channel_id, session.user_id, &request.name) + .await?; + + response.send(proto::Ack {})?; + + let mut update = proto::UpdateChannels::default(); + update.channels.push(proto::Channel { + id: request.channel_id, + name: new_name, + parent_id: None, + }); + + 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())?; + } + } + + Ok(()) +} + async fn get_channel_members( request: proto::GetChannelMembers, response: Response, @@ -2345,7 +2370,6 @@ async fn respond_to_channel_invite( .extend(channels.into_iter().map(|channel| proto::Channel { id: channel.id.to_proto(), name: channel.name, - user_is_admin: channel.user_is_admin, parent_id: channel.parent_id.map(ChannelId::to_proto), })); update @@ -2505,7 +2529,6 @@ fn build_initial_channels_update( update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - user_is_admin: channel.user_is_admin, parent_id: channel.parent_id.map(|id| id.to_proto()), }); } @@ -2523,7 +2546,6 @@ fn build_initial_channels_update( update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, - user_is_admin: false, parent_id: None, }); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 9723b18394..b2e9cae08a 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -40,14 +40,12 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: true, depth: 0, }), Arc::new(Channel { id: channel_b_id, name: "channel-b".to_string(), parent_id: Some(channel_a_id), - user_is_admin: true, depth: 1, }) ] @@ -82,7 +80,6 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: false, depth: 0, })] ) @@ -131,14 +128,13 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: false, + depth: 0, }), Arc::new(Channel { id: channel_b_id, name: "channel-b".to_string(), parent_id: Some(channel_a_id), - user_is_admin: false, depth: 1, }) ] @@ -162,21 +158,18 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: false, depth: 0, }), Arc::new(Channel { id: channel_b_id, name: "channel-b".to_string(), parent_id: Some(channel_a_id), - user_is_admin: false, depth: 1, }), Arc::new(Channel { id: channel_c_id, name: "channel-c".to_string(), parent_id: Some(channel_b_id), - user_is_admin: false, depth: 2, }), ] @@ -204,21 +197,18 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: true, depth: 0, }), Arc::new(Channel { id: channel_b_id, name: "channel-b".to_string(), parent_id: Some(channel_a_id), - user_is_admin: false, depth: 1, }), Arc::new(Channel { id: channel_c_id, name: "channel-c".to_string(), parent_id: Some(channel_b_id), - user_is_admin: false, depth: 2, }), ] @@ -244,7 +234,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: true, + depth: 0, })] ) @@ -256,7 +246,7 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: true, + depth: 0, })] ) @@ -281,7 +271,6 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - user_is_admin: true, depth: 0, })] ) @@ -395,7 +384,6 @@ async fn test_channel_room( id: zed_id, name: "zed".to_string(), parent_id: None, - user_is_admin: false, depth: 0, })] ) @@ -617,7 +605,7 @@ async fn test_permissions_update_while_invited( id: rust_id, name: "rust".to_string(), parent_id: None, - user_is_admin: false, + depth: 0, })], ); @@ -643,7 +631,7 @@ async fn test_permissions_update_while_invited( id: rust_id, name: "rust".to_string(), parent_id: None, - user_is_admin: true, + depth: 0, })], ); @@ -651,3 +639,59 @@ async fn test_permissions_update_while_invited( assert_eq!(channels.channels(), &[],); }); } + +#[gpui::test] +async fn test_channel_rename( + 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 rust_id = server + .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + // Rename the channel + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.rename(rust_id, "#rust-archive", cx) + }) + .await + .unwrap(); + + let rust_archive_id = rust_id; + deterministic.run_until_parked(); + + // Client A sees the channel with its new name. + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: rust_archive_id, + name: "rust-archive".to_string(), + parent_id: None, + + depth: 0, + })], + ); + }); + + // Client B sees the channel with its new name. + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: rust_archive_id, + name: "rust-archive".to_string(), + parent_id: None, + + depth: 0, + })], + ); + }); +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index dd2a0db243..7bf2290622 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -64,11 +64,22 @@ struct ManageMembers { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct RenameChannel { + channel_id: u64, +} + actions!(collab_panel, [ToggleFocus, Remove, Secondary]); impl_actions!( collab_panel, - [RemoveChannel, NewChannel, InviteMembers, ManageMembers] + [ + RemoveChannel, + NewChannel, + InviteMembers, + ManageMembers, + RenameChannel + ] ); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; @@ -83,16 +94,19 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); cx.add_action(CollabPanel::remove); - cx.add_action(CollabPanel::remove_channel_action); + cx.add_action(CollabPanel::remove_selected_channel); cx.add_action(CollabPanel::show_inline_context_menu); cx.add_action(CollabPanel::new_subchannel); cx.add_action(CollabPanel::invite_members); cx.add_action(CollabPanel::manage_members); + cx.add_action(CollabPanel::rename_selected_channel); + cx.add_action(CollabPanel::rename_channel); } -#[derive(Debug, Default)] -pub struct ChannelEditingState { - parent_id: Option, +#[derive(Debug)] +pub enum ChannelEditingState { + Create { parent_id: Option }, + Rename { channel_id: u64 }, } pub struct CollabPanel { @@ -581,19 +595,32 @@ impl CollabPanel { executor.clone(), )); if let Some(state) = &self.channel_editing_state { - if state.parent_id.is_none() { + if matches!(state, ChannelEditingState::Create { parent_id: None }) { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } for mat in matches { let channel = &channels[mat.candidate_id]; - self.entries.push(ListEntry::Channel(channel.clone())); - if let Some(state) = &self.channel_editing_state { - if state.parent_id == Some(channel.id) { + + match &self.channel_editing_state { + Some(ChannelEditingState::Create { parent_id }) + if *parent_id == Some(channel.id) => + { + self.entries.push(ListEntry::Channel(channel.clone())); self.entries.push(ListEntry::ChannelEditor { depth: channel.depth + 1, }); } + Some(ChannelEditingState::Rename { channel_id }) + if *channel_id == channel.id => + { + self.entries.push(ListEntry::ChannelEditor { + depth: channel.depth + 1, + }); + } + _ => { + self.entries.push(ListEntry::Channel(channel.clone())); + } } } } @@ -1065,15 +1092,15 @@ impl CollabPanel { &mut self, cx: &mut ViewContext, ) -> Option<(ChannelEditingState, String)> { - let result = self - .channel_editing_state - .take() - .map(|state| (state, self.channel_name_editor.read(cx).text(cx))); - - self.channel_name_editor - .update(cx, |editor, cx| editor.set_text("", cx)); - - result + if let Some(state) = self.channel_editing_state.take() { + self.channel_name_editor.update(cx, |editor, cx| { + let name = editor.text(cx); + editor.set_text("", cx); + Some((state, name)) + }) + } else { + None + } } fn render_header( @@ -1646,6 +1673,7 @@ impl CollabPanel { ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), ContextMenuItem::action("Manage members", ManageMembers { channel_id }), ContextMenuItem::action("Invite members", InviteMembers { channel_id }), + ContextMenuItem::action("Rename Channel", RenameChannel { channel_id }), ], cx, ); @@ -1702,6 +1730,10 @@ impl CollabPanel { } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if self.confirm_channel_edit(cx) { + return; + } + if let Some(selection) = self.selection { if let Some(entry) = self.entries.get(selection) { match entry { @@ -1747,30 +1779,38 @@ impl CollabPanel { ListEntry::Channel(channel) => { self.join_channel(channel.id, cx); } - ListEntry::ChannelEditor { .. } => { - self.confirm_channel_edit(cx); - } _ => {} } } - } else { - self.confirm_channel_edit(cx); } } - fn confirm_channel_edit(&mut self, cx: &mut ViewContext<'_, '_, CollabPanel>) { + fn confirm_channel_edit(&mut self, cx: &mut ViewContext<'_, '_, CollabPanel>) -> bool { if let Some((editing_state, channel_name)) = self.take_editing_state(cx) { - let create_channel = self.channel_store.update(cx, |channel_store, _| { - channel_store.create_channel(&channel_name, editing_state.parent_id) - }); - + match editing_state { + ChannelEditingState::Create { parent_id } => { + let request = self.channel_store.update(cx, |channel_store, _| { + channel_store.create_channel(&channel_name, parent_id) + }); + cx.foreground() + .spawn(async move { + request.await?; + anyhow::Ok(()) + }) + .detach(); + } + ChannelEditingState::Rename { channel_id } => { + self.channel_store + .update(cx, |channel_store, cx| { + channel_store.rename(channel_id, &channel_name, cx) + }) + .detach(); + } + } self.update_entries(false, cx); - - cx.foreground() - .spawn(async move { - create_channel.await.log_err(); - }) - .detach(); + true + } else { + false } } @@ -1804,14 +1844,14 @@ impl CollabPanel { } fn new_root_channel(&mut self, cx: &mut ViewContext) { - self.channel_editing_state = Some(ChannelEditingState { parent_id: None }); + self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: None }); self.update_entries(true, cx); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { - self.channel_editing_state = Some(ChannelEditingState { + self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), }); self.update_entries(true, cx); @@ -1835,7 +1875,33 @@ impl CollabPanel { } } - fn rename(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) {} + fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.rename_channel( + &RenameChannel { + channel_id: channel.id, + }, + cx, + ); + } + } + + fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { + if let Some(channel) = self + .channel_store + .read(cx) + .channel_for_id(action.channel_id) + { + self.channel_editing_state = Some(ChannelEditingState::Rename { + channel_id: action.channel_id, + }); + self.channel_name_editor.update(cx, |editor, cx| { + editor.set_text(channel.name.clone(), cx); + editor.select_all(&Default::default(), cx); + }); + self.update_entries(true, cx); + } + } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { @@ -1887,7 +1953,7 @@ impl CollabPanel { .detach(); } - fn remove_channel_action(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { + fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { self.remove_channel(action.channel_id, cx) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8f187a87c6..13b4c60aad 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -141,6 +141,7 @@ message Envelope { GetChannelMembers get_channel_members = 128; GetChannelMembersResponse get_channel_members_response = 129; SetChannelMemberAdmin set_channel_member_admin = 130; + RenameChannel rename_channel = 131; } } @@ -874,6 +875,12 @@ message UpdateChannels { repeated Channel channel_invitations = 3; repeated uint64 remove_channel_invitations = 4; repeated ChannelParticipants channel_participants = 5; + repeated ChannelPermission channel_permissions = 6; +} + +message ChannelPermission { + uint64 channel_id = 1; + bool is_admin = 2; } message ChannelParticipants { @@ -935,6 +942,11 @@ message SetChannelMemberAdmin { bool admin = 3; } +message RenameChannel { + uint64 channel_id = 1; + string name = 2; +} + message RespondToChannelInvite { uint64 channel_id = 1; bool accept = 2; @@ -1303,8 +1315,7 @@ message Nonce { message Channel { uint64 id = 1; string name = 2; - bool user_is_admin = 3; - optional uint64 parent_id = 4; + optional uint64 parent_id = 3; } message Contact { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index fac011f803..d3a3091131 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -217,6 +217,7 @@ messages!( (JoinChannel, Foreground), (RoomUpdated, Foreground), (SaveBuffer, Foreground), + (RenameChannel, Foreground), (SetChannelMemberAdmin, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), @@ -304,6 +305,7 @@ request_messages!( (JoinChannel, JoinRoomResponse), (RemoveChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), + (RenameChannel, Ack), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (ShareProject, ShareProjectResponse), From 60e25d780a7c54ebbf353044634f71e9f73db63f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Aug 2023 13:43:16 -0700 Subject: [PATCH 144/326] Send channel permissions to clients when they fetch their channels --- crates/client/src/channel_store_tests.rs | 9 ++++-- crates/collab/src/db.rs | 30 ++++++++++++------- crates/collab/src/db/tests.rs | 12 ++++---- crates/collab/src/rpc.rs | 38 +++++++++++++++++------- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs index 4ee54d3eca..f74169eb2a 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/client/src/channel_store_tests.rs @@ -1,6 +1,5 @@ -use util::http::FakeHttpClient; - use super::*; +use util::http::FakeHttpClient; #[gpui::test] fn test_update_channels(cx: &mut AppContext) { @@ -25,6 +24,10 @@ fn test_update_channels(cx: &mut AppContext) { parent_id: None, }, ], + channel_permissions: vec![proto::ChannelPermission { + channel_id: 1, + is_admin: true, + }], ..Default::default() }, cx, @@ -64,7 +67,7 @@ fn test_update_channels(cx: &mut AppContext) { (0, "a", false), (1, "y", false), (0, "b", true), - (1, "x", false), + (1, "x", true), ], cx, ); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 8faea0e402..b7718be118 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3442,10 +3442,7 @@ impl Database { .await } - pub async fn get_channels_for_user( - &self, - user_id: UserId, - ) -> Result<(Vec, HashMap>)> { + pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -3462,6 +3459,11 @@ impl Database { .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) .await?; + let channels_with_admin_privileges = channel_memberships + .iter() + .filter_map(|membership| membership.admin.then_some(membership.channel_id)) + .collect(); + let mut channels = Vec::with_capacity(parents_by_child_id.len()); { let mut rows = channel::Entity::find() @@ -3484,7 +3486,7 @@ impl Database { UserId, } - let mut participants_by_channel: HashMap> = HashMap::default(); + let mut channel_participants: HashMap> = HashMap::default(); { let mut rows = room_participant::Entity::find() .inner_join(room::Entity) @@ -3497,14 +3499,15 @@ impl Database { .await?; while let Some(row) = rows.next().await { let row: (ChannelId, UserId) = row?; - participants_by_channel - .entry(row.0) - .or_default() - .push(row.1) + channel_participants.entry(row.0).or_default().push(row.1) } } - Ok((channels, participants_by_channel)) + Ok(ChannelsForUser { + channels, + channel_participants, + channels_with_admin_privileges, + }) }) .await } @@ -4072,6 +4075,13 @@ pub struct Channel { pub parent_id: Option, } +#[derive(Debug, PartialEq)] +pub struct ChannelsForUser { + pub channels: Vec, + pub channel_participants: HashMap>, + pub channels_with_admin_privileges: HashSet, +} + fn random_invite_code() -> String { nanoid::nanoid!(16) } diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index a659f3d164..2680d81aac 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -954,9 +954,9 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { .await .unwrap(); - let (channels, _) = db.get_channels_for_user(a_id).await.unwrap(); + let result = db.get_channels_for_user(a_id).await.unwrap(); assert_eq!( - channels, + result.channels, vec![ Channel { id: zed_id, @@ -996,9 +996,9 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { ] ); - let (channels, _) = db.get_channels_for_user(b_id).await.unwrap(); + let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( - channels, + result.channels, vec![ Channel { id: zed_id, @@ -1029,9 +1029,9 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await; assert!(set_channel_admin.is_ok()); - let (channels, _) = db.get_channels_for_user(b_id).await.unwrap(); + let result = db.get_channels_for_user(b_id).await.unwrap(); assert_eq!( - channels, + result.channels, vec![ Channel { id: zed_id, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0f52c8c03a..07d343959f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -529,7 +529,7 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, invite_code, (channels, channel_participants), channel_invites) = future::try_join4( + let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4( this.app_state.db.get_contacts(user_id), this.app_state.db.get_invite_code_for_user(user_id), this.app_state.db.get_channels_for_user(user_id), @@ -540,7 +540,11 @@ impl Server { let mut pool = this.connection_pool.lock(); pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; - this.peer.send(connection_id, build_initial_channels_update(channels, channel_participants, channel_invites))?; + this.peer.send(connection_id, build_initial_channels_update( + channels_for_user.channels, + channels_for_user.channel_participants, + channel_invites + ))?; if let Some((code, count)) = invite_code { this.peer.send(connection_id, proto::UpdateInviteInfo { @@ -2364,22 +2368,36 @@ async fn respond_to_channel_invite( .remove_channel_invitations .push(channel_id.to_proto()); if request.accept { - let (channels, participants) = db.get_channels_for_user(session.user_id).await?; + let result = db.get_channels_for_user(session.user_id).await?; update .channels - .extend(channels.into_iter().map(|channel| proto::Channel { + .extend(result.channels.into_iter().map(|channel| proto::Channel { id: channel.id.to_proto(), name: channel.name, parent_id: channel.parent_id.map(ChannelId::to_proto), })); update .channel_participants - .extend(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(), - } - })); + .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(), + is_admin: true, + }), + ); } session.peer.send(session.connection_id, update)?; response.send(proto::Ack {})?; From ac1b2b18aaeb5cc979849afb91154c6bbf9f940e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Aug 2023 14:40:47 -0700 Subject: [PATCH 145/326] Send user ids of channels of which they are admins on connecting Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 14 ++++---- crates/collab/src/rpc.rs | 24 ++++++++++---- crates/collab/src/tests/channel_tests.rs | 41 +++++++++++++++++++++--- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index b9aa4268cd..6325bc1a30 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -1,3 +1,4 @@ +use crate::Status; use crate::{Client, Subscription, User, UserStore}; use anyhow::anyhow; use anyhow::Result; @@ -21,7 +22,7 @@ pub struct ChannelStore { client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, - _maintain_user: Task<()>, + _watch_connection_status: Task<()>, } #[derive(Clone, Debug, PartialEq)] @@ -57,15 +58,16 @@ impl ChannelStore { let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_channels); - let mut current_user = user_store.read(cx).watch_current_user(); - let maintain_user = cx.spawn_weak(|this, mut cx| async move { - while let Some(current_user) = current_user.next().await { - if current_user.is_none() { + 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 { + if matches!(status, Status::ConnectionLost | Status::SignedOut) { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { this.channels.clear(); this.channel_invitations.clear(); this.channel_participants.clear(); + this.channels_with_admin_privileges.clear(); this.outgoing_invites.clear(); cx.notify(); }); @@ -84,7 +86,7 @@ impl ChannelStore { client, user_store, _rpc_subscription: rpc_subscription, - _maintain_user: maintain_user, + _watch_connection_status: watch_connection_status, } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 07d343959f..c2f0d31f90 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,7 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, executor::Executor, AppState, Result, }; @@ -541,8 +541,7 @@ impl Server { pool.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_channels_update( - channels_for_user.channels, - channels_for_user.channel_participants, + channels_for_user, channel_invites ))?; @@ -2537,13 +2536,12 @@ fn to_tungstenite_message(message: AxumMessage) -> TungsteniteMessage { } fn build_initial_channels_update( - channels: Vec, - channel_participants: HashMap>, + channels: ChannelsForUser, channel_invites: Vec, ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); - for channel in channels { + for channel in channels.channels { update.channels.push(proto::Channel { id: channel.id.to_proto(), name: channel.name, @@ -2551,7 +2549,7 @@ fn build_initial_channels_update( }); } - for (channel_id, participants) in channel_participants { + for (channel_id, participants) in channels.channel_participants { update .channel_participants .push(proto::ChannelParticipants { @@ -2560,6 +2558,18 @@ fn build_initial_channels_update( }); } + update + .channel_permissions + .extend( + channels + .channels_with_admin_privileges + .into_iter() + .map(|id| proto::ChannelPermission { + channel_id: id.to_proto(), + is_admin: true, + }), + ); + for channel in channel_invites { update.channel_invitations.push(proto::Channel { id: channel.id.to_proto(), diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index b2e9cae08a..63fab0d5f8 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1,8 +1,11 @@ -use crate::tests::{room_participants, RoomParticipants, TestServer}; +use crate::{ + rpc::RECONNECT_TIMEOUT, + tests::{room_participants, RoomParticipants, TestServer}, +}; use call::ActiveCall; use client::{Channel, ChannelMembership, User}; use gpui::{executor::Deterministic, TestAppContext}; -use rpc::proto; +use rpc::{proto, RECEIVE_TIMEOUT}; use std::sync::Arc; #[gpui::test] @@ -49,7 +52,9 @@ async fn test_core_channels( depth: 1, }) ] - ) + ); + assert!(channels.is_user_admin(channel_a_id)); + assert!(channels.is_user_admin(channel_b_id)); }); client_b @@ -84,6 +89,7 @@ async fn test_core_channels( })] ) }); + let members = client_a .channel_store() .update(cx_a, |store, cx| { @@ -128,7 +134,6 @@ async fn test_core_channels( id: channel_a_id, name: "channel-a".to_string(), parent_id: None, - depth: 0, }), Arc::new(Channel { @@ -138,7 +143,9 @@ async fn test_core_channels( depth: 1, }) ] - ) + ); + assert!(!channels.is_user_admin(channel_a_id)); + assert!(!channels.is_user_admin(channel_b_id)); }); let channel_c_id = client_a @@ -280,6 +287,30 @@ async fn test_core_channels( client_b .channel_store() .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); + + // When disconnected, client A sees no channels. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_eq!(channels.channels(), &[]); + assert!(!channels.is_user_admin(channel_a_id)); + }); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_eq!( + channels.channels(), + &[Arc::new(Channel { + id: channel_a_id, + name: "channel-a".to_string(), + parent_id: None, + depth: 0, + })] + ); + assert!(channels.is_user_admin(channel_a_id)); + }); } fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u64]) { From 076b72cf2b122d8bd516766a89de1abf7f44b04a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Aug 2023 15:11:30 -0700 Subject: [PATCH 146/326] Improve styling of collab panel --- styles/src/style_tree/collab_panel.ts | 70 +++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index fd6e75d9ec..0979760b88 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -52,7 +52,61 @@ export default function contacts_panel(): any { }, } - const headerButton = toggleable_icon_button(theme, {}) + const headerButton = toggleable({ + state: { + inactive: interactive({ + base: { + corner_radius: 6, + padding: { + top: 2, + bottom: 2, + left: 4, + right: 4, + }, + icon_width: 14, + icon_height: 14, + button_width: 20, + button_height: 16, + color: foreground(layer, "on"), + }, + state: { + default: { + }, + hovered: { + background: background(layer, "base", "hovered"), + }, + clicked: { + background: background(layer, "base", "pressed"), + }, + }, + }), + active: interactive({ + base: { + corner_radius: 6, + padding: { + top: 2, + bottom: 2, + left: 4, + right: 4, + }, + icon_width: 14, + icon_height: 14, + button_width: 20, + button_height: 16, + color: foreground(layer, "on"), + }, + state: { + default: { + background: background(layer, "base", "active"), + }, + clicked: { + background: background(layer, "base", "active"), + }, + }, + }), + }, + }) + return { channel_modal: channel_modal(), @@ -154,9 +208,6 @@ export default function contacts_panel(): any { ...text(layer, "mono", "active", { size: "sm" }), background: background(layer, "active"), }, - hovered: { - background: background(layer, "hovered"), - }, clicked: { background: background(layer, "pressed"), }, @@ -196,23 +247,22 @@ export default function contacts_panel(): any { }, }, state: { - hovered: { - background: background(layer, "hovered"), - }, clicked: { background: background(layer, "pressed"), }, }, }), state: { + inactive: { + hovered: { + background: background(layer, "hovered"), + }, + }, active: { default: { ...text(layer, "mono", "active", { size: "sm" }), background: background(layer, "active"), }, - hovered: { - background: background(layer, "hovered"), - }, clicked: { background: background(layer, "pressed"), }, From b3447ada275b4005e6bab70242f827e3b3dc39ce Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 9 Aug 2023 17:11:52 -0700 Subject: [PATCH 147/326] Dial in the channel creating/renaming UI * Ensure channel list is in a consistent state with no flicker while the channel creation / rename request is outstanding. * Maintain selection properly when renaming and creating channels. * Style the channel name editor more consistently with the non-editable channel names. Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 62 +++++- crates/collab/src/rpc.rs | 28 +-- crates/collab/src/tests.rs | 4 +- crates/collab/src/tests/channel_tests.rs | 16 +- crates/collab_ui/src/collab_panel.rs | 234 ++++++++++++++++------- crates/rpc/proto/zed.proto | 18 +- crates/rpc/src/proto.rs | 6 +- styles/src/style_tree/collab_panel.ts | 2 +- 8 files changed, 260 insertions(+), 110 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 6325bc1a30..206423579a 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -39,8 +39,13 @@ pub struct ChannelMembership { pub admin: bool, } +pub enum ChannelEvent { + ChannelCreated(ChannelId), + ChannelRenamed(ChannelId), +} + impl Entity for ChannelStore { - type Event = (); + type Event = ChannelEvent; } pub enum ChannelMemberStatus { @@ -127,15 +132,37 @@ impl ChannelStore { &self, name: &str, parent_id: Option, - ) -> impl Future> { + cx: &mut ModelContext, + ) -> Task> { let client = self.client.clone(); let name = name.trim_start_matches("#").to_owned(); - async move { - Ok(client + cx.spawn(|this, mut cx| async move { + let channel = client .request(proto::CreateChannel { name, parent_id }) .await? - .channel_id) - } + .channel + .ok_or_else(|| anyhow!("missing channel in response"))?; + + let channel_id = channel.id; + + this.update(&mut cx, |this, cx| { + this.update_channels( + proto::UpdateChannels { + channels: vec![channel], + ..Default::default() + }, + cx, + ); + + // This event is emitted because the collab panel wants to clear the pending edit state + // before this frame is rendered. But we can't guarantee that the collab panel's future + // will resolve before this flush_effects finishes. Synchronously emitting this event + // ensures that the collab panel will observe this creation before the frame completes + cx.emit(ChannelEvent::ChannelCreated(channel_id)); + }); + + Ok(channel_id) + }) } pub fn invite_member( @@ -240,10 +267,27 @@ impl ChannelStore { ) -> Task> { let client = self.client.clone(); let name = new_name.to_string(); - cx.spawn(|_this, _cx| async move { - client + cx.spawn(|this, mut cx| async move { + let channel = client .request(proto::RenameChannel { channel_id, name }) - .await?; + .await? + .channel + .ok_or_else(|| anyhow!("missing channel in response"))?; + this.update(&mut cx, |this, cx| { + this.update_channels( + proto::UpdateChannels { + channels: vec![channel], + ..Default::default() + }, + cx, + ); + + // This event is emitted because the collab panel wants to clear the pending edit state + // before this frame is rendered. But we can't guarantee that the collab panel's future + // will resolve before this flush_effects finishes. Synchronously emitting this event + // ensures that the collab panel will observe this creation before the frame complete + cx.emit(ChannelEvent::ChannelRenamed(channel_id)) + }); Ok(()) }) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c2f0d31f90..f9f2d4a2e2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2146,16 +2146,18 @@ async fn create_channel( .create_channel(&request.name, parent_id, &live_kit_room, session.user_id) .await?; - response.send(proto::CreateChannelResponse { - channel_id: id.to_proto(), - })?; - - let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { + let channel = proto::Channel { id: id.to_proto(), name: request.name, parent_id: request.parent_id, - }); + }; + + response.send(proto::ChannelResponse { + channel: Some(channel.clone()), + })?; + + let mut update = proto::UpdateChannels::default(); + update.channels.push(channel); let user_ids_to_notify = if let Some(parent_id) = parent_id { db.get_channel_members(parent_id).await? @@ -2317,14 +2319,16 @@ async fn rename_channel( .rename_channel(channel_id, session.user_id, &request.name) .await?; - response.send(proto::Ack {})?; - - let mut update = proto::UpdateChannels::default(); - update.channels.push(proto::Channel { + let channel = proto::Channel { id: request.channel_id, name: new_name, parent_id: None, - }); + }; + response.send(proto::ChannelResponse { + channel: Some(channel.clone()), + })?; + let mut update = proto::UpdateChannels::default(); + update.channels.push(channel); let member_ids = db.get_channel_members(channel_id).await?; diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 31d7b629f8..46cbcb0213 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -278,8 +278,8 @@ impl TestServer { let channel_id = admin_client .app_state .channel_store - .update(admin_cx, |channel_store, _| { - channel_store.create_channel(channel, None) + .update(admin_cx, |channel_store, cx| { + channel_store.create_channel(channel, None, cx) }) .await .unwrap(); diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 63fab0d5f8..0dc6d478d1 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -21,15 +21,15 @@ async fn test_core_channels( let channel_a_id = client_a .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.create_channel("channel-a", None) + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("channel-a", None, cx) }) .await .unwrap(); let channel_b_id = client_a .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.create_channel("channel-b", Some(channel_a_id)) + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("channel-b", Some(channel_a_id), cx) }) .await .unwrap(); @@ -150,8 +150,8 @@ async fn test_core_channels( let channel_c_id = client_a .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.create_channel("channel-c", Some(channel_b_id)) + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("channel-c", Some(channel_b_id), cx) }) .await .unwrap(); @@ -351,8 +351,8 @@ async fn test_joining_channel_ancestor_member( let sub_id = client_a .channel_store() - .update(cx_a, |channel_store, _| { - channel_store.create_channel("sub_channel", Some(parent_id)) + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("sub_channel", Some(parent_id), cx) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 7bf2290622..cb40d496b6 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -4,7 +4,9 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{proto::PeerId, Channel, ChannelId, ChannelStore, Client, Contact, User, UserStore}; +use client::{ + proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, +}; use contact_finder::build_contact_finder; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; @@ -105,8 +107,23 @@ pub fn init(_client: Arc, cx: &mut AppContext) { #[derive(Debug)] pub enum ChannelEditingState { - Create { parent_id: Option }, - Rename { channel_id: u64 }, + Create { + parent_id: Option, + pending_name: Option, + }, + Rename { + channel_id: u64, + pending_name: Option, + }, +} + +impl ChannelEditingState { + fn pending_name(&self) -> Option<&str> { + match self { + ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), + ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), + } + } } pub struct CollabPanel { @@ -211,7 +228,7 @@ impl CollabPanel { if !query.is_empty() { this.selection.take(); } - this.update_entries(false, cx); + this.update_entries(true, cx); if !query.is_empty() { this.selection = this .entries @@ -233,6 +250,11 @@ impl CollabPanel { cx.subscribe(&channel_name_editor, |this, _, event, cx| { if let editor::Event::Blurred = event { + if let Some(state) = &this.channel_editing_state { + if state.pending_name().is_some() { + return; + } + } this.take_editing_state(cx); this.update_entries(false, cx); cx.notify(); @@ -391,17 +413,35 @@ impl CollabPanel { let active_call = ActiveCall::global(cx); this.subscriptions .push(cx.observe(&this.user_store, |this, _, cx| { - this.update_entries(false, cx) + this.update_entries(true, cx) })); this.subscriptions .push(cx.observe(&this.channel_store, |this, _, cx| { - this.update_entries(false, cx) + this.update_entries(true, cx) })); this.subscriptions - .push(cx.observe(&active_call, |this, _, cx| this.update_entries(false, cx))); + .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); this.subscriptions.push( - cx.observe_global::(move |this, cx| this.update_entries(false, cx)), + cx.observe_global::(move |this, cx| this.update_entries(true, cx)), ); + this.subscriptions.push(cx.subscribe( + &this.channel_store, + |this, _channel_store, e, cx| match e { + ChannelEvent::ChannelCreated(channel_id) + | ChannelEvent::ChannelRenamed(channel_id) => { + if this.take_editing_state(cx) { + this.update_entries(false, cx); + this.selection = this.entries.iter().position(|entry| { + if let ListEntry::Channel(channel) = entry { + channel.id == *channel_id + } else { + false + } + }); + } + } + }, + )); this }) @@ -453,7 +493,7 @@ impl CollabPanel { ); } - fn update_entries(&mut self, select_editor: bool, cx: &mut ViewContext) { + fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); let query = self.filter_editor.read(cx).text(cx); @@ -595,7 +635,13 @@ impl CollabPanel { executor.clone(), )); if let Some(state) = &self.channel_editing_state { - if matches!(state, ChannelEditingState::Create { parent_id: None }) { + if matches!( + state, + ChannelEditingState::Create { + parent_id: None, + .. + } + ) { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } @@ -603,7 +649,7 @@ impl CollabPanel { let channel = &channels[mat.candidate_id]; match &self.channel_editing_state { - Some(ChannelEditingState::Create { parent_id }) + Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => { self.entries.push(ListEntry::Channel(channel.clone())); @@ -611,11 +657,11 @@ impl CollabPanel { depth: channel.depth + 1, }); } - Some(ChannelEditingState::Rename { channel_id }) + Some(ChannelEditingState::Rename { channel_id, .. }) if *channel_id == channel.id => { self.entries.push(ListEntry::ChannelEditor { - depth: channel.depth + 1, + depth: channel.depth, }); } _ => { @@ -775,14 +821,7 @@ impl CollabPanel { } } - if select_editor { - for (ix, entry) in self.entries.iter().enumerate() { - if matches!(*entry, ListEntry::ChannelEditor { .. }) { - self.selection = Some(ix); - break; - } - } - } else { + if select_same_item { if let Some(prev_selected_entry) = prev_selected_entry { self.selection.take(); for (ix, entry) in self.entries.iter().enumerate() { @@ -792,6 +831,14 @@ impl CollabPanel { } } } + } else { + self.selection = self.selection.and_then(|prev_selection| { + if self.entries.is_empty() { + None + } else { + Some(prev_selection.min(self.entries.len() - 1)) + } + }); } let old_scroll_top = self.list_state.logical_scroll_top(); @@ -1088,18 +1135,14 @@ impl CollabPanel { .into_any() } - fn take_editing_state( - &mut self, - cx: &mut ViewContext, - ) -> Option<(ChannelEditingState, String)> { - if let Some(state) = self.channel_editing_state.take() { + fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { + if let Some(_) = self.channel_editing_state.take() { self.channel_name_editor.update(cx, |editor, cx| { - let name = editor.text(cx); editor.set_text("", cx); - Some((state, name)) - }) + }); + true } else { - None + false } } @@ -1367,22 +1410,43 @@ impl CollabPanel { .left(), ) .with_child( - ChildView::new(&self.channel_name_editor, cx) + if let Some(pending_name) = self + .channel_editing_state + .as_ref() + .and_then(|state| state.pending_name()) + { + Label::new( + pending_name.to_string(), + theme.collab_panel.contact_username.text.clone(), + ) .contained() - .with_style(theme.collab_panel.channel_editor) - .flex(1.0, true), + .with_style(theme.collab_panel.contact_username.container) + .aligned() + .left() + .flex(1., true) + .into_any() + } else { + ChildView::new(&self.channel_name_editor, cx) + .aligned() + .left() + .contained() + .with_style(theme.collab_panel.channel_editor) + .flex(1.0, true) + .into_any() + }, ) .align_children_center() + .constrained() + .with_height(theme.collab_panel.row_height) .contained() + .with_style(gpui::elements::ContainerStyle { + background_color: Some(theme.editor.background), + ..*theme.collab_panel.contact_row.default_style() + }) .with_padding_left( theme.collab_panel.contact_row.default_style().padding.left + theme.collab_panel.channel_indent * depth as f32, ) - .contained() - .with_style(gpui::elements::ContainerStyle { - background_color: Some(theme.editor.background), - ..Default::default() - }) .into_any() } @@ -1684,7 +1748,7 @@ impl CollabPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.take_editing_state(cx).is_some() { + if self.take_editing_state(cx) { cx.focus(&self.filter_editor); } else { self.filter_editor.update(cx, |editor, cx| { @@ -1785,29 +1849,47 @@ impl CollabPanel { } } - fn confirm_channel_edit(&mut self, cx: &mut ViewContext<'_, '_, CollabPanel>) -> bool { - if let Some((editing_state, channel_name)) = self.take_editing_state(cx) { + fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { + if let Some(editing_state) = &mut self.channel_editing_state { match editing_state { - ChannelEditingState::Create { parent_id } => { - let request = self.channel_store.update(cx, |channel_store, _| { - channel_store.create_channel(&channel_name, parent_id) - }); - cx.foreground() - .spawn(async move { - request.await?; - anyhow::Ok(()) - }) - .detach(); - } - ChannelEditingState::Rename { channel_id } => { + ChannelEditingState::Create { + parent_id, + pending_name, + .. + } => { + if pending_name.is_some() { + return false; + } + let channel_name = self.channel_name_editor.read(cx).text(cx); + + *pending_name = Some(channel_name.clone()); + self.channel_store .update(cx, |channel_store, cx| { - channel_store.rename(channel_id, &channel_name, cx) + channel_store.create_channel(&channel_name, *parent_id, cx) }) .detach(); + cx.notify(); + } + ChannelEditingState::Rename { + channel_id, + pending_name, + } => { + if pending_name.is_some() { + return false; + } + let channel_name = self.channel_name_editor.read(cx).text(cx); + *pending_name = Some(channel_name.clone()); + + self.channel_store + .update(cx, |channel_store, cx| { + channel_store.rename(*channel_id, &channel_name, cx) + }) + .detach(); + cx.notify(); } } - self.update_entries(false, cx); + cx.focus_self(); true } else { false @@ -1844,17 +1926,30 @@ impl CollabPanel { } fn new_root_channel(&mut self, cx: &mut ViewContext) { - self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: None }); - self.update_entries(true, cx); + self.channel_editing_state = Some(ChannelEditingState::Create { + parent_id: None, + pending_name: None, + }); + self.update_entries(false, cx); + self.select_channel_editor(); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } + fn select_channel_editor(&mut self) { + self.selection = self.entries.iter().position(|entry| match entry { + ListEntry::ChannelEditor { .. } => true, + _ => false, + }); + } + fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), + pending_name: None, }); - self.update_entries(true, cx); + self.update_entries(false, cx); + self.select_channel_editor(); cx.focus(self.channel_name_editor.as_any()); cx.notify(); } @@ -1887,19 +1982,22 @@ impl CollabPanel { } fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { - if let Some(channel) = self - .channel_store - .read(cx) - .channel_for_id(action.channel_id) - { + let channel_store = self.channel_store.read(cx); + if !channel_store.is_user_admin(action.channel_id) { + return; + } + if let Some(channel) = channel_store.channel_for_id(action.channel_id) { self.channel_editing_state = Some(ChannelEditingState::Rename { channel_id: action.channel_id, + pending_name: None, }); self.channel_name_editor.update(cx, |editor, cx| { editor.set_text(channel.name.clone(), cx); editor.select_all(&Default::default(), cx); }); - self.update_entries(true, cx); + cx.focus(self.channel_name_editor.as_any()); + self.update_entries(false, cx); + self.select_channel_editor(); } } @@ -2069,8 +2167,12 @@ impl View for CollabPanel { if !self.has_focus { self.has_focus = true; if !self.context_menu.is_focused(cx) { - if self.channel_editing_state.is_some() { - cx.focus(&self.channel_name_editor); + if let Some(editing_state) = &self.channel_editing_state { + if editing_state.pending_name().is_none() { + cx.focus(&self.channel_name_editor); + } else { + cx.focus(&self.filter_editor); + } } else { cx.focus(&self.filter_editor); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 13b4c60aad..fc9a66753c 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -131,17 +131,17 @@ message Envelope { RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; - CreateChannelResponse create_channel_response = 120; + ChannelResponse channel_response = 120; InviteChannelMember invite_channel_member = 121; RemoveChannelMember remove_channel_member = 122; RespondToChannelInvite respond_to_channel_invite = 123; UpdateChannels update_channels = 124; - JoinChannel join_channel = 126; - RemoveChannel remove_channel = 127; - GetChannelMembers get_channel_members = 128; - GetChannelMembersResponse get_channel_members_response = 129; - SetChannelMemberAdmin set_channel_member_admin = 130; - RenameChannel rename_channel = 131; + JoinChannel join_channel = 125; + RemoveChannel remove_channel = 126; + GetChannelMembers get_channel_members = 127; + GetChannelMembersResponse get_channel_members_response = 128; + SetChannelMemberAdmin set_channel_member_admin = 129; + RenameChannel rename_channel = 130; } } @@ -921,8 +921,8 @@ message CreateChannel { optional uint64 parent_id = 2; } -message CreateChannelResponse { - uint64 channel_id = 1; +message ChannelResponse { + Channel channel = 1; } message InviteChannelMember { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d3a3091131..92732b00b5 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -146,7 +146,7 @@ messages!( (CopyProjectEntry, Foreground), (CreateBufferForPeer, Foreground), (CreateChannel, Foreground), - (CreateChannelResponse, Foreground), + (ChannelResponse, Foreground), (CreateProjectEntry, Foreground), (CreateRoom, Foreground), (CreateRoomResponse, Foreground), @@ -262,7 +262,7 @@ request_messages!( (CopyProjectEntry, ProjectEntryResponse), (CreateProjectEntry, ProjectEntryResponse), (CreateRoom, CreateRoomResponse), - (CreateChannel, CreateChannelResponse), + (CreateChannel, ChannelResponse), (DeclineCall, Ack), (DeleteProjectEntry, ProjectEntryResponse), (ExpandProjectEntry, ExpandProjectEntryResponse), @@ -305,7 +305,7 @@ request_messages!( (JoinChannel, JoinRoomResponse), (RemoveChannel, Ack), (RenameProjectEntry, ProjectEntryResponse), - (RenameChannel, Ack), + (RenameChannel, ChannelResponse), (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (ShareProject, ShareProjectResponse), diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 0979760b88..6c10da7482 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -358,7 +358,7 @@ export default function contacts_panel(): any { face_overlap: 8, channel_editor: { padding: { - left: 8, + left: name_margin, } } } From b2f773e91dd391c543d176b246d047bd510bb931 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 10 Aug 2023 12:57:12 +0200 Subject: [PATCH 148/326] Increase padding on search --- crates/search/src/project_search.rs | 6 ++---- crates/search/src/search_bar.rs | 13 +------------ styles/src/style_tree/search.ts | 13 +++++++++++-- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 52797aa8db..a3ae61f0a8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1616,7 +1616,6 @@ impl View for ProjectSearchBar { ) .with_children(filters) .contained() - .with_style(theme.search.container) .aligned() .top() .flex(1., false), @@ -1643,7 +1642,6 @@ impl View for ProjectSearchBar { .constrained() .with_height(theme.workspace.toolbar.height) .contained() - .with_style(theme.search.container) .aligned() .right() .top() @@ -1659,7 +1657,7 @@ impl View for ProjectSearchBar { .flex(1., true), ) .contained() - .with_uniform_padding(theme.workspace.toolbar.height / 3.) + .with_style(theme.search.container) .flex_float() .into_any_named("project search") } else { @@ -1693,7 +1691,7 @@ impl ToolbarItemView for ProjectSearchBar { .as_ref() .map(|search| { let offset = search.read(cx).filters_enabled as usize; - 2 + offset + 3 + offset }) .unwrap_or_else(|| 2) } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 19d8fa6b98..19feb10c95 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -65,11 +65,6 @@ pub(super) fn render_nav_button( MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let mut style = theme.search.nav_button.style_for(state).clone(); - - match direction { - Direction::Prev => style.container.border.left = false, - Direction::Next => style.container.border.right = false, - }; let label = Label::new(icon, style.label.clone()) .contained() .with_style(style.container.clone()); @@ -110,13 +105,7 @@ pub(super) fn render_nav_button( .with_height(theme.workspace.toolbar.height), } }) - .on_click( - MouseButton::Left, - on_click, /*move |_, this, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); - }*/ - ) + .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::( direction as usize, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index fb9b356591..3de76f1d7e 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -34,6 +34,7 @@ export default function search(): any { } return { + padding: { top: 32, bottom: 32, left: 32, right: 32 }, // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive match_background: with_opacity( foreground(theme.highest, "accent"), @@ -187,7 +188,11 @@ export default function search(): any { ...text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - border: border(theme.highest, "on"), + border: { + ...border(theme.highest, "on"), + left: false, + right: false + }, padding: { bottom: 6, @@ -230,7 +235,11 @@ export default function search(): any { text: text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), corner_radius: 2, - border: border(theme.highest, "on"), + border: { + ...border(theme.highest, "on"), + left: false, + right: false, + }, padding: { bottom: 6, From da3a4174ce6d4fd824fc50c02b822371eda96190 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:01:20 +0200 Subject: [PATCH 149/326] Bump row_count for buffer search --- crates/search/src/buffer_search.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ec2f1c6855..e55d91ac1c 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -367,6 +367,9 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } } + fn row_count(&self, _: &ViewContext) -> usize { + 3 + } } impl BufferSearchBar { From 9ad308f2f081ae5e11e4f2d4962f973459db685f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 10 Aug 2023 14:53:25 +0200 Subject: [PATCH 150/326] Fix padding inside search input. Adjust borders for toggle buttons --- styles/src/style_tree/search.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 3de76f1d7e..35e7042ceb 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -20,9 +20,9 @@ export default function search(): any { right: 12, }, padding: { - top: 3, - bottom: 3, - left: 10, + top: 4, + bottom: 4, + left: 4, right: 4, }, } @@ -46,8 +46,8 @@ export default function search(): any { ...text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), corner_radius: 2, - border: border(theme.highest, "on"), - + margin: { right: 2 }, + border: { width: 0, color: foreground(theme.highest, "variant") }, padding: { bottom: 6, left: 6, @@ -59,25 +59,26 @@ export default function search(): any { hovered: { ...text(theme.highest, "mono", "on", "hovered"), background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), }, clicked: { ...text(theme.highest, "mono", "on", "pressed"), background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), }, }, }), state: { active: { default: { - ...text(theme.highest, "mono", "accent"), + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "on"), }, hovered: { ...text(theme.highest, "mono", "accent", "hovered"), + border: border(theme.highest, "on", "hovered"), }, clicked: { ...text(theme.highest, "mono", "accent", "pressed"), + border: border(theme.highest, "on", "pressed"), }, }, }, From 096e293c179865f5f3e651fe9fdb36f18fcc098d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:26:05 +0200 Subject: [PATCH 151/326] Fix rounding artifacts of nav buttons --- crates/search/src/search_bar.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 19feb10c95..9993fa530a 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -65,6 +65,7 @@ pub(super) fn render_nav_button( MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let mut style = theme.search.nav_button.style_for(state).clone(); + style.container.corner_radius = 0.; let label = Label::new(icon, style.label.clone()) .contained() .with_style(style.container.clone()); @@ -124,13 +125,6 @@ pub(crate) fn render_search_mode_button( cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); - // let is_active = if let Some(search) = self.active_project_search.as_ref() { - // let search = search.read(cx); - // search.current_mode == mode - // } else { - // false - // }; - enum SearchModeButton {} MouseEventHandler::::new(mode.region_id(), cx, |state, cx| { let theme = theme::current(cx); From a9a2d281c3455957f57d02b56ae6fa7c7d341c9c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:10:56 +0200 Subject: [PATCH 152/326] Make ButtonSide scale with corner_radius --- crates/search/src/search_bar.rs | 12 +++++++----- crates/theme/src/theme.rs | 1 - styles/src/style_tree/search.ts | 5 ++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 9993fa530a..7b2ec33187 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -65,6 +65,7 @@ pub(super) fn render_nav_button( MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let mut style = theme.search.nav_button.style_for(state).clone(); + let button_side_width = style.container.corner_radius; style.container.corner_radius = 0.; let label = Label::new(icon, style.label.clone()) .contained() @@ -82,7 +83,7 @@ pub(super) fn render_nav_button( .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.search.mode_filling_width), + .with_max_width(button_side_width), ) .with_child(label) .constrained() @@ -100,7 +101,7 @@ pub(super) fn render_nav_button( .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.search.mode_filling_width), + .with_max_width(button_side_width), ) .constrained() .with_height(theme.workspace.toolbar.height), @@ -134,7 +135,8 @@ pub(crate) fn render_search_mode_button( .in_state(is_active) .style_for(state) .clone(); - + let side_width = style.container.corner_radius; + style.container.corner_radius = 0.; let label = Label::new(mode.label(), style.text.clone()) .contained() .with_style(style.container); @@ -156,7 +158,7 @@ pub(crate) fn render_search_mode_button( .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.search.mode_filling_width), + .with_max_width(side_width), ) .with_child(label) .into_any() @@ -174,7 +176,7 @@ pub(crate) fn render_search_mode_button( .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(theme.search.mode_filling_width), + .with_max_width(side_width), ) .into_any() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1107906ca7..e950845b49 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -388,7 +388,6 @@ pub struct Search { pub dismiss_button: Interactive, pub editor_icon: IconStyle, pub mode_button: Toggleable>, - pub mode_filling_width: f32, pub nav_button: Interactive, } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 35e7042ceb..8da7e21eb6 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -201,7 +201,7 @@ export default function search(): any { right: 10, top: 6, }, - corner_radius: 2, + corner_radius: 6, }, state: { hovered: { @@ -230,12 +230,11 @@ export default function search(): any { }, }, }), - mode_filling_width: 4.0, nav_button: interactive({ base: { text: text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - corner_radius: 2, + corner_radius: 6, border: { ...border(theme.highest, "on"), left: false, From b8df26b19405749bba898c4482562fa20c2e5d01 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:26:22 +0200 Subject: [PATCH 153/326] mode_button: get rid of borders before creating a label --- crates/search/src/search_bar.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 7b2ec33187..94d1407b9b 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -137,14 +137,15 @@ pub(crate) fn render_search_mode_button( .clone(); let side_width = style.container.corner_radius; style.container.corner_radius = 0.; + if let Some(button_side) = mode.button_side() { + style.container.border.left = mode.border_left(); + style.container.border.right = mode.border_right(); + } let label = Label::new(mode.label(), style.text.clone()) .contained() .with_style(style.container); if let Some(button_side) = mode.button_side() { - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - if button_side == Side::Left { Flex::row() .align_children_center() From 3d103546bc2169ed51640a8114e4735d89496880 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:44:22 +0200 Subject: [PATCH 154/326] Make ButtonSide respect corner_radius --- crates/search/src/elements.rs | 41 +++++++++++++++++++++++---------- crates/search/src/search_bar.rs | 10 ++++++-- styles/src/style_tree/search.ts | 4 ++-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/crates/search/src/elements.rs b/crates/search/src/elements.rs index bd81f4f6a2..0a7990c24a 100644 --- a/crates/search/src/elements.rs +++ b/crates/search/src/elements.rs @@ -7,7 +7,7 @@ use gpui::json::{self, ToJson}; use gpui::{scene::Path, LayoutContext}; use gpui::{Element, PaintContext, SceneBuilder, View, ViewContext}; -type CreatePath = fn(RectF, Color) -> Path; +type CreatePath = fn(RectF, Color, f32) -> Path; type AdjustBorder = fn(RectF, f32) -> RectF; type BorderThickness = f32; @@ -18,26 +18,43 @@ pub(crate) struct ButtonSide { /// the drawing bounds have to be adjusted by different factors in different dimensions. border_adjustment: AdjustBorder, border: Option<(BorderThickness, Color)>, + radius: f32, } impl ButtonSide { - fn new(color: Color, factory: CreatePath, border_adjustment: AdjustBorder) -> Self { + fn new( + color: Color, + factory: CreatePath, + border_adjustment: AdjustBorder, + radius: f32, + ) -> Self { Self { color, factory, border_adjustment, border: None, + radius, } } pub fn with_border(mut self, width: f32, color: Color) -> Self { self.border = Some((width, color)); self } - pub fn left(color: Color) -> Self { - Self::new(color, left_button_side, left_button_border_adjust) + pub fn left(color: Color, corner_radius: f32) -> Self { + Self::new( + color, + left_button_side, + left_button_border_adjust, + corner_radius, + ) } - pub fn right(color: Color) -> Self { - Self::new(color, right_button_side, right_button_border_adjust) + pub fn right(color: Color, corner_radius: f32) -> Self { + Self::new( + color, + right_button_side, + right_button_border_adjust, + corner_radius, + ) } } @@ -53,13 +70,13 @@ fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { origin.set_x(origin.x() - width.x()); RectF::from_points(origin, bounds.lower_right() - width) } -fn left_button_side(bounds: RectF, color: Color) -> Path { +fn left_button_side(bounds: RectF, color: Color, radius: f32) -> Path { use gpui::geometry::PathBuilder; let mut path = PathBuilder::new(); path.reset(bounds.lower_right()); path.line_to(bounds.upper_right()); let mut middle_point = bounds.origin(); - let distance_to_line = (middle_point.y() - bounds.lower_left().y()) / 4.; + let distance_to_line = (middle_point.y() - bounds.lower_left().y()).min(-radius.abs()); middle_point.set_y(middle_point.y() - distance_to_line); path.curve_to(middle_point, bounds.origin()); let mut target = bounds.lower_left(); @@ -69,13 +86,13 @@ fn left_button_side(bounds: RectF, color: Color) -> Path { path.build(color, None) } -fn right_button_side(bounds: RectF, color: Color) -> Path { +fn right_button_side(bounds: RectF, color: Color, radius: f32) -> Path { use gpui::geometry::PathBuilder; let mut path = PathBuilder::new(); path.reset(bounds.lower_left()); path.line_to(bounds.origin()); let mut middle_point = bounds.upper_right(); - let distance_to_line = (middle_point.y() - bounds.lower_right().y()) / 4.; + let distance_to_line = (middle_point.y() - bounds.lower_right().y()).min(-radius.abs()); middle_point.set_y(middle_point.y() - distance_to_line); path.curve_to(middle_point, bounds.upper_right()); let mut target = bounds.lower_right(); @@ -110,10 +127,10 @@ impl Element for ButtonSide { ) -> Self::PaintState { let mut bounds = bounds; if let Some((border_width, border_color)) = self.border.as_ref() { - scene.push_path((self.factory)(bounds, border_color.clone())); + scene.push_path((self.factory)(bounds, border_color.clone(), self.radius)); bounds = (self.border_adjustment)(bounds, *border_width); }; - scene.push_path((self.factory)(bounds, self.color)); + scene.push_path((self.factory)(bounds, self.color, self.radius)); } fn rect_for_text_range( diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 94d1407b9b..fa463d4e0d 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -79,6 +79,7 @@ pub(super) fn render_nav_button( .container .background_color .unwrap_or_else(gpui::color::Color::transparent_black), + button_side_width, ) .with_border(style.container.border.width, style.container.border.color) .contained() @@ -97,6 +98,7 @@ pub(super) fn render_nav_button( .container .background_color .unwrap_or_else(gpui::color::Color::transparent_black), + button_side_width, ) .with_border(style.container.border.width, style.container.border.color) .contained() @@ -155,11 +157,13 @@ pub(crate) fn render_search_mode_button( .container .background_color .unwrap_or_else(gpui::color::Color::transparent_black), + side_width, ) .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(side_width), + .with_max_width(side_width) + .with_height(32.), ) .with_child(label) .into_any() @@ -173,11 +177,13 @@ pub(crate) fn render_search_mode_button( .container .background_color .unwrap_or_else(gpui::color::Color::transparent_black), + side_width, ) .with_border(style.container.border.width, style.container.border.color) .contained() .constrained() - .with_max_width(side_width), + .with_max_width(side_width) + .with_height(32.), ) .into_any() } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 8da7e21eb6..52bffa6cad 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -196,10 +196,10 @@ export default function search(): any { }, padding: { - bottom: 6, + bottom: 4, left: 10, right: 10, - top: 6, + top: 5, }, corner_radius: 6, }, From 9e9b3093a9a91b55e49e041898b4dc1cab53f444 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:32:15 +0200 Subject: [PATCH 155/326] Style mode/option buttons --- crates/search/src/search_bar.rs | 1 + styles/src/style_tree/search.ts | 39 +++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index fa463d4e0d..6cdb36acc3 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -223,6 +223,7 @@ pub(crate) fn render_option_button_icon( .contained() .with_style(style.container) .constrained() + .with_height(22.) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 52bffa6cad..ff36e3532d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -47,22 +47,30 @@ export default function search(): any { background: background(theme.highest, "on"), corner_radius: 2, margin: { right: 2 }, - border: { width: 0, color: foreground(theme.highest, "variant") }, + border: { + width: 1., color: background(theme.highest, "on") + }, padding: { - bottom: 6, - left: 6, - right: 6, - top: 6, + bottom: 4, + left: 4, + right: 4, + top: 4, }, }, state: { hovered: { ...text(theme.highest, "mono", "on", "hovered"), background: background(theme.highest, "on", "hovered"), + border: { + width: 1., color: background(theme.highest, "on", "hovered") + }, }, clicked: { ...text(theme.highest, "mono", "on", "pressed"), background: background(theme.highest, "on", "pressed"), + border: { + width: 1., color: background(theme.highest, "on", "pressed") + }, }, }, }), @@ -186,8 +194,8 @@ export default function search(): any { mode_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), + ...text(theme.highest, "mono", "variant"), + background: background(theme.highest, "variant"), border: { ...border(theme.highest, "on"), @@ -205,13 +213,13 @@ export default function search(): any { }, state: { hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "variant", "hovered"), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "variant", "pressed"), border: border(theme.highest, "on", "pressed"), }, }, @@ -219,13 +227,16 @@ export default function search(): any { state: { active: { default: { - ...text(theme.highest, "mono", "accent"), + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on") }, hovered: { - ...text(theme.highest, "mono", "accent", "hovered"), + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered") }, clicked: { - ...text(theme.highest, "mono", "accent", "pressed"), + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed") }, }, }, From f6ecf83f33d2c95e014eaa5b8951dae7434986c9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:43:07 +0200 Subject: [PATCH 156/326] Increase editor's left hand side padding to 10 --- styles/src/style_tree/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index ff36e3532d..e612c8daf5 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -22,7 +22,7 @@ export default function search(): any { padding: { top: 4, bottom: 4, - left: 4, + left: 10, right: 4, }, } From 100a8961b5dfd4f7f7e28bbf3afec25a9a100fbd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:51:18 +0200 Subject: [PATCH 157/326] Add accent colors to search option buttons --- styles/src/style_tree/search.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index e612c8daf5..252e759a6d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -77,16 +77,16 @@ export default function search(): any { state: { active: { default: { - background: background(theme.highest, "accent", "hovered"), - border: border(theme.highest, "on"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), }, hovered: { - ...text(theme.highest, "mono", "accent", "hovered"), - border: border(theme.highest, "on", "hovered"), + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "accent", "pressed"), - border: border(theme.highest, "on", "pressed"), + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), }, }, }, From 799278e2966c50711fd76dc2def85abaac2790d2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:02:25 +0200 Subject: [PATCH 158/326] Add row_height --- crates/search/src/project_search.rs | 4 +++- crates/search/src/search_bar.rs | 4 ++-- crates/theme/src/theme.rs | 1 + styles/src/style_tree/search.ts | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a3ae61f0a8..aa30d47243 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1592,7 +1592,8 @@ impl View for ProjectSearchBar { .top() .left(), ) - .contained() + .constrained() + .with_max_height(theme.search.search_bar_row_height) .flex(1., true), ) .with_child( @@ -1609,6 +1610,7 @@ impl View for ProjectSearchBar { .constrained() .with_min_width(theme.search.editor.min_width) .with_max_width(theme.search.editor.max_width) + .with_max_height(theme.search.search_bar_row_height) .flex(1., false), ) .contained() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 6cdb36acc3..593e0aeb69 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -163,7 +163,7 @@ pub(crate) fn render_search_mode_button( .contained() .constrained() .with_max_width(side_width) - .with_height(32.), + .with_height(theme.search.search_bar_row_height), ) .with_child(label) .into_any() @@ -183,7 +183,7 @@ pub(crate) fn render_search_mode_button( .contained() .constrained() .with_max_width(side_width) - .with_height(32.), + .with_height(theme.search.search_bar_row_height), ) .into_any() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e950845b49..dbcffe0382 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -389,6 +389,7 @@ pub struct Search { pub editor_icon: IconStyle, pub mode_button: Toggleable>, pub nav_button: Interactive, + pub search_bar_row_height: f32, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 252e759a6d..c8e347a57d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -272,6 +272,7 @@ export default function search(): any { }, }, }), + search_bar_row_height: 32, } } From e4c593c2fb09c7f97bea75a07c5fa8f39afa49bd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:09:26 +0200 Subject: [PATCH 159/326] chore: fix compiler warning --- crates/search/src/search_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 593e0aeb69..1569174c92 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -139,7 +139,7 @@ pub(crate) fn render_search_mode_button( .clone(); let side_width = style.container.corner_radius; style.container.corner_radius = 0.; - if let Some(button_side) = mode.button_side() { + if mode.button_side().is_some() { style.container.border.left = mode.border_left(); style.container.border.right = mode.border_right(); } From 0a0314eec92472e840de4753f883baeb3092fbb3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:19:45 +0200 Subject: [PATCH 160/326] Shrink project search + decrease padding --- crates/search/src/project_search.rs | 2 +- styles/src/style_tree/search.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index aa30d47243..f0363ee5d9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1693,7 +1693,7 @@ impl ToolbarItemView for ProjectSearchBar { .as_ref() .map(|search| { let offset = search.read(cx).filters_enabled as usize; - 3 + offset + 2 + offset }) .unwrap_or_else(|| 2) } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c8e347a57d..6a6b2cc3ac 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -34,7 +34,7 @@ export default function search(): any { } return { - padding: { top: 32, bottom: 32, left: 32, right: 32 }, + padding: { top: 16, bottom: 16, left: 16, right: 16 }, // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive match_background: with_opacity( foreground(theme.highest, "accent"), From ff1261b3008d4e0bc06bdc6d39ab6bb8a69101d5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 11 Aug 2023 13:32:46 -0400 Subject: [PATCH 161/326] WIP Restyle channel modal Co-Authored-By: Mikayla Maki --- .../src/collab_panel/channel_modal.rs | 15 ++-- crates/theme/src/theme.rs | 2 +- styles/src/component/input.ts | 26 ++++++ styles/src/component/text_button.ts | 9 +- styles/src/style_tree/channel_modal.ts | 88 ++++++------------- 5 files changed, 64 insertions(+), 76 deletions(-) create mode 100644 styles/src/component/input.ts diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 09be3798a6..77401d269c 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -175,19 +175,16 @@ impl View for ChannelModal { this.set_mode(mode, cx); } }) - .with_cursor_style(if active { - CursorStyle::Arrow - } else { - CursorStyle::PointingHand - }) + .with_cursor_style(CursorStyle::PointingHand) .into_any() } Flex::column() - .with_child(Label::new( - format!("#{}", channel.name), - theme.header.clone(), - )) + .with_child( + Label::new(format!("#{}", channel.name), theme.header.text.clone()) + .contained() + .with_style(theme.header.container.clone()), + ) .with_child(Flex::row().with_children([ render_mode_button::( Mode::InviteMembers, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1756f91fb8..9025bf1cd2 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -253,7 +253,7 @@ pub struct CollabPanel { pub struct ChannelModal { pub container: ContainerStyle, pub height: f32, - pub header: TextStyle, + pub header: ContainedText, pub mode_button: Toggleable>, pub picker: Picker, pub row_height: f32, diff --git a/styles/src/component/input.ts b/styles/src/component/input.ts new file mode 100644 index 0000000000..52d0b42d97 --- /dev/null +++ b/styles/src/component/input.ts @@ -0,0 +1,26 @@ +import { useTheme } from "../common" +import { background, border, text } from "../style_tree/components" + +export const input = () => { + const theme = useTheme() + + return { + background: background(theme.highest), + corner_radius: 8, + min_width: 200, + max_width: 500, + placeholder_text: text(theme.highest, "mono", "disabled"), + selection: theme.players[0], + text: text(theme.highest, "mono", "default"), + border: border(theme.highest), + margin: { + right: 12, + }, + padding: { + top: 3, + bottom: 3, + left: 12, + right: 8, + } + } +} diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 58b2a1cbf2..3311081a6f 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -13,6 +13,7 @@ interface TextButtonOptions { | Theme["lowest"] | Theme["middle"] | Theme["highest"] + variant?: "default" | "ghost" color?: keyof Theme["lowest"] margin?: Partial text_properties?: TextProperties @@ -23,6 +24,7 @@ type ToggleableTextButtonOptions = TextButtonOptions & { } export function text_button({ + variant = "default", color, layer, margin, @@ -59,7 +61,7 @@ export function text_button({ }, state: { default: { - background: background(layer ?? theme.lowest, color), + background: variant !== "ghost" ? background(layer ?? theme.lowest, color) : null, color: foreground(layer ?? theme.lowest, color), }, hovered: { @@ -76,14 +78,15 @@ export function text_button({ export function toggleable_text_button( theme: Theme, - { color, active_color, margin }: ToggleableTextButtonOptions + { variant, color, active_color, margin }: ToggleableTextButtonOptions ) { if (!color) color = "base" return toggleable({ state: { - inactive: text_button({ color, margin }), + inactive: text_button({ variant, color, margin }), active: text_button({ + variant, color: active_color ? active_color : color, margin, layer: theme.middle, diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 447522070b..764ab9fc93 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -2,6 +2,8 @@ import { useTheme } from "../theme" import { interactive, toggleable } from "../element" import { background, border, foreground, text } from "./components" import picker from "./picker" +import { input } from "../component/input" +import { toggleable_text_button } from "../component/text_button" export default function channel_modal(): any { const theme = useTheme() @@ -19,29 +21,10 @@ export default function channel_modal(): any { delete picker_style.shadow delete picker_style.border - const picker_input = { - background: background(theme.middle, "on"), - corner_radius: 6, - text: text(theme.middle, "mono"), - placeholder_text: text(theme.middle, "mono", "on", "disabled", { - size: "xs", - }), - selection: theme.players[0], - border: border(theme.middle), - padding: { - bottom: 8, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: side_margin, - right: side_margin, - bottom: 8, - }, - } + const picker_input = input() return { + // This is used for the icons that are rendered to the right of channel Members in both UIs member_icon: { background: background(theme.middle), padding: { @@ -53,6 +36,7 @@ export default function channel_modal(): any { width: 5, color: foreground(theme.middle, "accent"), }, + // This is used for the icons that are rendered to the right of channel invites in both UIs invitee_icon: { background: background(theme.middle), padding: { @@ -89,54 +73,32 @@ export default function channel_modal(): any { } }, container: { - background: background(theme.lowest), - border: border(theme.lowest), + background: background(theme.middle), + border: border(theme.middle), shadow: theme.modal_shadow, corner_radius: 12, padding: { - bottom: 4, - left: 20, - right: 20, - top: 20, + bottom: 0, + left: 0, + right: 0, + top: 0, }, }, height: 400, - header: text(theme.middle, "sans", "on", { size: "lg" }), - mode_button: toggleable({ - base: interactive({ - base: { - ...text(theme.middle, "sans", { size: "xs" }), - border: border(theme.middle, "active"), - corner_radius: 4, - padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, - }, - - margin: { left: 6, top: 6, bottom: 6 }, - }, - state: { - hovered: { - ...text(theme.middle, "sans", "default", { size: "xs" }), - background: background(theme.middle, "hovered"), - border: border(theme.middle, "active"), - }, - }, - }), - state: { - active: { - default: { - color: foreground(theme.middle, "accent"), - }, - hovered: { - color: foreground(theme.middle, "accent", "hovered"), - }, - clicked: { - color: foreground(theme.middle, "accent", "pressed"), - }, - }, + header: { + ...text(theme.middle, "sans", "on", { size: "lg" }), + padding: { + left: 6, + } + }, + mode_button: toggleable_text_button(theme, { + variant: "ghost", + layer: theme.middle, + active_color: "accent", + margin: { + top: 8, + bottom: 8, + right: 4 } }), picker: { From 9b5551a079e93a7e698efd79e94df8e9e76d15b6 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 11 Aug 2023 11:35:51 -0700 Subject: [PATCH 162/326] split into body and header --- .../src/collab_panel/channel_modal.rs | 53 +++++++++++-------- crates/theme/src/theme.rs | 9 ++-- styles/src/style_tree/channel_modal.ts | 36 ++++++++----- 3 files changed, 60 insertions(+), 38 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 77401d269c..f72eafe7da 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -181,31 +181,42 @@ impl View for ChannelModal { Flex::column() .with_child( - Label::new(format!("#{}", channel.name), theme.header.text.clone()) + Flex::column() + .with_child( + Label::new(format!("#{}", channel.name), theme.title.text.clone()) + .contained() + .with_style(theme.title.container.clone()), + ) + .with_child(Flex::row().with_children([ + render_mode_button::( + Mode::InviteMembers, + "Invite members", + mode, + theme, + cx, + ), + render_mode_button::( + Mode::ManageMembers, + "Manage members", + mode, + theme, + cx, + ), + ])) + .expanded() .contained() - .with_style(theme.header.container.clone()), + .with_style(theme.header), + ) + .with_child( + ChildView::new(&self.picker, cx) + .contained() + .with_style(theme.body), ) - .with_child(Flex::row().with_children([ - render_mode_button::( - Mode::InviteMembers, - "Invite members", - mode, - theme, - cx, - ), - render_mode_button::( - Mode::ManageMembers, - "Manage members", - mode, - theme, - cx, - ), - ])) - .with_child(ChildView::new(&self.picker, cx)) .constrained() - .with_max_height(theme.height) + .with_max_height(theme.max_height) + .with_max_width(theme.max_width) .contained() - .with_style(theme.container) + .with_style(theme.modal) .into_any() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9025bf1cd2..f455cfca73 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -251,9 +251,9 @@ pub struct CollabPanel { #[derive(Deserialize, Default, JsonSchema)] pub struct ChannelModal { - pub container: ContainerStyle, - pub height: f32, - pub header: ContainedText, + pub max_height: f32, + pub max_width: f32, + pub title: ContainedText, pub mode_button: Toggleable>, pub picker: Picker, pub row_height: f32, @@ -264,6 +264,9 @@ pub struct ChannelModal { pub member_icon: Icon, pub invitee_icon: Icon, pub member_tag: ContainedText, + pub modal: ContainerStyle, + pub header: ContainerStyle, + pub body: ContainerStyle, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 764ab9fc93..d09ab2db7b 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -24,6 +24,25 @@ export default function channel_modal(): any { const picker_input = input() return { + header: { + background: background(theme.middle, "accent"), + border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), + }, + body: { + background: background(theme.middle), + }, + modal: { + background: background(theme.middle), + shadow: theme.modal_shadow, + corner_radius: 12, + padding: { + bottom: 0, + left: 0, + right: 0, + top: 0, + }, + + }, // This is used for the icons that are rendered to the right of channel Members in both UIs member_icon: { background: background(theme.middle), @@ -72,20 +91,9 @@ export default function channel_modal(): any { right: 4, } }, - container: { - background: background(theme.middle), - border: border(theme.middle), - shadow: theme.modal_shadow, - corner_radius: 12, - padding: { - bottom: 0, - left: 0, - right: 0, - top: 0, - }, - }, - height: 400, - header: { + max_height: 400, + max_width: 540, + title: { ...text(theme.middle, "sans", "on", { size: "lg" }), padding: { left: 6, From c0356fdf16320347c18c95acbab326c5b505d482 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 23:47:16 +0200 Subject: [PATCH 163/326] Decrease row count for buffer search bar --- crates/search/src/buffer_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e55d91ac1c..505e871af7 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -368,7 +368,7 @@ impl ToolbarItemView for BufferSearchBar { } } fn row_count(&self, _: &ViewContext) -> usize { - 3 + 2 } } From 94f1d8281ab326b19e72dd7cdd80f6f7782f89ee Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 12 Aug 2023 22:18:27 +0200 Subject: [PATCH 164/326] Remove buttonside, use new corner_radii instead --- crates/search/src/elements.rs | 163 ---------------------------- crates/search/src/project_search.rs | 4 +- crates/search/src/search.rs | 1 - crates/search/src/search_bar.rs | 117 +++++++------------- styles/src/style_tree/search.ts | 2 +- 5 files changed, 42 insertions(+), 245 deletions(-) delete mode 100644 crates/search/src/elements.rs diff --git a/crates/search/src/elements.rs b/crates/search/src/elements.rs deleted file mode 100644 index 0a7990c24a..0000000000 --- a/crates/search/src/elements.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::ops::Range; - -use gpui::color::Color; -use gpui::geometry::rect::RectF; -use gpui::geometry::vector::IntoVector2F; -use gpui::json::{self, ToJson}; -use gpui::{scene::Path, LayoutContext}; -use gpui::{Element, PaintContext, SceneBuilder, View, ViewContext}; - -type CreatePath = fn(RectF, Color, f32) -> Path; -type AdjustBorder = fn(RectF, f32) -> RectF; -type BorderThickness = f32; - -pub(crate) struct ButtonSide { - color: Color, - factory: CreatePath, - /// After the outline is drawn with border color, - /// the drawing bounds have to be adjusted by different factors in different dimensions. - border_adjustment: AdjustBorder, - border: Option<(BorderThickness, Color)>, - radius: f32, -} - -impl ButtonSide { - fn new( - color: Color, - factory: CreatePath, - border_adjustment: AdjustBorder, - radius: f32, - ) -> Self { - Self { - color, - factory, - border_adjustment, - border: None, - radius, - } - } - pub fn with_border(mut self, width: f32, color: Color) -> Self { - self.border = Some((width, color)); - self - } - pub fn left(color: Color, corner_radius: f32) -> Self { - Self::new( - color, - left_button_side, - left_button_border_adjust, - corner_radius, - ) - } - pub fn right(color: Color, corner_radius: f32) -> Self { - Self::new( - color, - right_button_side, - right_button_border_adjust, - corner_radius, - ) - } -} - -fn left_button_border_adjust(bounds: RectF, width: f32) -> RectF { - let width = width.into_vector_2f(); - let mut lower_right = bounds.clone().lower_right(); - lower_right.set_x(lower_right.x() + width.x()); - RectF::from_points(bounds.origin() + width, lower_right) -} -fn right_button_border_adjust(bounds: RectF, width: f32) -> RectF { - let width = width.into_vector_2f(); - let mut origin = bounds.clone().origin(); - origin.set_x(origin.x() - width.x()); - RectF::from_points(origin, bounds.lower_right() - width) -} -fn left_button_side(bounds: RectF, color: Color, radius: f32) -> Path { - use gpui::geometry::PathBuilder; - let mut path = PathBuilder::new(); - path.reset(bounds.lower_right()); - path.line_to(bounds.upper_right()); - let mut middle_point = bounds.origin(); - let distance_to_line = (middle_point.y() - bounds.lower_left().y()).min(-radius.abs()); - middle_point.set_y(middle_point.y() - distance_to_line); - path.curve_to(middle_point, bounds.origin()); - let mut target = bounds.lower_left(); - target.set_y(target.y() + distance_to_line); - path.line_to(target); - path.curve_to(bounds.lower_right(), bounds.lower_left()); - path.build(color, None) -} - -fn right_button_side(bounds: RectF, color: Color, radius: f32) -> Path { - use gpui::geometry::PathBuilder; - let mut path = PathBuilder::new(); - path.reset(bounds.lower_left()); - path.line_to(bounds.origin()); - let mut middle_point = bounds.upper_right(); - let distance_to_line = (middle_point.y() - bounds.lower_right().y()).min(-radius.abs()); - middle_point.set_y(middle_point.y() - distance_to_line); - path.curve_to(middle_point, bounds.upper_right()); - let mut target = bounds.lower_right(); - target.set_y(target.y() + distance_to_line); - path.line_to(target); - path.curve_to(bounds.lower_left(), bounds.lower_right()); - path.build(color, None) -} - -impl Element for ButtonSide { - type LayoutState = (); - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - _: &mut V, - _: &mut LayoutContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - (constraint.max, ()) - } - - fn paint( - &mut self, - scene: &mut SceneBuilder, - bounds: RectF, - _: RectF, - _: &mut Self::LayoutState, - _: &mut V, - _: &mut PaintContext, - ) -> Self::PaintState { - let mut bounds = bounds; - if let Some((border_width, border_color)) = self.border.as_ref() { - scene.push_path((self.factory)(bounds, border_color.clone(), self.radius)); - bounds = (self.border_adjustment)(bounds, *border_width); - }; - scene.push_path((self.factory)(bounds, self.color, self.radius)); - } - - fn rect_for_text_range( - &self, - _: Range, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> Option { - None - } - - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> gpui::json::Value { - json::json!({ - "type": "ButtonSide", - "bounds": bounds.to_json(), - "color": self.color.to_json(), - }) - } -} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index db48fa14c3..5589688368 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1593,7 +1593,7 @@ impl View for ProjectSearchBar { .left(), ) .constrained() - .with_max_height(theme.search.search_bar_row_height) + .with_height(theme.search.search_bar_row_height) .flex(1., true), ) .with_child( @@ -1642,7 +1642,7 @@ impl View for ProjectSearchBar { None, )) .constrained() - .with_height(theme.workspace.toolbar.height) + .with_height(theme.search.search_bar_row_height) .contained() .aligned() .right() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 33905f83ec..079a8965eb 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -6,7 +6,6 @@ use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; pub mod buffer_search; -mod elements; mod history; mod mode; pub mod project_search; diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index ecfb51b1c6..afc0d109d6 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,13 +1,12 @@ use gpui::{ elements::{Flex, Label, MouseEventHandler, ParentElement, Svg}, platform::{CursorStyle, MouseButton}, - scene::MouseClick, + scene::{CornerRadii, MouseClick}, Action, AnyElement, Element, EventContext, View, ViewContext, }; use workspace::searchable::Direction; use crate::{ - elements::ButtonSide, mode::{SearchMode, Side}, SearchOptions, SelectNextMatch, SelectPrevMatch, }; @@ -65,48 +64,31 @@ pub(super) fn render_nav_button( MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let mut style = theme.search.nav_button.style_for(state).clone(); - let button_side_width = style.container.corner_radii.top_left; - style.container.corner_radii = (0.).into(); - let label = Label::new(icon, style.label.clone()) - .contained() - .with_style(style.container.clone()); + let mut container_style = style.container.clone(); + let label = Label::new(icon, style.label.clone()).contained(); match direction { - Direction::Prev => Flex::row() - .with_child( - ButtonSide::left( - style - .clone() - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - button_side_width, - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() + Direction::Prev => { + container_style.corner_radii = CornerRadii { + bottom_right: 0., + top_right: 0., + ..container_style.corner_radii + }; + Flex::row() + .with_child(label.with_style(container_style)) .constrained() - .with_max_width(button_side_width), - ) - .with_child(label) - .constrained() - .with_height(theme.workspace.toolbar.height), - Direction::Next => Flex::row() - .with_child(label) - .with_child( - ButtonSide::right( - style - .clone() - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - button_side_width, - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() + //.with_height(theme.workspace.toolbar.height) + } + Direction::Next => { + container_style.corner_radii = CornerRadii { + bottom_left: 0., + top_left: 0., + ..container_style.corner_radii + }; + Flex::row() + .with_child(label.with_style(container_style)) .constrained() - .with_max_width(button_side_width), - ) - .constrained() - .with_height(theme.workspace.toolbar.height), + // .with_height(theme.workspace.toolbar.height) + } } }) .on_click(MouseButton::Left, on_click) @@ -137,58 +119,37 @@ pub(crate) fn render_search_mode_button( .in_state(is_active) .style_for(state) .clone(); - let side_width = style.container.corner_radii.top_left; - style.container.corner_radii = (0.).into(); if mode.button_side().is_some() { style.container.border.left = mode.border_left(); style.container.border.right = mode.border_right(); } - let label = Label::new(mode.label(), style.text.clone()) - .contained() - .with_style(style.container); - + let label = Label::new(mode.label(), style.text.clone()).contained(); + let mut container_style = style.container.clone(); if let Some(button_side) = mode.button_side() { if button_side == Side::Left { + container_style.corner_radii = CornerRadii { + bottom_right: 0., + top_right: 0., + ..container_style.corner_radii + }; Flex::row() .align_children_center() - .with_child( - ButtonSide::left( - style - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - side_width, - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(side_width) - .with_height(theme.search.search_bar_row_height), - ) - .with_child(label) + .with_child(label.with_style(container_style)) .into_any() } else { + container_style.corner_radii = CornerRadii { + bottom_left: 0., + top_left: 0., + ..container_style.corner_radii + }; Flex::row() .align_children_center() - .with_child(label) - .with_child( - ButtonSide::right( - style - .container - .background_color - .unwrap_or_else(gpui::color::Color::transparent_black), - side_width, - ) - .with_border(style.container.border.width, style.container.border.color) - .contained() - .constrained() - .with_max_width(side_width) - .with_height(theme.search.search_bar_row_height), - ) + .with_child(label.with_style(container_style)) .into_any() } } else { - label.into_any() + container_style.corner_radii = CornerRadii::default(); + label.with_style(container_style).into_any() } }) .on_click(MouseButton::Left, on_click) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 6a6b2cc3ac..e853beacdd 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -245,7 +245,7 @@ export default function search(): any { base: { text: text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - corner_radius: 6, + corner_radius: { top_left: 6, top_right: 6, bottom_right: 6, bottom_left: 6 }, border: { ...border(theme.highest, "on"), left: false, From c6425b36dadf442fecd55194c0198c30b2f6bbf7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 12 Aug 2023 22:20:22 +0200 Subject: [PATCH 165/326] chore: remove redundant mut --- crates/search/src/search_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index afc0d109d6..8bc444ed67 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -63,7 +63,7 @@ pub(super) fn render_nav_button( enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); - let mut style = theme.search.nav_button.style_for(state).clone(); + let style = theme.search.nav_button.style_for(state).clone(); let mut container_style = style.container.clone(); let label = Label::new(icon, style.label.clone()).contained(); match direction { From e151555542a28505706808288a2bdff3c60d6935 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 13 Aug 2023 01:06:35 +0200 Subject: [PATCH 166/326] Style dismiss button. Fix clipping in nav buttons and mode buttons. Add missing borders to outskirts of mode buttons. --- crates/search/src/mode.rs | 4 +-- crates/search/src/project_search.rs | 49 ++++++++++++++++---------- crates/search/src/search_bar.rs | 53 ++++++++++------------------- styles/src/style_tree/search.ts | 27 ++++++++++----- 4 files changed, 69 insertions(+), 64 deletions(-) diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index bb620f0670..ecaa00f6ac 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -49,14 +49,14 @@ impl SearchMode { } } - pub(crate) fn border_left(&self) -> bool { + pub(crate) fn border_right(&self) -> bool { match self { SearchMode::Text => false, _ => true, } } - pub(crate) fn border_right(&self) -> bool { + pub(crate) fn border_left(&self) -> bool { match self { SearchMode::Regex => false, _ => true, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5589688368..a462327ec0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1590,10 +1590,10 @@ impl View for ProjectSearchBar { .with_children(matches) .aligned() .top() - .left(), + .left() + .constrained() + .with_height(theme.search.search_bar_row_height), ) - .constrained() - .with_height(theme.search.search_bar_row_height) .flex(1., true), ) .with_child( @@ -1627,23 +1627,36 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .align_children_center() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .with_child(super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - )) + .with_child( + Flex::row() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_children(semantic_index) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .aligned() + .left() + .contained() + .with_margin_right(3.), + ) + .with_child( + super::search_bar::render_close_button( + "Dismiss Project Search", + &theme.search, + cx, + |_, this, cx| { + if let Some(search) = + this.active_project_search.as_mut() + { + search + .update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) + } + }, + None, + ) + .aligned() + .right(), + ) .constrained() .with_height(theme.search.search_bar_row_height) - .contained() .aligned() .right() .top() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 8bc444ed67..f0ab9a99fb 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -28,10 +28,10 @@ pub(super) fn render_close_button( .constrained() .with_width(style.icon_width) .aligned() - .constrained() - .with_width(style.button_width) .contained() .with_style(style.container) + .constrained() + .with_height(theme.search_bar_row_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) @@ -66,30 +66,19 @@ pub(super) fn render_nav_button( let style = theme.search.nav_button.style_for(state).clone(); let mut container_style = style.container.clone(); let label = Label::new(icon, style.label.clone()).contained(); - match direction { - Direction::Prev => { - container_style.corner_radii = CornerRadii { - bottom_right: 0., - top_right: 0., - ..container_style.corner_radii - }; - Flex::row() - .with_child(label.with_style(container_style)) - .constrained() - //.with_height(theme.workspace.toolbar.height) - } - Direction::Next => { - container_style.corner_radii = CornerRadii { - bottom_left: 0., - top_left: 0., - ..container_style.corner_radii - }; - Flex::row() - .with_child(label.with_style(container_style)) - .constrained() - // .with_height(theme.workspace.toolbar.height) - } - } + container_style.corner_radii = match direction { + Direction::Prev => CornerRadii { + bottom_right: 0., + top_right: 0., + ..container_style.corner_radii + }, + Direction::Next => CornerRadii { + bottom_left: 0., + top_left: 0., + ..container_style.corner_radii + }, + }; + label.with_style(container_style) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) @@ -132,24 +121,18 @@ pub(crate) fn render_search_mode_button( top_right: 0., ..container_style.corner_radii }; - Flex::row() - .align_children_center() - .with_child(label.with_style(container_style)) - .into_any() + label.with_style(container_style) } else { container_style.corner_radii = CornerRadii { bottom_left: 0., top_left: 0., ..container_style.corner_radii }; - Flex::row() - .align_children_center() - .with_child(label.with_style(container_style)) - .into_any() + label.with_style(container_style) } } else { container_style.corner_radii = CornerRadii::default(); - label.with_style(container_style).into_any() + label.with_style(container_style) } }) .on_click(MouseButton::Left, on_click) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index e853beacdd..a4428fe4a8 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -161,19 +161,28 @@ export default function search(): any { dismiss_button: interactive({ base: { color: foreground(theme.highest, "variant"), - icon_width: 12, - button_width: 14, + icon_width: 14, + button_width: 32, + corner_radius: 6, padding: { - left: 10, - right: 10, + top: 8, + bottom: 8, + left: 8, + right: 8, }, + + background: background(theme.highest, "variant"), + + border: border(theme.highest, "on"), }, state: { hovered: { color: foreground(theme.highest, "hovered"), + background: background(theme.highest, "variant", "hovered") }, clicked: { color: foreground(theme.highest, "pressed"), + background: background(theme.highest, "variant", "pressed") }, }, }), @@ -245,7 +254,7 @@ export default function search(): any { base: { text: text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - corner_radius: { top_left: 6, top_right: 6, bottom_right: 6, bottom_left: 6 }, + corner_radius: 6, border: { ...border(theme.highest, "on"), left: false, @@ -253,10 +262,10 @@ export default function search(): any { }, padding: { - bottom: 6, - left: 6, - right: 6, - top: 6, + bottom: 3, + left: 10, + right: 10, + top: 3, }, }, state: { From b586601cab24255466b7b593d7d509a334855217 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 13 Aug 2023 01:29:35 +0200 Subject: [PATCH 167/326] Style nav buttons --- crates/search/src/buffer_search.rs | 1 + crates/search/src/project_search.rs | 2 + crates/search/src/search_bar.rs | 18 +++++-- crates/theme/src/theme.rs | 2 +- styles/src/style_tree/search.ts | 84 +++++++++++++++++++---------- 5 files changed, 73 insertions(+), 34 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 505e871af7..5c02874f97 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -209,6 +209,7 @@ impl View for BufferSearchBar { render_nav_button( label, direction, + self.active_match_index.is_some(), move |_, this, cx| match direction { Direction::Prev => this.select_prev_match(&Default::default(), cx), Direction::Next => this.select_next_match(&Default::default(), cx), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a462327ec0..7bc7741882 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1552,12 +1552,14 @@ impl View for ProjectSearchBar { cx, ) }; + let is_active = search.active_match_index.is_some(); let semantic_index = SemanticIndex::enabled(cx) .then(|| search_button_for_mode(SearchMode::Semantic, cx)); let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, direction, + is_active, move |_, this, cx| { if let Some(search) = this.active_project_search.as_ref() { search.update(cx, |search, cx| search.select_match(direction, cx)); diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index f0ab9a99fb..19a40a4a78 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,5 +1,5 @@ use gpui::{ - elements::{Flex, Label, MouseEventHandler, ParentElement, Svg}, + elements::{Label, MouseEventHandler, Svg}, platform::{CursorStyle, MouseButton}, scene::{CornerRadii, MouseClick}, Action, AnyElement, Element, EventContext, View, ViewContext, @@ -42,6 +42,7 @@ pub(super) fn render_close_button( pub(super) fn render_nav_button( icon: &'static str, direction: Direction, + active: bool, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, cx: &mut ViewContext, ) -> AnyElement { @@ -59,11 +60,20 @@ pub(super) fn render_nav_button( } }; let tooltip_style = theme::current(cx).tooltip.clone(); - + let cursor_style = if active { + CursorStyle::PointingHand + } else { + CursorStyle::default() + }; enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.nav_button.style_for(state).clone(); + let style = theme + .search + .nav_button + .in_state(active) + .style_for(state) + .clone(); let mut container_style = style.container.clone(); let label = Label::new(icon, style.label.clone()).contained(); container_style.corner_radii = match direction { @@ -81,7 +91,7 @@ pub(super) fn render_nav_button( label.with_style(container_style) }) .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) + .with_cursor_style(cursor_style) .with_tooltip::( direction as usize, tooltip.to_string(), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index dbcffe0382..41a9eaaba5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -388,7 +388,7 @@ pub struct Search { pub dismiss_button: Interactive, pub editor_icon: IconStyle, pub mode_button: Toggleable>, - pub nav_button: Interactive, + pub nav_button: Toggleable>, pub search_bar_row_height: f32, } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index a4428fe4a8..16de861310 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -250,36 +250,62 @@ export default function search(): any { }, }, }), - nav_button: interactive({ - base: { - text: text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - corner_radius: 6, - border: { - ...border(theme.highest, "on"), - left: false, - right: false, - }, - - padding: { - bottom: 3, - left: 10, - right: 10, - top: 3, - }, - }, + nav_button: toggleable({ state: { - hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), - }, - clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), - }, - }, + inactive: interactive({ + base: { + background: background(theme.highest, "disabled"), + text: text(theme.highest, "mono", "disabled"), + corner_radius: 6, + border: { + ...border(theme.highest, "disabled"), + left: false, + right: false, + }, + + padding: { + bottom: 3, + left: 10, + right: 10, + top: 3, + }, + }, + state: { + hovered: {} + } + }), + active: interactive({ + base: { + text: text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 6, + border: { + ...border(theme.highest, "on"), + left: false, + right: false, + }, + + padding: { + bottom: 3, + left: 10, + right: 10, + top: 3, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }) + } }), search_bar_row_height: 32, From 5d2750e0d4e5e9206146cef07c564beb92168cb7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 14:39:37 +0300 Subject: [PATCH 168/326] Hide inlay cache fields --- crates/collab/src/tests/integration_tests.rs | 42 +++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 23 ++++++++--- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ce7fd8a094..657457d592 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7953,7 +7953,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host editor update the cache version after every cache/view change", ); }); @@ -7976,7 +7977,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -7996,7 +7998,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host should get hints from the 1st edit and 1st LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8010,7 +8012,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_a.update(cx_a, |editor, cx| { @@ -8035,7 +8037,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( 4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8051,7 +8053,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); fake_language_server @@ -8077,7 +8079,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host should accepted all edits and bump its cache version every time" ); }); @@ -8098,7 +8101,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, + inlay_cache.version(), edits_made, "Guest should accepted all edits and bump its cache version every time" ); @@ -8264,7 +8267,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8279,7 +8283,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -8296,7 +8301,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8311,7 +8317,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest should accepted all edits and bump its cache version every time" ); }); @@ -8343,13 +8350,10 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP fn extract_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); - for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), } } labels diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2d75b4d2ce..44fbbf163d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -20,10 +20,10 @@ use language::language_settings::InlayHintSettings; use util::post_inc; pub struct InlayHintCache { - pub hints: HashMap>>, - pub allowed_hint_kinds: HashSet>, - pub version: usize, - pub enabled: bool, + hints: HashMap>>, + allowed_hint_kinds: HashSet>, + version: usize, + enabled: bool, update_tasks: HashMap, } @@ -32,7 +32,7 @@ pub struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: u64, - pub hints: Vec<(InlayId, InlayHint)>, + hints: Vec<(InlayId, InlayHint)>, } #[derive(Debug, Clone, Copy)] @@ -368,6 +368,19 @@ impl InlayHintCache { self.update_tasks.clear(); self.hints.clear(); } + + pub fn hints(&self) -> Vec { + let mut hints = Vec::new(); + for excerpt_hints in self.hints.values() { + let excerpt_hints = excerpt_hints.read(); + hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned()); + } + hints + } + + pub fn version(&self) -> usize { + self.version + } } fn spawn_new_update_tasks( From 708409e06d381269a21dedbd02fa86b30c5d4047 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 14:51:38 +0300 Subject: [PATCH 169/326] Query hints on every scroll --- crates/editor/src/inlay_hint_cache.rs | 4 +++- crates/editor/src/scroll.rs | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 44fbbf163d..1dbef165fa 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -24,6 +24,7 @@ pub struct InlayHintCache { allowed_hint_kinds: HashSet>, version: usize, enabled: bool, + // TODO kb track them by excerpt range update_tasks: HashMap, } @@ -100,6 +101,7 @@ impl InvalidationStrategy { } impl ExcerptQuery { + // TODO kb query only visible + one visible below and above fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { let visible_range = self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; @@ -168,7 +170,6 @@ impl InlayHintCache { ); if new_splice.is_some() { self.version += 1; - self.update_tasks.clear(); self.allowed_hint_kinds = new_allowed_hint_kinds; } ControlFlow::Break(new_splice) @@ -464,6 +465,7 @@ fn spawn_new_update_tasks( cx, ) }; + // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { hash_map::Entry::Occupied(mut o) => { let update_task = o.get_mut(); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d595337428..1f3adaf477 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Bias, Point}; use util::ResultExt; -use workspace::{item::Item, WorkspaceId}; +use workspace::WorkspaceId; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -333,9 +333,7 @@ impl Editor { cx, ); - if !self.is_singleton(cx) { - self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); - } + self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { From 0e2a1fc14996c5831c1060fe7cf3ddb3670a6626 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 18:09:09 +0300 Subject: [PATCH 170/326] Query inlay hints for parts of the file --- crates/editor/src/inlay_hint_cache.rs | 396 ++++++++++---------------- 1 file changed, 145 insertions(+), 251 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1dbef165fa..30f02c17f5 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; -use gpui::{ModelHandle, Task, ViewContext}; +use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; @@ -24,8 +24,13 @@ pub struct InlayHintCache { allowed_hint_kinds: HashSet>, version: usize, enabled: bool, - // TODO kb track them by excerpt range - update_tasks: HashMap, + update_tasks: HashMap, +} + +#[derive(Debug)] +struct TasksForRanges { + tasks: Vec>, + ranges: Vec>, } #[derive(Debug)] @@ -49,18 +54,6 @@ pub struct InlaySplice { pub to_insert: Vec, } -struct UpdateTask { - invalidate: InvalidationStrategy, - cache_version: usize, - task: RunningTask, - pending_refresh: Option, -} - -struct RunningTask { - _task: Task<()>, - is_running_rx: smol::channel::Receiver<()>, -} - #[derive(Debug)] struct ExcerptHintsUpdate { excerpt_id: ExcerptId, @@ -73,24 +66,10 @@ struct ExcerptHintsUpdate { struct ExcerptQuery { buffer_id: u64, excerpt_id: ExcerptId, - dimensions: ExcerptDimensions, cache_version: usize, invalidate: InvalidationStrategy, } -#[derive(Debug, Clone, Copy)] -struct ExcerptDimensions { - excerpt_range_start: language::Anchor, - excerpt_range_end: language::Anchor, - excerpt_visible_range_start: language::Anchor, - excerpt_visible_range_end: language::Anchor, -} - -struct HintFetchRanges { - visible_range: Range, - other_ranges: Vec>, -} - impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( @@ -100,37 +79,43 @@ impl InvalidationStrategy { } } -impl ExcerptQuery { - // TODO kb query only visible + one visible below and above - fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { - let visible_range = - self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; - let mut other_ranges = Vec::new(); - if self - .dimensions - .excerpt_range_start - .cmp(&visible_range.start, buffer) - .is_lt() - { - let mut end = visible_range.start; - end.offset -= 1; - other_ranges.push(self.dimensions.excerpt_range_start..end); - } - if self - .dimensions - .excerpt_range_end - .cmp(&visible_range.end, buffer) - .is_gt() - { - let mut start = visible_range.end; - start.offset += 1; - other_ranges.push(start..self.dimensions.excerpt_range_end); +impl TasksForRanges { + fn new(ranges: Vec>, task: Task<()>) -> Self { + Self { + tasks: vec![task], + ranges, } + } - HintFetchRanges { - visible_range, - other_ranges: other_ranges.into_iter().map(|range| range).collect(), - } + fn update_cached_tasks( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + invalidate: InvalidationStrategy, + spawn_task: impl FnOnce(Vec>) -> Task<()>, + ) { + let ranges_to_query = match invalidate { + InvalidationStrategy::None => { + // let mut ranges_to_query = Vec::new(); + + // todo!("TODO kb also remove task ranges on invalidation"); + // if ranges_to_query.is_empty() { + // return; + // } + // ranges_to_query + vec![query_range] + } + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { + self.tasks.clear(); + self.ranges.clear(); + vec![query_range] + } + }; + + self.ranges.extend(ranges_to_query.clone()); + self.ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + self.tasks.push(spawn_task(ranges_to_query)); } } @@ -198,7 +183,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, - mut excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) -> Option { @@ -206,11 +191,10 @@ impl InlayHintCache { return None; } - let update_tasks = &mut self.update_tasks; let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { let mut changed = false; - update_tasks.retain(|task_excerpt_id, _| { + self.update_tasks.retain(|task_excerpt_id, _| { let retain = excerpts_to_query.contains_key(task_excerpt_id); changed |= !retain; retain @@ -232,17 +216,6 @@ impl InlayHintCache { } let cache_version = self.version; - excerpts_to_query.retain(|visible_excerpt_id, _| { - match update_tasks.entry(*visible_excerpt_id) { - hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) { - cmp::Ordering::Less => true, - cmp::Ordering::Equal => invalidate.should_invalidate(), - cmp::Ordering::Greater => false, - }, - hash_map::Entry::Vacant(_) => true, - } - }); - cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -392,13 +365,14 @@ fn spawn_new_update_tasks( cx: &mut ViewContext<'_, '_, Editor>, ) { let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in + for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in excerpts_to_query { if excerpt_visible_range.is_empty() { continue; } - let buffer = buffer_handle.read(cx); + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); let buffer_snapshot = buffer.snapshot(); if buffer_snapshot .version() @@ -416,203 +390,120 @@ fn spawn_new_update_tasks( { continue; } - if !new_task_buffer_version.changed_since(&cached_buffer_version) - && !matches!(invalidate, InvalidationStrategy::RefreshRequested) - { - continue; - } }; - let buffer_id = buffer.remote_id(); - let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start); - let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end); - - let (multi_buffer_snapshot, full_excerpt_range) = + let (multi_buffer_snapshot, Some(query_range)) = editor.buffer.update(cx, |multi_buffer, cx| { - let multi_buffer_snapshot = multi_buffer.snapshot(cx); ( - multi_buffer_snapshot, - multi_buffer - .excerpts_for_buffer(&buffer_handle, cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context), + multi_buffer.snapshot(cx), + determine_query_range( + multi_buffer, + excerpt_id, + &excerpt_buffer, + excerpt_visible_range, + cx, + ), ) - }); + }) else { return; }; + let query = ExcerptQuery { + buffer_id, + excerpt_id, + cache_version: update_cache_version, + invalidate, + }; - if let Some(full_excerpt_range) = full_excerpt_range { - let query = ExcerptQuery { - buffer_id, - excerpt_id, - dimensions: ExcerptDimensions { - excerpt_range_start: full_excerpt_range.start, - excerpt_range_end: full_excerpt_range.end, - excerpt_visible_range_start, - excerpt_visible_range_end, - }, - cache_version: update_cache_version, - invalidate, - }; - - let new_update_task = |is_refresh_after_regular_task| { - new_update_task( - query, - multi_buffer_snapshot, - buffer_snapshot, - Arc::clone(&visible_hints), - cached_excerpt_hints, - is_refresh_after_regular_task, - cx, - ) - }; - // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - let update_task = o.get_mut(); - match (update_task.invalidate, invalidate) { - (_, InvalidationStrategy::None) => {} - ( - InvalidationStrategy::BufferEdited, - InvalidationStrategy::RefreshRequested, - ) if !update_task.task.is_running_rx.is_closed() => { - update_task.pending_refresh = Some(query); - } - _ => { - o.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } - } - } - hash_map::Entry::Vacant(v) => { - v.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } + let new_update_task = |fetch_ranges| { + new_update_task( + query, + fetch_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + cx, + ) + }; + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().update_cached_tasks( + &buffer_snapshot, + query_range, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + vec![query_range.clone()], + new_update_task(vec![query_range]), + )); } } } } +fn determine_query_range( + multi_buffer: &mut MultiBuffer, + excerpt_id: ExcerptId, + excerpt_buffer: &ModelHandle, + excerpt_visible_range: Range, + cx: &mut ModelContext<'_, MultiBuffer>, +) -> Option> { + let full_excerpt_range = multi_buffer + .excerpts_for_buffer(excerpt_buffer, cx) + .into_iter() + .find(|(id, _)| id == &excerpt_id) + .map(|(_, range)| range.context)?; + + let buffer = excerpt_buffer.read(cx); + let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; + let start = buffer.anchor_before( + excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range.start.offset), + ); + let end = buffer.anchor_after( + excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range.end.offset) + .min(buffer.len()), + ); + Some(start..end) +} + fn new_update_task( query: ExcerptQuery, + hints_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, - is_refresh_after_regular_task: bool, cx: &mut ViewContext<'_, '_, Editor>, -) -> RunningTask { - let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot); - let (is_running_tx, is_running_rx) = smol::channel::bounded(1); - let _task = cx.spawn(|editor, mut cx| async move { - let _is_running_tx = is_running_tx; - let create_update_task = |range| { - fetch_and_update_hints( - editor.clone(), - multi_buffer_snapshot.clone(), - buffer_snapshot.clone(), - Arc::clone(&visible_hints), - cached_excerpt_hints.as_ref().map(Arc::clone), - query, - range, - cx.clone(), - ) - }; - - if is_refresh_after_regular_task { - let visible_range_has_updates = - match create_update_task(hints_fetch_ranges.visible_range).await { - Ok(updated) => updated, - Err(e) => { - error!("inlay hint visible range update task failed: {e:#}"); - return; - } - }; - - if visible_range_has_updates { - let other_update_results = futures::future::join_all( - hints_fetch_ranges - .other_ranges - .into_iter() - .map(create_update_task), +) -> Task<()> { + cx.spawn(|editor, cx| async move { + let task_update_results = + futures::future::join_all(hints_fetch_ranges.into_iter().map(|range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + range, + cx.clone(), ) - .await; - - for result in other_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } - } - } - } else { - let task_update_results = futures::future::join_all( - std::iter::once(hints_fetch_ranges.visible_range) - .chain(hints_fetch_ranges.other_ranges.into_iter()) - .map(create_update_task), - ) + })) .await; - for result in task_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } + for result in task_update_results { + if let Err(e) = result { + error!("inlay hint update task failed: {e:#}"); } } - - editor - .update(&mut cx, |editor, cx| { - let pending_refresh_query = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - .and_then(|task| task.pending_refresh.take()); - - if let Some(pending_refresh_query) = pending_refresh_query { - let refresh_multi_buffer = editor.buffer().read(cx); - let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx); - let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - let refresh_cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .get(&pending_refresh_query.excerpt_id) - .map(Arc::clone); - if let Some(buffer) = - refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) - { - editor.inlay_hint_cache.update_tasks.insert( - pending_refresh_query.excerpt_id, - UpdateTask { - invalidate: InvalidationStrategy::RefreshRequested, - cache_version: editor.inlay_hint_cache.version, - task: new_update_task( - pending_refresh_query, - refresh_multi_buffer_snapshot, - buffer.read(cx).snapshot(), - refresh_visible_hints, - refresh_cached_excerpt_hints, - true, - cx, - ), - pending_refresh: None, - }, - ); - } - } - }) - .ok(); - }); - - RunningTask { - _task, - is_running_rx, - } + }) } async fn fetch_and_update_hints( @@ -2202,7 +2093,8 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // TODO kb find the range needed + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2227,7 +2119,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2255,7 +2147,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2284,6 +2176,8 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), + // TODO kb why? + "main hint(edited) #3".to_string(), "main hint(edited) #4".to_string(), "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), From 56f89739f888c9122f54de48d701a52ced806f91 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 17:36:34 +0300 Subject: [PATCH 171/326] Do not add duplicate hints to the cache --- crates/editor/src/inlay_hint_cache.rs | 106 +++++++++++++------------- crates/editor/src/scroll.rs | 1 + 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 30f02c17f5..e792064ec7 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -193,29 +193,21 @@ impl InlayHintCache { let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { - let mut changed = false; - self.update_tasks.retain(|task_excerpt_id, _| { - let retain = excerpts_to_query.contains_key(task_excerpt_id); - changed |= !retain; - retain - }); + self.update_tasks + .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); - changed |= !retain; if !retain { invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); } retain }); - if changed { - self.version += 1; - } } if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { return None; } - let cache_version = self.version; + let cache_version = self.version + 1; cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -475,7 +467,7 @@ fn determine_query_range( fn new_update_task( query: ExcerptQuery, - hints_fetch_ranges: Vec>, + hint_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -484,7 +476,7 @@ fn new_update_task( ) -> Task<()> { cx.spawn(|editor, cx| async move { let task_update_results = - futures::future::join_all(hints_fetch_ranges.into_iter().map(|range| { + futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { fetch_and_update_hints( editor.clone(), multi_buffer_snapshot.clone(), @@ -515,7 +507,7 @@ async fn fetch_and_update_hints( query: ExcerptQuery, fetch_range: Range, mut cx: gpui::AsyncAppContext, -) -> anyhow::Result { +) -> anyhow::Result<()> { let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { editor @@ -531,8 +523,7 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let mut update_happened = false; - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) }; + let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(()) }; let new_hints = inlay_hints_fetch_task .await .context("inlay hint fetch task")?; @@ -555,10 +546,6 @@ async fn fetch_and_update_hints( editor .update(&mut cx, |editor, cx| { if let Some(new_update) = new_update { - update_happened = !new_update.add_to_cache.is_empty() - || !new_update.remove_from_cache.is_empty() - || !new_update.remove_from_visible.is_empty(); - let cached_excerpt_hints = editor .inlay_hint_cache .hints @@ -578,43 +565,51 @@ async fn fetch_and_update_hints( cached_excerpt_hints.version = query.cache_version; } } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); cached_excerpt_hints .hints .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - editor.inlay_hint_cache.version += 1; - let mut splice = InlaySplice { to_remove: new_update.remove_from_visible, to_insert: Vec::new(), }; - for new_hint in new_update.add_to_cache { - let new_hint_position = multi_buffer_snapshot - .anchor_in_excerpt(query.excerpt_id, new_hint.position); - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - { - splice.to_insert.push(Inlay::hint( - new_inlay_id, - new_hint_position, - &new_hint, - )); + let cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints.binary_search_by(|probe| { + probe.1.position.cmp(&new_hint.position, &buffer_snapshot) + }) { + Ok(i) => { + if cached_hints[i].1.text() == new_hint.text() { + None + } else { + Some(i) + } + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = multi_buffer_snapshot + .anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice.to_insert.push(Inlay::hint( + new_inlay_id, + new_hint_position, + &new_hint, + )); + } + cached_hints + .insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; } - - cached_excerpt_hints - .hints - .push((InlayId::Hint(new_inlay_id), new_hint)); } - - cached_excerpt_hints - .hints - .sort_by(|(_, hint_a), (_, hint_b)| { - hint_a.position.cmp(&hint_b.position, &buffer_snapshot) - }); + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); if query.invalidate.should_invalidate() { @@ -633,6 +628,7 @@ async fn fetch_and_update_hints( .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); } } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); editor .inlay_hint_cache .hints @@ -643,14 +639,18 @@ async fn fetch_and_update_hints( to_remove, to_insert, } = splice; - if !to_remove.is_empty() || !to_insert.is_empty() { + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { editor.splice_inlay_hints(to_remove, to_insert, cx) } } }) .ok(); - Ok(update_happened) + Ok(()) } fn calculate_hint_updates( @@ -2093,7 +2093,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // TODO kb find the range needed + // TODO kb find the range needed. Is it due to the hint not fitting any excerpt subranges? // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), @@ -2176,8 +2176,6 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), - // TODO kb why? - "main hint(edited) #3".to_string(), "main hint(edited) #4".to_string(), "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), @@ -2192,8 +2190,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - last_scroll_update_version + expected_layers.len() + 1, - "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal" + last_scroll_update_version + expected_layers.len(), + "Due to every excerpt having one hint, cache should update per new excerpt received" ); }); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 1f3adaf477..9a6748fa7c 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -333,6 +333,7 @@ impl Editor { cx, ); + // TODO kb too many events + too many LSP requests even due to deduplication, why? self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } From 449c009639d07d760a33d8caac8dc80e05dac9c5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 23:06:57 +0300 Subject: [PATCH 172/326] Properly generate ranges to query --- crates/editor/src/inlay_hint_cache.rs | 71 ++++++++++++++++++++++----- crates/editor/src/scroll.rs | 1 - 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index e792064ec7..a3edb65128 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -96,14 +96,55 @@ impl TasksForRanges { ) { let ranges_to_query = match invalidate { InvalidationStrategy::None => { - // let mut ranges_to_query = Vec::new(); + let mut ranges_to_query = Vec::new(); + let mut last_cache_range_stop = None::; + for cached_range in self + .ranges + .iter() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match last_cache_range_stop { + Some(last_cache_range_stop) => { + if last_cache_range_stop.offset.saturating_add(1) + < cached_range.start.offset + { + ranges_to_query.push(last_cache_range_stop..cached_range.start); + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + } + } + } + last_cache_range_stop = Some(cached_range.end); + } - // todo!("TODO kb also remove task ranges on invalidation"); - // if ranges_to_query.is_empty() { - // return; - // } - // ranges_to_query - vec![query_range] + match last_cache_range_stop { + Some(last_cache_range_stop) => { + if last_cache_range_stop.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(last_cache_range_stop..query_range.end); + } + } + None => ranges_to_query.push(query_range), + } + + ranges_to_query } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); @@ -112,10 +153,12 @@ impl TasksForRanges { } }; - self.ranges.extend(ranges_to_query.clone()); - self.ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); - self.tasks.push(spawn_task(ranges_to_query)); + if !ranges_to_query.is_empty() { + self.ranges.extend(ranges_to_query.clone()); + self.ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + self.tasks.push(spawn_task(ranges_to_query)); + } } } @@ -462,7 +505,11 @@ fn determine_query_range( .min(full_excerpt_range.end.offset) .min(buffer.len()), ); - Some(start..end) + if start.cmp(&end, buffer).is_eq() { + None + } else { + Some(start..end) + } } fn new_update_task( diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 9a6748fa7c..1f3adaf477 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -333,7 +333,6 @@ impl Editor { cx, ); - // TODO kb too many events + too many LSP requests even due to deduplication, why? self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } From 9121178ba0154b101e587de6e2214cd8bbe2db9f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:59:42 +0200 Subject: [PATCH 173/326] project_search: Swap places of case-sens/whole-word --- crates/search/src/project_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7bc7741882..7389faf031 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1488,8 +1488,8 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .with_child(filter_button) - .with_children(whole_word) .with_children(case_sensitive) + .with_children(whole_word) .flex(1., true) .contained(), ) From d37ebe7841f5d92d428d8ee6e945bca842234658 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:10:37 +0200 Subject: [PATCH 174/326] Add option_button_height style --- crates/search/src/search_bar.rs | 2 +- crates/theme/src/theme.rs | 1 + styles/src/style_tree/search.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 19a40a4a78..10f1a984b5 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -177,7 +177,7 @@ pub(crate) fn render_option_button_icon( .contained() .with_style(style.container) .constrained() - .with_height(22.) + .with_height(theme.search.option_button_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 41a9eaaba5..100b2c676b 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -390,6 +390,7 @@ pub struct Search { pub mode_button: Toggleable>, pub nav_button: Toggleable>, pub search_bar_row_height: f32, + pub option_button_height: f32, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 16de861310..edf9a46e42 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -308,6 +308,7 @@ export default function search(): any { } }), search_bar_row_height: 32, + option_button_height: 22, } } From 1c35db7e97997dea0f0c59201ebbab1ff63c91a2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:20:59 +0200 Subject: [PATCH 175/326] project_search: style filters button like the rest of the buttons --- crates/search/src/buffer_search.rs | 4 ++- crates/search/src/project_search.rs | 51 ++++++++++------------------- crates/search/src/search_bar.rs | 18 +++++----- 3 files changed, 28 insertions(+), 45 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5c02874f97..585672c45b 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -174,7 +174,9 @@ impl View for BufferSearchBar { crate::search_bar::render_option_button_icon::( is_active, icon, - option, + option.bits as usize, + format!("Toggle {}", option.label()), + option.to_toggle_action(), move |_, this, cx| { this.toggle_search_option(option, cx); }, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7389faf031..de0c424e7b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,7 +1,7 @@ use crate::{ history::SearchHistory, mode::SearchMode, - search_bar::{render_nav_button, render_search_mode_button}, + search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; @@ -16,11 +16,9 @@ use futures::StreamExt; use gpui::platform::PromptLevel; use gpui::{ - actions, - elements::*, - platform::{CursorStyle, MouseButton}, - Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, - Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext, + Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, }; use menu::Confirm; @@ -1417,41 +1415,26 @@ impl View for ProjectSearchBar { .flex(1.0, true); let row_spacing = theme.workspace.toolbar.container.padding.bottom; let search = _search.read(cx); - let filter_button = { - let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = search.filters_enabled; - MouseEventHandler::::new(0, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Svg::new("icons/filter_12.svg") - .with_color(style.text.color.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { + let filter_button = render_option_button_icon( + search.filters_enabled, + "icons/filter_12.svg", + 0, + "Toggle filters", + Box::new(ToggleFilters), + move |_, this, cx| { this.toggle_filters(cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - 0, - "Toggle filters", - Some(Box::new(ToggleFilters)), - tooltip_style, - cx, - ) - .into_any() - }; + }, + cx, + ); let search = _search.read(cx); let is_semantic_disabled = search.semantic_state.is_none(); let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), path, - option, + option.bits as usize, + format!("Toggle {}", option.label()), + option.to_toggle_action(), move |_, this, cx| { this.toggle_search_option(option, cx); }, diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 10f1a984b5..0cd63922ef 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use gpui::{ elements::{Label, MouseEventHandler, Svg}, platform::{CursorStyle, MouseButton}, @@ -8,7 +10,7 @@ use workspace::searchable::Direction; use crate::{ mode::{SearchMode, Side}, - SearchOptions, SelectNextMatch, SelectPrevMatch, + SelectNextMatch, SelectPrevMatch, }; pub(super) fn render_close_button( @@ -160,12 +162,14 @@ pub(crate) fn render_search_mode_button( pub(crate) fn render_option_button_icon( is_active: bool, icon: &'static str, - option: SearchOptions, + id: usize, + label: impl Into>, + action: Box, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + MouseEventHandler::::new(id, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -181,12 +185,6 @@ pub(crate) fn render_option_button_icon( }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - option.bits as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, - ) + .with_tooltip::(id, label, Some(action), tooltip_style, cx) .into_any() } From 8bb3bfa6a87d720e5cb7fee0a434d78915ec98ee Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:25:59 +0200 Subject: [PATCH 176/326] Style buffer modes like project modes --- crates/search/src/buffer_search.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 585672c45b..681c098a37 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -237,7 +237,8 @@ impl View for BufferSearchBar { .left() .top() .flex(1., true) - .constrained(), + .constrained() + .with_max_height(theme.search.search_bar_row_height), ) .contained(), ) @@ -280,6 +281,7 @@ impl View for BufferSearchBar { .constrained() .with_min_width(theme.search.editor.min_width) .with_max_width(theme.search.editor.max_width) + .with_max_height(theme.search.search_bar_row_height) .flex(1., false), ) .with_child( @@ -307,6 +309,8 @@ impl View for BufferSearchBar { |_, this, cx| this.dismiss(&Default::default(), cx), Some(Box::new(Dismiss)), )) + .constrained() + .with_height(theme.search.search_bar_row_height) .contained() .aligned() .right() From 94ac84a908eb74bce91bd24e5e0b7fde187f8167 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:28:35 +0200 Subject: [PATCH 177/326] Fix borders being cut off in buffer search --- crates/search/src/buffer_search.rs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 681c098a37..33c6c1ec85 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -300,18 +300,28 @@ impl View for BufferSearchBar { Flex::column().with_child( Flex::row() .align_children_center() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .with_child(super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - )) + .with_child( + Flex::row() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .aligned() + .left() + .contained() + .with_margin_right(3.), + ) + .with_child( + super::search_bar::render_close_button( + "Dismiss Buffer Search", + &theme.search, + cx, + |_, this, cx| this.dismiss(&Default::default(), cx), + Some(Box::new(Dismiss)), + ) + .aligned() + .right(), + ) .constrained() .with_height(theme.search.search_bar_row_height) - .contained() .aligned() .right() .top() From ae229d4c524cdb62734b00afd495e08c90f578ed Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:31:11 +0200 Subject: [PATCH 178/326] Align "Select all" to the center --- crates/search/src/buffer_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 33c6c1ec85..c7186065d6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -247,9 +247,9 @@ impl View for BufferSearchBar { .align_children_center() .with_child( Flex::row() + .align_children_center() .with_child( Flex::row() - .align_children_center() .with_child( Svg::for_style(icon_style.icon) .contained() From 9ae28f81c14146bfb1bf1a22aae2949706d4e692 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:35:33 +0200 Subject: [PATCH 179/326] Update option buttons text color --- styles/src/style_tree/search.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index edf9a46e42..7d9600771d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -43,7 +43,7 @@ export default function search(): any { option_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "on"), + ...text(theme.highest, "mono", "variant"), background: background(theme.highest, "on"), corner_radius: 2, margin: { right: 2 }, @@ -59,14 +59,14 @@ export default function search(): any { }, state: { hovered: { - ...text(theme.highest, "mono", "on", "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", "on", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed"), background: background(theme.highest, "on", "pressed"), border: { width: 1., color: background(theme.highest, "on", "pressed") From b72a42ec2916f3e65f62eb59f77f6943cbb64d05 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:31:14 +0200 Subject: [PATCH 180/326] buffer: fix alignment of editor icon. Co-authored-by: Kyle --- crates/search/src/buffer_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c7186065d6..d77f8ac51c 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -250,6 +250,7 @@ impl View for BufferSearchBar { .align_children_center() .with_child( Flex::row() + .align_children_center() .with_child( Svg::for_style(icon_style.icon) .contained() From 7314456685a618382ee1ff0a176cb18d12178eea Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:33:40 +0200 Subject: [PATCH 181/326] nav_button: Fix double border between nav buttons. Co-authored-by: Kyle --- crates/search/src/search_bar.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 0cd63922ef..00eff11f8a 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -90,6 +90,11 @@ pub(super) fn render_nav_button( ..container_style.corner_radii }, }; + if direction == Direction::Prev { + // Remove right border so that when both Next and Prev buttons are + // next to one another, there's no double border between them. + container_style.border.right = false; + } label.with_style(container_style) }) .on_click(MouseButton::Left, on_click) From d17aba4fd34a30812e938baaecbd7c450eaebd14 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:43:06 +0200 Subject: [PATCH 182/326] Fix double borders in mode buttons. Co-authored-by: Kyle --- crates/search/src/mode.rs | 9 +++++---- crates/search/src/search_bar.rs | 7 +++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index ecaa00f6ac..2c180be761 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -51,15 +51,16 @@ impl SearchMode { pub(crate) fn border_right(&self) -> bool { match self { - SearchMode::Text => false, - _ => true, + SearchMode::Regex => true, + SearchMode::Text => true, + SearchMode::Semantic => true, } } pub(crate) fn border_left(&self) -> bool { match self { - SearchMode::Regex => false, - _ => true, + SearchMode::Text => true, + _ => false, } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 00eff11f8a..088a96b279 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -125,10 +125,9 @@ pub(crate) fn render_search_mode_button( .in_state(is_active) .style_for(state) .clone(); - if mode.button_side().is_some() { - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - } + style.container.border.left = mode.border_left(); + style.container.border.right = mode.border_right(); + let label = Label::new(mode.label(), style.text.clone()).contained(); let mut container_style = style.container.clone(); if let Some(button_side) = mode.button_side() { From f4121b42da9dd6d2c72a4e5bc920a6337d9eeb41 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:01:01 +0200 Subject: [PATCH 183/326] Add more padding to dismiss button. Add a style for mode buttons group margin Co-authored-by: Kyle --- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 4 +++- crates/theme/src/theme.rs | 1 + styles/src/style_tree/search.ts | 13 +++++++++---- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index d77f8ac51c..408c696a91 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -308,7 +308,7 @@ impl View for BufferSearchBar { .aligned() .left() .contained() - .with_margin_right(3.), + .with_style(theme.search.modes_container), ) .with_child( super::search_bar::render_close_button( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index de0c424e7b..4e265a8c65 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1617,10 +1617,12 @@ impl View for ProjectSearchBar { .with_child(search_button_for_mode(SearchMode::Text, cx)) .with_children(semantic_index) .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .constrained() + .with_height(theme.search.search_bar_row_height) .aligned() .left() .contained() - .with_margin_right(3.), + .with_style(theme.search.modes_container), ) .with_child( super::search_bar::render_close_button( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 100b2c676b..91d7eeb5dc 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -391,6 +391,7 @@ pub struct Search { pub nav_button: Toggleable>, pub search_bar_row_height: f32, pub option_button_height: f32, + pub modes_container: ContainerStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 7d9600771d..76452ddec3 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -165,10 +165,10 @@ export default function search(): any { button_width: 32, corner_radius: 6, padding: { - top: 8, - bottom: 8, - left: 8, - right: 8, + top: 10, + bottom: 10, + left: 10, + right: 10, }, background: background(theme.highest, "variant"), @@ -309,6 +309,11 @@ export default function search(): any { }), search_bar_row_height: 32, option_button_height: 22, + modes_container: { + margin: { + right: 9 + } + } } } From 87e6651ecb2acbe8f949e786528bcb751ff41def Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 11:24:49 +0300 Subject: [PATCH 184/326] Fix hint tests, add a char boundary bug test --- crates/editor/src/editor.rs | 2 +- crates/editor/src/inlay_hint_cache.rs | 534 ++++++++++++++++++-------- 2 files changed, 377 insertions(+), 159 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d8b77ea95..f65f19cfec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2723,7 +2723,7 @@ impl Editor { .collect() } - fn excerpt_visible_offsets( + pub fn excerpt_visible_offsets( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index a3edb65128..b06a720090 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -30,7 +30,7 @@ pub struct InlayHintCache { #[derive(Debug)] struct TasksForRanges { tasks: Vec>, - ranges: Vec>, + sorted_ranges: Vec>, } #[derive(Debug)] @@ -80,10 +80,10 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(ranges: Vec>, task: Task<()>) -> Self { + fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { Self { tasks: vec![task], - ranges, + sorted_ranges, } } @@ -99,8 +99,8 @@ impl TasksForRanges { let mut ranges_to_query = Vec::new(); let mut last_cache_range_stop = None::; for cached_range in self - .ranges - .iter() + .sorted_ranges + .iter_mut() .skip_while(|cached_range| { cached_range .end @@ -148,14 +148,14 @@ impl TasksForRanges { } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); - self.ranges.clear(); + self.sorted_ranges.clear(); vec![query_range] } }; if !ranges_to_query.is_empty() { - self.ranges.extend(ranges_to_query.clone()); - self.ranges + self.sorted_ranges.extend(ranges_to_query.clone()); + self.sorted_ranges .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); self.tasks.push(spawn_task(ranges_to_query)); } @@ -458,6 +458,7 @@ fn spawn_new_update_tasks( cx, ) }; + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( @@ -570,10 +571,10 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(()) }; - let new_hints = inlay_hints_fetch_task - .await - .context("inlay hint fetch task")?; + let new_hints = match inlay_hints_fetch_task { + Some(task) => task.await.context("inlay hint fetch task")?, + None => return Ok(()), + }; let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx @@ -589,114 +590,20 @@ async fn fetch_and_update_hints( ) }) .await; - - editor - .update(&mut cx, |editor, cx| { - if let Some(new_update) = new_update { - let cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .entry(new_update.excerpt_id) - .or_insert_with(|| { - Arc::new(RwLock::new(CachedExcerptHints { - version: query.cache_version, - buffer_version: buffer_snapshot.version().clone(), - buffer_id: query.buffer_id, - hints: Vec::new(), - })) - }); - let mut cached_excerpt_hints = cached_excerpt_hints.write(); - match query.cache_version.cmp(&cached_excerpt_hints.version) { - cmp::Ordering::Less => return, - cmp::Ordering::Greater | cmp::Ordering::Equal => { - cached_excerpt_hints.version = query.cache_version; - } - } - - let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); - cached_excerpt_hints - .hints - .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); - let mut splice = InlaySplice { - to_remove: new_update.remove_from_visible, - to_insert: Vec::new(), - }; - for new_hint in new_update.add_to_cache { - let cached_hints = &mut cached_excerpt_hints.hints; - let insert_position = match cached_hints.binary_search_by(|probe| { - probe.1.position.cmp(&new_hint.position, &buffer_snapshot) - }) { - Ok(i) => { - if cached_hints[i].1.text() == new_hint.text() { - None - } else { - Some(i) - } - } - Err(i) => Some(i), - }; - - if let Some(insert_position) = insert_position { - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - { - let new_hint_position = multi_buffer_snapshot - .anchor_in_excerpt(query.excerpt_id, new_hint.position); - splice.to_insert.push(Inlay::hint( - new_inlay_id, - new_hint_position, - &new_hint, - )); - } - cached_hints - .insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); - cached_inlays_changed = true; - } - } - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - drop(cached_excerpt_hints); - - if query.invalidate.should_invalidate() { - let mut outdated_excerpt_caches = HashSet::default(); - for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - if excerpt_hints.buffer_id == query.buffer_id - && excerpt_id != &query.excerpt_id - && buffer_snapshot - .version() - .changed_since(&excerpt_hints.buffer_version) - { - outdated_excerpt_caches.insert(*excerpt_id); - splice - .to_remove - .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); - } - } - cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); - editor - .inlay_hint_cache - .hints - .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); - } - - let InlaySplice { - to_remove, - to_insert, - } = splice; - let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); - if cached_inlays_changed || displayed_inlays_changed { - editor.inlay_hint_cache.version += 1; - } - if displayed_inlays_changed { - editor.splice_inlay_hints(to_remove, to_insert, cx) - } - } - }) - .ok(); - + if let Some(new_update) = new_update { + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } Ok(()) } @@ -808,6 +715,113 @@ fn contains_position( && range.end.cmp(&position, buffer_snapshot).is_ge() } +fn apply_hint_update( + editor: &mut Editor, + new_update: ExcerptHintsUpdate, + query: ExcerptQuery, + buffer_snapshot: BufferSnapshot, + multi_buffer_snapshot: MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + hints: Vec::new(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); + cached_excerpt_hints + .hints + .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + for new_hint in new_update.add_to_cache { + let cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints + .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) + { + Ok(i) => { + if cached_hints[i].1.text() == new_hint.text() { + None + } else { + Some(i) + } + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); + } + cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if query.invalidate.should_invalidate() { + let mut outdated_excerpt_caches = HashSet::default(); + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -819,6 +833,7 @@ mod tests { }; use futures::StreamExt; use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use itertools::Itertools; use language::{ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, }; @@ -826,7 +841,7 @@ mod tests { use parking_lot::Mutex; use project::{FakeFs, Project}; use settings::SettingsStore; - use text::Point; + use text::{Point, ToPoint}; use workspace::Workspace; use crate::editor_tests::update_test_language_settings; @@ -1832,7 +1847,7 @@ mod tests { task_lsp_request_ranges.lock().push(params.range); let query_start = params.range.start; - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; Ok(Some(vec![lsp::InlayHint { position: query_start, label: lsp::InlayHintLabel::String(i.to_string()), @@ -1847,18 +1862,44 @@ mod tests { }) .next() .await; + fn editor_visible_range( + editor: &ViewHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + let initial_visible_range = editor_visible_range(&editor, cx); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 1); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line"); - assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 1, + "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}"); + let query_range = &ranges[0]; + assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document"); + assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document"); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2, - "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - let expected_layers = vec!["1".to_string(), "2".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); + let expected_layers = vec!["1".to_string()]; assert_eq!( expected_layers, cached_hint_labels(editor), @@ -1866,37 +1907,108 @@ mod tests { ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( - editor.inlay_hint_cache().version, 2, - "Both LSP queries should've bumped the cache version" + editor.inlay_hint_cache().version, 1, + "LSP queries should've bumped the cache version" ); }); editor.update(cx, |editor, cx| { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - editor.change_selections(None, cx, |s| s.select_ranges([600..600])); - editor.handle_input("++++more text++++", cx); }); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + cx.foreground().run_until_parked(); + let selection_in_cached_range = editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" + ); + assert_eq!( + first_scroll.start, expected_initial_query_range_end, + "First scroll should start the query right after the end of the original scroll", + ); + let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + expected_increment, + 0 + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + assert_eq!( + lsp_request_count.load(Ordering::Acquire), + 3, + "Should query for hints after every scroll" + ); + let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Should update the cache for every LSP response with hints added" + ); + + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= expected_increment; + selection_in_cached_range + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |_, _| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end"); - assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning"); - assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning"); - assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line"); - assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; + assert_eq!(ranges.len(), 1, + "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); + let query_range = &ranges[0]; + assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); + assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5, - "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints"); - let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "Should query for hints after the scroll and again after the edit"); + let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor), - "Should have hints from the new LSP response after edit"); + "Should have hints from the new LSP response after the edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 3, "Should update the cache for every LSP response with hints added"); }); } @@ -2130,7 +2242,7 @@ mod tests { s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) }); editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) }); }); cx.foreground().run_until_parked(); @@ -2140,8 +2252,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // TODO kb find the range needed. Is it due to the hint not fitting any excerpt subranges? - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2166,7 +2277,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2194,7 +2305,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2442,8 +2553,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, - "Excerpt removal should trigger cache update" + 2, + "Excerpt removal should trigger a cache update" ); }); @@ -2470,12 +2581,119 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 4, - "Settings change should trigger cache update" + 3, + "Settings change should trigger a cache update" ); }); } + #[gpui::test] + async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec!["1".to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { cx.foreground().forbid_parking(); From 4aa5df4cda9180a437b30f42a46c26d945cfe3b7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:13:17 +0200 Subject: [PATCH 185/326] Extract columns into separate objects. Co-authored-by: Kyle --- crates/search/src/buffer_search.rs | 66 ++++++----- crates/search/src/project_search.rs | 165 +++++++++++++--------------- 2 files changed, 110 insertions(+), 121 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 408c696a91..0234784952 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -219,29 +219,30 @@ impl View for BufferSearchBar { cx, ) }; - let icon_style = theme.search.editor_icon.clone(); - Flex::row() + + let nav_column = Flex::column() .with_child( - Flex::column() + Flex::row() + .align_children_center() .with_child( Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .aligned(), - ) - .with_children(match_count) - .aligned() - .left() - .top() - .flex(1., true) - .constrained() - .with_max_height(theme.search.search_bar_row_height), + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .aligned(), ) - .contained(), + .with_children(match_count) + .aligned() + .left() + .top() + .flex(1., true) + .constrained() + .with_max_height(theme.search.search_bar_row_height), ) + .contained(); + + let icon_style = theme.search.editor_icon.clone(); + Flex::row() + .with_child(nav_column) .with_child( Flex::column() .align_children_center() @@ -263,18 +264,23 @@ impl View for BufferSearchBar { .left() .flex(1., true), ) - .with_children(render_search_option( - supported_options.case, - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_children(render_search_option( - supported_options.word, - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - )) + .with_child( + Flex::row() + .with_children(render_search_option( + supported_options.case, + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + )) + .with_children(render_search_option( + supported_options.word, + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + )) + .flex(1., true) + .contained(), + ) .contained() .with_style(editor_container) .aligned() diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4e265a8c65..f216efb46b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1551,113 +1551,96 @@ impl View for ProjectSearchBar { cx, ) }; - - Flex::row() + let nav_column = Flex::column() .with_child( - Flex::column() - .with_child( - Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(nav_button_for_direction( - "<", - Direction::Prev, - cx, - )) - .with_child(nav_button_for_direction( - ">", - Direction::Next, - cx, - )) - .aligned(), - ) - .with_children(matches) - .aligned() - .top() - .left() - .constrained() - .with_height(theme.search.search_bar_row_height), - ) - .flex(1., true), - ) - .with_child( - Flex::column() + Flex::row() .align_children_center() .with_child( Flex::row() - .with_child( - Flex::row() - .with_child(query) - .contained() - .with_style(query_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_max_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .contained() - .with_margin_bottom(row_spacing), + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .aligned(), ) - .with_children(filters) - .contained() + .with_children(matches) .aligned() .top() - .flex(1., false), + .left() + .constrained() + .with_height(theme.search.search_bar_row_height), ) + .flex(1., true); + let editor_column = Flex::column() + .align_children_center() .with_child( - Flex::column() + Flex::row() .with_child( Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .constrained() - .with_height(theme.search.search_bar_row_height) - .aligned() - .left() - .contained() - .with_style(theme.search.modes_container), - ) - .with_child( - super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = - this.active_project_search.as_mut() - { - search - .update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - ) - .aligned() - .right(), - ) - .constrained() - .with_height(theme.search.search_bar_row_height) + .with_child(query) + .contained() + .with_style(query_container_style) .aligned() - .right() - .top() - .flex(1., true), - ) - .with_children( - _search - .read(cx) - .filters_enabled - .then(|| Flex::row().flex(1., true)), + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_max_height(theme.search.search_bar_row_height) + .flex(1., false), ) .contained() + .with_margin_bottom(row_spacing), + ) + .with_children(filters) + .contained() + .aligned() + .top() + .flex(1., false); + let mode_column = Flex::column() + .with_child( + Flex::row() + .align_children_center() + .with_child( + Flex::row() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_children(semantic_index) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .aligned() + .left() + .contained() + .with_style(theme.search.modes_container), + ) + .with_child( + super::search_bar::render_close_button( + "Dismiss Project Search", + &theme.search, + cx, + |_, this, cx| { + if let Some(search) = this.active_project_search.as_mut() { + search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) + } + }, + None, + ) + .aligned() + .right(), + ) + .constrained() + .with_height(theme.search.search_bar_row_height) + .aligned() + .right() + .top() .flex(1., true), ) + .with_children( + _search + .read(cx) + .filters_enabled + .then(|| Flex::row().flex(1., true)), + ) + .contained() + .flex(1., true); + Flex::row() + .with_child(nav_column) + .with_child(editor_column) + .with_child(mode_column) .contained() .with_style(theme.search.container) .flex_float() From ff4370f88ec74d9c66cf0b74416dfc692f5bbe8c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:14:57 +0200 Subject: [PATCH 186/326] buffer: Extract columns to separate objects. Co-authored-by: Kyle --- crates/search/src/buffer_search.rs | 152 ++++++++++++++--------------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 0234784952..4897d0b729 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -220,6 +220,7 @@ impl View for BufferSearchBar { ) }; + let icon_style = theme.search.editor_icon.clone(); let nav_column = Flex::column() .with_child( Flex::row() @@ -240,101 +241,98 @@ impl View for BufferSearchBar { ) .contained(); - let icon_style = theme.search.editor_icon.clone(); - Flex::row() - .with_child(nav_column) + let editor_column = Flex::column() + .align_children_center() .with_child( - Flex::column() + Flex::row() .align_children_center() .with_child( Flex::row() .align_children_center() .with_child( - Flex::row() - .align_children_center() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container) - .constrained(), - ) - .with_child( - ChildView::new(&self.query_editor, cx) - .aligned() - .left() - .flex(1., true), - ) - .with_child( - Flex::row() - .with_children(render_search_option( - supported_options.case, - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_children(render_search_option( - supported_options.word, - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - )) - .flex(1., true) - .contained(), - ) + Svg::for_style(icon_style.icon) .contained() - .with_style(editor_container) + .with_style(icon_style.container) + .constrained(), + ) + .with_child( + ChildView::new(&self.query_editor, cx) .aligned() - .top() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_max_height(theme.search.search_bar_row_height) - .flex(1., false), + .left() + .flex(1., true), ) .with_child( Flex::row() - .with_child(self.render_action_button("Select All", cx)) - .aligned(), + .with_children(render_search_option( + supported_options.case, + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + )) + .with_children(render_search_option( + supported_options.word, + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + )) + .flex(1., true) + .contained(), ) + .contained() + .with_style(editor_container) + .aligned() + .top() + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_max_height(theme.search.search_bar_row_height) .flex(1., false), ) - .contained() - .aligned() - .top() + .with_child( + Flex::row() + .with_child(self.render_action_button("Select All", cx)) + .aligned(), + ) .flex(1., false), ) - .with_child( - Flex::column().with_child( + .contained() + .aligned() + .top() + .flex(1., false); + let mode_column = Flex::column().with_child( + Flex::row() + .align_children_center() + .with_child( Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .aligned() - .left() - .contained() - .with_style(theme.search.modes_container), - ) - .with_child( - super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - ) - .aligned() - .right(), - ) - .constrained() - .with_height(theme.search.search_bar_row_height) + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) .aligned() - .right() - .top() - .flex(1., true), - ), - ) + .left() + .contained() + .with_style(theme.search.modes_container), + ) + .with_child( + super::search_bar::render_close_button( + "Dismiss Buffer Search", + &theme.search, + cx, + |_, this, cx| this.dismiss(&Default::default(), cx), + Some(Box::new(Dismiss)), + ) + .aligned() + .right(), + ) + .constrained() + .with_height(theme.search.search_bar_row_height) + .aligned() + .right() + .top() + .flex(1., true), + ); + Flex::row() + .with_child(nav_column) + .with_child(editor_column) + .with_child(mode_column) .contained() .with_style(theme.search.container) .flex_float() From 558367dc8babf93484db741ad1235014501d58f0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 16:19:44 +0300 Subject: [PATCH 187/326] Optimize query ranges tracking --- crates/editor/src/inlay_hint_cache.rs | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b06a720090..d014e71488 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -97,7 +97,7 @@ impl TasksForRanges { let ranges_to_query = match invalidate { InvalidationStrategy::None => { let mut ranges_to_query = Vec::new(); - let mut last_cache_range_stop = None::; + let mut latest_cached_range = None::<&mut Range>; for cached_range in self .sorted_ranges .iter_mut() @@ -114,12 +114,13 @@ impl TasksForRanges { .is_le() }) { - match last_cache_range_stop { - Some(last_cache_range_stop) => { - if last_cache_range_stop.offset.saturating_add(1) + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset { - ranges_to_query.push(last_cache_range_stop..cached_range.start); + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; } } None => { @@ -129,19 +130,28 @@ impl TasksForRanges { .is_lt() { ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; } } } - last_cache_range_stop = Some(cached_range.end); + latest_cached_range = Some(cached_range); } - match last_cache_range_stop { - Some(last_cache_range_stop) => { - if last_cache_range_stop.offset.saturating_add(1) < query_range.end.offset { - ranges_to_query.push(last_cache_range_stop..query_range.end); + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset + { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; } } - None => ranges_to_query.push(query_range), + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges.sort_by(|range_a, range_b| { + range_a.start.cmp(&range_b.start, buffer_snapshot) + }); + } } ranges_to_query @@ -154,9 +164,6 @@ impl TasksForRanges { }; if !ranges_to_query.is_empty() { - self.sorted_ranges.extend(ranges_to_query.clone()); - self.sorted_ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); self.tasks.push(spawn_task(ranges_to_query)); } } From db36a5fe2d04d9c81c8c252921a7356f1885fefe Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:27:57 +0200 Subject: [PATCH 188/326] Refactor buffer search UI rendering in a quest to find the meaning of life. Co-authored-by: Kyle --- crates/search/src/buffer_search.rs | 73 ++++++++++++++++-------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4897d0b729..702bafe975 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -103,7 +103,7 @@ impl View for BufferSearchBar { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); - let editor_container = if self.query_contains_error { + let query_container_style = if self.query_contains_error { theme.search.invalid_editor } else { theme.search.editor.input.container @@ -241,47 +241,50 @@ impl View for BufferSearchBar { ) .contained(); + let query = Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container) + .constrained(), + ) + .with_child( + ChildView::new(&self.query_editor, cx) + .constrained() + .flex(1., true), + ) + .with_child( + Flex::row() + .with_children(render_search_option( + supported_options.case, + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + )) + .with_children(render_search_option( + supported_options.word, + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + )) + .flex(1., true) + .contained(), + ) + .align_children_center() + .aligned() + .left() + .flex(1., true); + let row_spacing = theme.workspace.toolbar.container.padding.bottom; let editor_column = Flex::column() .align_children_center() .with_child( Flex::row() - .align_children_center() .with_child( Flex::row() - .align_children_center() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container) - .constrained(), - ) - .with_child( - ChildView::new(&self.query_editor, cx) - .aligned() - .left() - .flex(1., true), - ) - .with_child( - Flex::row() - .with_children(render_search_option( - supported_options.case, - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - )) - .with_children(render_search_option( - supported_options.word, - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - )) - .flex(1., true) - .contained(), - ) + .with_child(query) .contained() - .with_style(editor_container) + .with_style(query_container_style) .aligned() - .top() .constrained() .with_min_width(theme.search.editor.min_width) .with_max_width(theme.search.editor.max_width) @@ -293,6 +296,8 @@ impl View for BufferSearchBar { .with_child(self.render_action_button("Select All", cx)) .aligned(), ) + .contained() + .with_margin_bottom(row_spacing) .flex(1., false), ) .contained() From 336fbb3392ece0db7d78e778257b6e652bf81dab Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 18:37:50 +0300 Subject: [PATCH 189/326] Clip offsets in inlay hint queries --- crates/editor/src/inlay_hint_cache.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d014e71488..24aa84ee88 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -17,6 +17,7 @@ use project::InlayHint; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use util::post_inc; pub struct InlayHintCache { @@ -500,19 +501,17 @@ fn determine_query_range( let buffer = excerpt_buffer.read(cx); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - let start = buffer.anchor_before( - excerpt_visible_range - .start - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range.start.offset), - ); - let end = buffer.anchor_after( - excerpt_visible_range - .end - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range.end.offset) - .min(buffer.len()), - ); + let start_offset = excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range.start.offset); + let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left)); + let end_offset = excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range.end.offset) + .min(buffer.len()); + let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right)); if start.cmp(&end, buffer).is_eq() { None } else { From 4b3273182ac90b4d2df9c5e7cd13b03665a1dfe6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 19:20:20 +0300 Subject: [PATCH 190/326] Do not filter out hints to be removed --- crates/editor/src/inlay_hint_cache.rs | 29 +++------------------------ 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 24aa84ee88..d6b45629a4 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -659,19 +659,6 @@ fn calculate_hint_updates( visible_hints .iter() .filter(|hint| hint.position.excerpt_id == query.excerpt_id) - .filter(|hint| { - contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot) - }) - .filter(|hint| { - fetch_range - .start - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_ge() - }) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -685,16 +672,6 @@ fn calculate_hint_updates( .filter(|(cached_inlay_id, _)| { !excerpt_hints_to_persist.contains_key(cached_inlay_id) }) - .filter(|(_, cached_hint)| { - fetch_range - .start - .cmp(&cached_hint.position, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&cached_hint.position, buffer_snapshot) - .is_ge() - }) .map(|(cached_inlay_id, _)| *cached_inlay_id), ); } @@ -2009,12 +1986,12 @@ mod tests { assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "Should query for hints after the scroll and again after the edit"); - let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); + let expected_layers = vec!["4".to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 3, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); }); } From e0d011e35475b84bb2af1457bb0c6fdc841f6d52 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 20:12:35 +0300 Subject: [PATCH 191/326] Better assert multibuffer edit test results --- crates/editor/src/inlay_hint_cache.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d6b45629a4..8be72aec46 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1901,6 +1901,8 @@ mod tests { }); let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = + editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); cx.foreground().run_until_parked(); let selection_in_cached_range = editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges @@ -1923,12 +1925,11 @@ mod tests { first_scroll.start, expected_initial_query_range_end, "First scroll should start the query right after the end of the original scroll", ); - let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; assert_eq!( second_scroll.end, lsp::Position::new( visible_range_after_scrolls.end.row - + expected_increment, + + visible_line_count.ceil() as u32, 0 ), "Second scroll should query one more screen down after the end of the visible range" @@ -1953,12 +1954,12 @@ mod tests { ); let mut selection_in_cached_range = visible_range_after_scrolls.end; - selection_in_cached_range.row -= expected_increment; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; selection_in_cached_range }); editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); @@ -1979,12 +1980,17 @@ mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; assert_eq!(ranges.len(), 1, "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); let query_range = &ranges[0]; - assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); - assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); + assert!(query_range.start.line < selection_in_cached_range.row, + "Hints should be queried with the selected range after the query range start"); + assert!(query_range.end.line > selection_in_cached_range.row, + "Hints should be queried with the selected range before the query range end"); + assert!(query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen before"); + assert!(query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen after"); assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); let expected_layers = vec!["4".to_string()]; From 3856137b6e5f2a19130c3323b8947f2aa1f95428 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 13:17:57 -0400 Subject: [PATCH 192/326] Add list empty state style --- styles/src/style_tree/channel_modal.ts | 1 - styles/src/style_tree/collab_panel.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index c21c26e0ef..b0621743fd 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -1,5 +1,4 @@ import { useTheme } from "../theme" -import { interactive, toggleable } from "../element" import { background, border, foreground, text } from "./components" import picker from "./picker" import { input } from "../component/input" diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 6c10da7482..627d5868b6 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -269,6 +269,10 @@ export default function contacts_panel(): any { }, }, }), + list_empty_state: { + ...text(layer, "ui_sans", "variant", { size: "sm" }), + padding: side_padding + }, contact_avatar: { corner_radius: 10, width: 18, From fde9653ad865242a250a10b90938735e2b1712ea Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 14 Aug 2023 10:23:50 -0700 Subject: [PATCH 193/326] Add placeholder implementation --- crates/collab_ui/src/collab_panel.rs | 23 +++++++++++++++++++++++ crates/theme/src/theme.rs | 1 + styles/src/style_tree/collab_panel.ts | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 869c159c42..b71749121d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -200,6 +200,7 @@ enum ListEntry { contact: Arc, calling: bool, }, + ContactPlaceholder, } impl Entity for CollabPanel { @@ -368,6 +369,9 @@ impl CollabPanel { ListEntry::ChannelEditor { depth } => { this.render_channel_editor(&theme, *depth, cx) } + ListEntry::ContactPlaceholder => { + this.render_contact_placeholder(&theme.collab_panel) + } } }); @@ -821,6 +825,10 @@ impl CollabPanel { } } + if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { + self.entries.push(ListEntry::ContactPlaceholder); + } + if select_same_item { if let Some(prev_selected_entry) = prev_selected_entry { self.selection.take(); @@ -1394,6 +1402,16 @@ impl CollabPanel { event_handler.into_any() } + fn render_contact_placeholder(&self, theme: &theme::CollabPanel) -> AnyElement { + Label::new( + "Add contacts to begin collaborating", + theme.placeholder.text.clone(), + ) + .contained() + .with_style(theme.placeholder.container) + .into_any() + } + fn render_channel_editor( &self, theme: &theme::Theme, @@ -2385,6 +2403,11 @@ impl PartialEq for ListEntry { return depth == other_depth; } } + ListEntry::ContactPlaceholder => { + if let ListEntry::ContactPlaceholder = other { + return true; + } + } } false } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f455cfca73..f9c7f37baf 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,6 +220,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub placeholder: ContainedText, pub log_in_button: Interactive, pub channel_editor: ContainerStyle, pub channel_hash: Icon, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 627d5868b6..a6ff3c68d5 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -110,6 +110,10 @@ export default function contacts_panel(): any { return { channel_modal: channel_modal(), + placeholder: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + padding: 5, + }, log_in_button: interactive({ base: { background: background(theme.middle), From b07555b6dfab51cb7d5c143a9f0988f861c08f38 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 14 Aug 2023 10:34:00 -0700 Subject: [PATCH 194/326] Make empty state interactive --- crates/collab_ui/src/collab_panel.rs | 27 +++++++++++++------ crates/theme/src/theme.rs | 2 +- styles/src/style_tree/collab_panel.ts | 38 +++++++++++++++++++++------ 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b71749121d..ed042dbf4e 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -370,7 +370,7 @@ impl CollabPanel { this.render_channel_editor(&theme, *depth, cx) } ListEntry::ContactPlaceholder => { - this.render_contact_placeholder(&theme.collab_panel) + this.render_contact_placeholder(&theme.collab_panel, is_selected, cx) } } }); @@ -1402,13 +1402,23 @@ impl CollabPanel { event_handler.into_any() } - fn render_contact_placeholder(&self, theme: &theme::CollabPanel) -> AnyElement { - Label::new( - "Add contacts to begin collaborating", - theme.placeholder.text.clone(), - ) - .contained() - .with_style(theme.placeholder.container) + fn render_contact_placeholder( + &self, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum AddContacts {} + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.list_empty_state.style_for(is_selected, state); + Label::new("Add contacts to begin collaborating", style.text.clone()) + .contained() + .with_style(style.container) + .into_any() + }) + .on_click(MouseButton::Left, |_, this, cx| { + this.toggle_contact_finder(cx); + }) .into_any() } @@ -1861,6 +1871,7 @@ impl CollabPanel { ListEntry::Channel(channel) => { self.join_channel(channel.id, cx); } + ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), _ => {} } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f9c7f37baf..cd31e312d4 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,7 +220,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, - pub placeholder: ContainedText, + pub list_empty_state: Toggleable>, pub log_in_button: Interactive, pub channel_editor: ContainerStyle, pub channel_hash: Icon, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index a6ff3c68d5..3df2dd13d2 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -110,10 +110,6 @@ export default function contacts_panel(): any { return { channel_modal: channel_modal(), - placeholder: { - ...text(theme.middle, "sans", "default", { size: "sm" }), - padding: 5, - }, log_in_button: interactive({ base: { background: background(theme.middle), @@ -273,10 +269,36 @@ export default function contacts_panel(): any { }, }, }), - list_empty_state: { - ...text(layer, "ui_sans", "variant", { size: "sm" }), - padding: side_padding - }, + list_empty_state: toggleable({ + base: interactive({ + base: { + ...text(layer, "ui_sans", "variant", { size: "sm" }), + padding: side_padding + + }, + state: { + clicked: { + background: background(layer, "pressed"), + }, + }, + }), + state: { + inactive: { + hovered: { + background: background(layer, "hovered"), + }, + }, + active: { + default: { + ...text(layer, "ui_sans", "active", { size: "sm" }), + background: background(layer, "active"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }, + }), contact_avatar: { corner_radius: 10, width: 18, From b6f3dd51a0d8f62fb9f1c6805b73fff417f50760 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 14 Aug 2023 10:47:29 -0700 Subject: [PATCH 195/326] Move default collab panel to the right --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 08faedbed6..2ddf4a137f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -126,7 +126,7 @@ // Whether to show the collaboration panel button in the status bar. "button": true, // Where to dock channels panel. Can be 'left' or 'right'. - "dock": "left", + "dock": "right", // Default width of the channels panel. "default_width": 240 }, From 2bb9f7929d5777044d616744abbe194f012b8890 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 11:36:49 -0700 Subject: [PATCH 196/326] Structure the contact finder more similarly to the channel modal Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 6 +- .../src/collab_panel/channel_modal.rs | 20 ++- .../src/collab_panel/contact_finder.rs | 141 ++++++++++++++-- crates/theme/src/theme.rs | 23 +-- crates/vcs_menu/src/lib.rs | 2 +- styles/src/style_tree/app.ts | 1 - styles/src/style_tree/channel_modal.ts | 153 ----------------- styles/src/style_tree/collab_modals.ts | 159 ++++++++++++++++++ styles/src/style_tree/collab_panel.ts | 6 +- styles/src/style_tree/contact_finder.ts | 72 ++++---- 10 files changed, 351 insertions(+), 232 deletions(-) delete mode 100644 styles/src/style_tree/channel_modal.ts create mode 100644 styles/src/style_tree/collab_modals.ts diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ed042dbf4e..0e99497cef 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -7,7 +7,7 @@ use call::ActiveCall; use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; -use contact_finder::build_contact_finder; + use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -46,6 +46,8 @@ use workspace::{ use crate::face_pile::FacePile; use channel_modal::ChannelModal; +use self::contact_finder::ContactFinder; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct RemoveChannel { channel_id: u64, @@ -1945,7 +1947,7 @@ impl CollabPanel { workspace.update(cx, |workspace, cx| { workspace.toggle_modal(cx, |_, cx| { cx.add_view(|cx| { - let finder = build_contact_finder(self.user_store.clone(), cx); + let mut finder = ContactFinder::new(self.user_store.clone(), cx); finder.set_query(self.filter_editor.read(cx).text(cx), cx); finder }) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index f72eafe7da..12c923594f 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -66,7 +66,7 @@ impl ChannelModal { }, cx, ) - .with_theme(|theme| theme.collab_panel.channel_modal.picker.clone()) + .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone()) }); cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); @@ -143,7 +143,7 @@ impl View for ChannelModal { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).collab_panel.channel_modal; + let theme = &theme::current(cx).collab_panel.tabbed_modal; let mode = self.picker.read(cx).delegate().mode; let Some(channel) = self @@ -160,12 +160,12 @@ impl View for ChannelModal { mode: Mode, text: &'static str, current_mode: Mode, - theme: &theme::ChannelModal, + theme: &theme::TabbedModal, cx: &mut ViewContext, ) -> AnyElement { let active = mode == current_mode; MouseEventHandler::::new(0, cx, move |state, _| { - let contained_text = theme.mode_button.style_for(active, state); + let contained_text = theme.tab_button.style_for(active, state); Label::new(text, contained_text.text.clone()) .contained() .with_style(contained_text.container.clone()) @@ -367,11 +367,17 @@ impl PickerDelegate for ChannelModalDelegate { selected: bool, cx: &gpui::AppContext, ) -> AnyElement> { - let theme = &theme::current(cx).collab_panel.channel_modal; + 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 request_status = self.member_status(user.id, cx); - let style = theme.picker.item.in_state(selected).style_for(mouse_state); + let style = tabbed_modal + .picker + .item + .in_state(selected) + .style_for(mouse_state); let in_manage = matches!(self.mode, Mode::ManageMembers); @@ -448,7 +454,7 @@ impl PickerDelegate for ChannelModalDelegate { .contained() .with_style(style.container) .constrained() - .with_height(theme.row_height) + .with_height(tabbed_modal.row_height) .into_any(); if selected { diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 41fff2af43..4cc7034f49 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -1,28 +1,127 @@ use client::{ContactRequestStatus, User, UserStore}; -use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; +use gpui::{ + elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, +}; use picker::{Picker, PickerDelegate, PickerEvent}; use std::sync::Arc; use util::TryFutureExt; +use workspace::Modal; pub fn init(cx: &mut AppContext) { Picker::::init(cx); } -pub type ContactFinder = Picker; +pub struct ContactFinder { + picker: ViewHandle>, + has_focus: bool, +} -pub fn build_contact_finder( - user_store: ModelHandle, - cx: &mut ViewContext, -) -> ContactFinder { - Picker::new( - ContactFinderDelegate { - user_store, - potential_contacts: Arc::from([]), - selected_index: 0, - }, - cx, - ) - .with_theme(|theme| theme.picker.clone()) +impl ContactFinder { + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { + let picker = cx.add_view(|cx| { + Picker::new( + ContactFinderDelegate { + user_store, + potential_contacts: Arc::from([]), + selected_index: 0, + }, + cx, + ) + .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone()) + }); + + cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); + + Self { + picker, + has_focus: false, + } + } + + pub fn set_query(&mut self, query: String, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + picker.set_query(query, cx); + }); + } +} + +impl Entity for ContactFinder { + type Event = PickerEvent; +} + +impl View for ContactFinder { + fn ui_name() -> &'static str { + "ContactFinder" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let full_theme = &theme::current(cx); + let theme = &full_theme.collab_panel.tabbed_modal; + + fn render_mode_button( + text: &'static str, + theme: &theme::TabbedModal, + _cx: &mut ViewContext, + ) -> AnyElement { + let contained_text = &theme.tab_button.active_state().default; + Label::new(text, contained_text.text.clone()) + .contained() + .with_style(contained_text.container.clone()) + .into_any() + } + + Flex::column() + .with_child( + Flex::column() + .with_child( + Label::new("Contacts", theme.title.text.clone()) + .contained() + .with_style(theme.title.container.clone()), + ) + .with_child(Flex::row().with_children([render_mode_button( + "Invite new contacts", + &theme, + cx, + )])) + .expanded() + .contained() + .with_style(theme.header), + ) + .with_child( + ChildView::new(&self.picker, cx) + .contained() + .with_style(theme.body), + ) + .constrained() + .with_max_height(theme.max_height) + .with_max_width(theme.max_width) + .contained() + .with_style(theme.modal) + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + if cx.is_self_focused() { + cx.focus(&self.picker) + } + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl Modal for ContactFinder { + fn has_focus(&self) -> bool { + self.has_focus + } + + fn dismiss_on_event(event: &Self::Event) -> bool { + match event { + PickerEvent::Dismiss => true, + } + } } pub struct ContactFinderDelegate { @@ -97,7 +196,9 @@ impl PickerDelegate for ContactFinderDelegate { selected: bool, cx: &gpui::AppContext, ) -> AnyElement> { - let theme = &theme::current(cx).contact_finder; + let full_theme = &theme::current(cx); + let theme = &full_theme.collab_panel.contact_finder; + let tabbed_modal = &full_theme.collab_panel.tabbed_modal; let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); @@ -113,7 +214,11 @@ impl PickerDelegate for ContactFinderDelegate { } else { &theme.contact_button }; - let style = theme.picker.item.in_state(selected).style_for(mouse_state); + let style = tabbed_modal + .picker + .item + .in_state(selected) + .style_for(mouse_state); Flex::row() .with_children(user.avatar.clone().map(|avatar| { Image::from_data(avatar) @@ -145,7 +250,7 @@ impl PickerDelegate for ContactFinderDelegate { .contained() .with_style(style.container) .constrained() - .with_height(theme.row_height) + .with_height(tabbed_modal.row_height) .into_any() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cd31e312d4..1e11fbbf82 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -48,7 +48,6 @@ pub struct Theme { pub collab_panel: CollabPanel, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, - pub contact_finder: ContactFinder, pub picker: Picker, pub editor: Editor, pub search: Search, @@ -224,6 +223,8 @@ pub struct CollabPanel { pub log_in_button: Interactive, pub channel_editor: ContainerStyle, pub channel_hash: Icon, + pub tabbed_modal: TabbedModal, + pub contact_finder: ContactFinder, pub channel_modal: ChannelModal, pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, @@ -251,13 +252,20 @@ pub struct CollabPanel { } #[derive(Deserialize, Default, JsonSchema)] -pub struct ChannelModal { +pub struct TabbedModal { + pub tab_button: Toggleable>, + pub modal: ContainerStyle, + pub header: ContainerStyle, + pub body: ContainerStyle, + pub title: ContainedText, + pub picker: Picker, pub max_height: f32, pub max_width: f32, - pub title: ContainedText, - pub mode_button: Toggleable>, - pub picker: Picker, pub row_height: f32, +} + +#[derive(Deserialize, Default, JsonSchema)] +pub struct ChannelModal { pub contact_avatar: ImageStyle, pub contact_username: ContainerStyle, pub remove_member_button: ContainedText, @@ -265,9 +273,6 @@ pub struct ChannelModal { pub member_icon: Icon, pub invitee_icon: Icon, pub member_tag: ContainedText, - pub modal: ContainerStyle, - pub header: ContainerStyle, - pub body: ContainerStyle, } #[derive(Deserialize, Default, JsonSchema)] @@ -286,8 +291,6 @@ pub struct TreeBranch { #[derive(Deserialize, Default, JsonSchema)] pub struct ContactFinder { - pub picker: Picker, - pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainerStyle, pub contact_button: IconButton, diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 384b622469..8be8ad2bde 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -256,7 +256,7 @@ impl PickerDelegate for BranchListDelegate { .contained() .with_style(style.container) .constrained() - .with_height(theme.contact_finder.row_height) + .with_height(theme.collab_panel.tabbed_modal.row_height) .into_any() } fn render_header( diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index be6d4d42bf..ee5e19e111 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -46,7 +46,6 @@ export default function app(): any { project_diagnostics: project_diagnostics(), project_panel: project_panel(), collab_panel: collab_panel(), - contact_finder: contact_finder(), toolbar_dropdown_menu: toolbar_dropdown_menu(), search: search(), shared_screen: shared_screen(), diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts deleted file mode 100644 index b0621743fd..0000000000 --- a/styles/src/style_tree/channel_modal.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { useTheme } from "../theme" -import { background, border, foreground, text } from "./components" -import picker from "./picker" -import { input } from "../component/input" -import { toggleable_text_button } from "../component/text_button" - -export default function channel_modal(): any { - const theme = useTheme() - - const side_margin = 6 - const contact_button = { - background: background(theme.middle, "variant"), - color: foreground(theme.middle, "variant"), - icon_width: 8, - button_width: 16, - corner_radius: 8, - } - - const picker_style = picker() - delete picker_style.shadow - delete picker_style.border - - const picker_input = input() - - return { - header: { - background: background(theme.middle, "accent"), - border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), - corner_radii: { - top_right: 12, - top_left: 12, - } - }, - body: { - background: background(theme.middle), - corner_radii: { - bottom_right: 12, - bottom_left: 12, - } - }, - modal: { - background: background(theme.middle), - shadow: theme.modal_shadow, - corner_radius: 12, - padding: { - bottom: 0, - left: 0, - right: 0, - top: 0, - }, - - }, - // This is used for the icons that are rendered to the right of channel Members in both UIs - member_icon: { - background: background(theme.middle), - padding: { - bottom: 4, - left: 4, - right: 4, - top: 4, - }, - width: 5, - color: foreground(theme.middle, "accent"), - }, - // This is used for the icons that are rendered to the right of channel invites in both UIs - invitee_icon: { - background: background(theme.middle), - padding: { - bottom: 4, - left: 4, - right: 4, - top: 4, - }, - width: 5, - color: foreground(theme.middle, "accent"), - }, - remove_member_button: { - ...text(theme.middle, "sans", { size: "xs" }), - background: background(theme.middle), - padding: { - left: 7, - right: 7 - } - }, - cancel_invite_button: { - ...text(theme.middle, "sans", { size: "xs" }), - background: background(theme.middle), - }, - member_tag: { - ...text(theme.middle, "sans", { size: "xs" }), - border: border(theme.middle, "active"), - background: background(theme.middle), - margin: { - left: 8, - }, - padding: { - left: 4, - right: 4, - } - }, - max_height: 400, - max_width: 540, - title: { - ...text(theme.middle, "sans", "on", { size: "lg" }), - padding: { - left: 6, - } - }, - mode_button: toggleable_text_button(theme, { - variant: "ghost", - layer: theme.middle, - active_color: "accent", - margin: { - top: 8, - bottom: 8, - right: 4 - } - }), - picker: { - empty_container: {}, - item: { - ...picker_style.item, - margin: { left: side_margin, right: side_margin }, - }, - no_matches: picker_style.no_matches, - input_editor: picker_input, - empty_input_editor: picker_input, - header: picker_style.header, - footer: picker_style.footer, - }, - row_height: 28, - contact_avatar: { - corner_radius: 10, - width: 18, - }, - contact_username: { - padding: { - left: 8, - }, - }, - contact_button: { - ...contact_button, - hover: { - background: background(theme.middle, "variant", "hovered"), - }, - }, - disabled_contact_button: { - ...contact_button, - background: background(theme.middle, "disabled"), - color: foreground(theme.middle, "disabled"), - }, - } -} diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts new file mode 100644 index 0000000000..95690b5d85 --- /dev/null +++ b/styles/src/style_tree/collab_modals.ts @@ -0,0 +1,159 @@ +import { useTheme } from "../theme" +import { background, border, foreground, text } from "./components" +import picker from "./picker" +import { input } from "../component/input" +import { toggleable_text_button } from "../component/text_button" +import contact_finder from "./contact_finder" + +export default function channel_modal(): any { + const theme = useTheme() + + const side_margin = 6 + const contact_button = { + background: background(theme.middle, "variant"), + color: foreground(theme.middle, "variant"), + icon_width: 8, + button_width: 16, + corner_radius: 8, + } + + const picker_style = picker() + delete picker_style.shadow + delete picker_style.border + + const picker_input = input() + + return { + contact_finder: contact_finder(), + tabbed_modal: { + tab_button: toggleable_text_button(theme, { + variant: "ghost", + layer: theme.middle, + active_color: "accent", + margin: { + top: 8, + bottom: 8, + right: 4 + } + }), + row_height: 28, + header: { + background: background(theme.middle, "accent"), + border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), + corner_radii: { + top_right: 12, + top_left: 12, + } + }, + body: { + background: background(theme.middle), + corner_radii: { + bottom_right: 12, + bottom_left: 12, + } + }, + modal: { + background: background(theme.middle), + shadow: theme.modal_shadow, + corner_radius: 12, + padding: { + bottom: 0, + left: 0, + right: 0, + top: 0, + }, + + }, + max_height: 400, + max_width: 540, + title: { + ...text(theme.middle, "sans", "on", { size: "lg" }), + padding: { + left: 6, + } + }, + picker: { + empty_container: {}, + item: { + ...picker_style.item, + margin: { left: side_margin, right: side_margin }, + }, + no_matches: picker_style.no_matches, + input_editor: picker_input, + empty_input_editor: picker_input, + header: picker_style.header, + footer: picker_style.footer, + }, + }, + channel_modal: { + // This is used for the icons that are rendered to the right of channel Members in both UIs + member_icon: { + background: background(theme.middle), + padding: { + bottom: 4, + left: 4, + right: 4, + top: 4, + }, + width: 5, + color: foreground(theme.middle, "accent"), + }, + // This is used for the icons that are rendered to the right of channel invites in both UIs + invitee_icon: { + background: background(theme.middle), + padding: { + bottom: 4, + left: 4, + right: 4, + top: 4, + }, + width: 5, + color: foreground(theme.middle, "accent"), + }, + remove_member_button: { + ...text(theme.middle, "sans", { size: "xs" }), + background: background(theme.middle), + padding: { + left: 7, + right: 7 + } + }, + cancel_invite_button: { + ...text(theme.middle, "sans", { size: "xs" }), + background: background(theme.middle), + }, + member_tag: { + ...text(theme.middle, "sans", { size: "xs" }), + border: border(theme.middle, "active"), + background: background(theme.middle), + margin: { + left: 8, + }, + padding: { + left: 4, + right: 4, + } + }, + contact_avatar: { + corner_radius: 10, + width: 18, + }, + contact_username: { + padding: { + left: 8, + }, + }, + contact_button: { + ...contact_button, + hover: { + background: background(theme.middle, "variant", "hovered"), + }, + }, + disabled_contact_button: { + ...contact_button, + background: background(theme.middle, "disabled"), + color: foreground(theme.middle, "disabled"), + }, + } + } +} diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 3df2dd13d2..06170901e9 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -7,9 +7,7 @@ import { } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" -import channel_modal from "./channel_modal" -import { icon_button, toggleable_icon_button } from "../component/icon_button" - +import collab_modals from "./collab_modals" export default function contacts_panel(): any { const theme = useTheme() @@ -109,7 +107,7 @@ export default function contacts_panel(): any { return { - channel_modal: channel_modal(), + ...collab_modals(), log_in_button: interactive({ base: { background: background(theme.middle), diff --git a/styles/src/style_tree/contact_finder.ts b/styles/src/style_tree/contact_finder.ts index aa88a9f26a..04f95cc367 100644 --- a/styles/src/style_tree/contact_finder.ts +++ b/styles/src/style_tree/contact_finder.ts @@ -1,11 +1,11 @@ -import picker from "./picker" +// import picker from "./picker" import { background, border, foreground, text } from "./components" import { useTheme } from "../theme" export default function contact_finder(): any { const theme = useTheme() - const side_margin = 6 + // const side_margin = 6 const contact_button = { background: background(theme.middle, "variant"), color: foreground(theme.middle, "variant"), @@ -14,42 +14,42 @@ export default function contact_finder(): any { corner_radius: 8, } - const picker_style = picker() - const picker_input = { - background: background(theme.middle, "on"), - corner_radius: 6, - text: text(theme.middle, "mono"), - placeholder_text: text(theme.middle, "mono", "on", "disabled", { - size: "xs", - }), - selection: theme.players[0], - border: border(theme.middle), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: side_margin, - right: side_margin, - }, - } + // const picker_style = picker() + // const picker_input = { + // background: background(theme.middle, "on"), + // corner_radius: 6, + // text: text(theme.middle, "mono"), + // placeholder_text: text(theme.middle, "mono", "on", "disabled", { + // size: "xs", + // }), + // selection: theme.players[0], + // border: border(theme.middle), + // padding: { + // bottom: 4, + // left: 8, + // right: 8, + // top: 4, + // }, + // margin: { + // left: side_margin, + // right: side_margin, + // }, + // } return { - picker: { - empty_container: {}, - item: { - ...picker_style.item, - margin: { left: side_margin, right: side_margin }, - }, - no_matches: picker_style.no_matches, - input_editor: picker_input, - empty_input_editor: picker_input, - header: picker_style.header, - footer: picker_style.footer, - }, - row_height: 28, + // picker: { + // empty_container: {}, + // item: { + // ...picker_style.item, + // margin: { left: side_margin, right: side_margin }, + // }, + // no_matches: picker_style.no_matches, + // input_editor: picker_input, + // empty_input_editor: picker_input, + // header: picker_style.header, + // footer: picker_style.footer, + // }, + // row_height: 28, contact_avatar: { corner_radius: 10, width: 18, From 3b10ae93107313251b3c96abd2f27ce5f366062f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 11:57:15 -0700 Subject: [PATCH 197/326] Add icon before the empty contacts text Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 16 +++++++++++++++- crates/theme/src/theme.rs | 2 ++ styles/src/style_tree/collab_panel.ts | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0e99497cef..274eeb9f2d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1413,7 +1413,21 @@ impl CollabPanel { enum AddContacts {} MouseEventHandler::::new(0, cx, |state, _| { let style = theme.list_empty_state.style_for(is_selected, state); - Label::new("Add contacts to begin collaborating", style.text.clone()) + Flex::row() + .with_child( + Svg::new("icons/plus_16.svg") + .with_color(theme.list_empty_icon.color) + .constrained() + .with_width(theme.list_empty_icon.width) + .aligned() + .left(), + ) + .with_child( + Label::new("Add a contact", style.text.clone()) + .contained() + .with_style(theme.list_empty_label_container), + ) + .align_children_center() .contained() .with_style(style.container) .into_any() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1e11fbbf82..4919eb93c7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -220,6 +220,8 @@ pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, pub list_empty_state: Toggleable>, + pub list_empty_icon: Icon, + pub list_empty_label_container: ContainerStyle, pub log_in_button: Interactive, pub channel_editor: ContainerStyle, pub channel_hash: Icon, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 06170901e9..8f8b8e504f 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -267,6 +267,15 @@ export default function contacts_panel(): any { }, }, }), + list_empty_label_container: { + margin: { + left: 5, + } + }, + list_empty_icon: { + color: foreground(layer, "on"), + width: 16, + }, list_empty_state: toggleable({ base: interactive({ base: { From 4a5b2fa5dc49261395cbd54092e49654c95f28b8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 15:13:57 -0400 Subject: [PATCH 198/326] Add ghost button variants --- styles/src/component/button.ts | 6 ++++++ styles/src/component/icon_button.ts | 12 ++++++++---- styles/src/component/text_button.ts | 9 ++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 styles/src/component/button.ts diff --git a/styles/src/component/button.ts b/styles/src/component/button.ts new file mode 100644 index 0000000000..ba72851768 --- /dev/null +++ b/styles/src/component/button.ts @@ -0,0 +1,6 @@ +export const ButtonVariant = { + Default: 'default', + Ghost: 'ghost' +} as const + +export type Variant = typeof ButtonVariant[keyof typeof ButtonVariant] diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 6887fc7c30..ae3fa763e7 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -1,6 +1,7 @@ import { interactive, toggleable } from "../element" import { background, foreground } from "../style_tree/components" import { useTheme, Theme } from "../theme" +import { ButtonVariant, Variant } from "./button" export type Margin = { top: number @@ -16,17 +17,20 @@ interface IconButtonOptions { | Theme["highest"] color?: keyof Theme["lowest"] margin?: Partial + variant?: Variant } type ToggleableIconButtonOptions = IconButtonOptions & { active_color?: keyof Theme["lowest"] } -export function icon_button({ color, margin, layer }: IconButtonOptions) { +export function icon_button({ color, margin, layer, variant = ButtonVariant.Default }: IconButtonOptions) { const theme = useTheme() if (!color) color = "base" + const background_color = variant === ButtonVariant.Ghost ? null : background(layer ?? theme.lowest, color) + const m = { top: margin?.top ?? 0, bottom: margin?.bottom ?? 0, @@ -51,7 +55,7 @@ export function icon_button({ color, margin, layer }: IconButtonOptions) { }, state: { default: { - background: background(layer ?? theme.lowest, color), + background: background_color, color: foreground(layer ?? theme.lowest, color), }, hovered: { @@ -68,13 +72,13 @@ export function icon_button({ color, margin, layer }: IconButtonOptions) { export function toggleable_icon_button( theme: Theme, - { color, active_color, margin }: ToggleableIconButtonOptions + { color, active_color, margin, variant }: ToggleableIconButtonOptions ) { if (!color) color = "base" return toggleable({ state: { - inactive: icon_button({ color, margin }), + inactive: icon_button({ color, margin, variant }), active: icon_button({ color: active_color ? active_color : color, margin, diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 3311081a6f..c7bdb26e7b 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -6,6 +6,7 @@ import { text, } from "../style_tree/components" import { useTheme, Theme } from "../theme" +import { ButtonVariant, Variant } from "./button" import { Margin } from "./icon_button" interface TextButtonOptions { @@ -13,7 +14,7 @@ interface TextButtonOptions { | Theme["lowest"] | Theme["middle"] | Theme["highest"] - variant?: "default" | "ghost" + variant?: Variant color?: keyof Theme["lowest"] margin?: Partial text_properties?: TextProperties @@ -24,7 +25,7 @@ type ToggleableTextButtonOptions = TextButtonOptions & { } export function text_button({ - variant = "default", + variant = ButtonVariant.Default, color, layer, margin, @@ -33,6 +34,8 @@ export function text_button({ const theme = useTheme() if (!color) color = "base" + const background_color = variant === ButtonVariant.Ghost ? null : background(layer ?? theme.lowest, color) + const text_options: TextProperties = { size: "xs", weight: "normal", @@ -61,7 +64,7 @@ export function text_button({ }, state: { default: { - background: variant !== "ghost" ? background(layer ?? theme.lowest, color) : null, + background: background_color, color: foreground(layer ?? theme.lowest, color), }, hovered: { From 8531cdaff72f245e858b50db2e5d6aac845739e9 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 15:50:37 -0400 Subject: [PATCH 199/326] Style channels panel items --- styles/src/component/text_button.ts | 4 +- styles/src/style_tree/collab_panel.ts | 247 +++++++++----------------- 2 files changed, 90 insertions(+), 161 deletions(-) diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index c7bdb26e7b..2be2dd19cb 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -30,7 +30,7 @@ export function text_button({ layer, margin, text_properties, -}: TextButtonOptions) { +}: TextButtonOptions = {}) { const theme = useTheme() if (!color) color = "base" @@ -81,7 +81,7 @@ export function text_button({ export function toggleable_text_button( theme: Theme, - { variant, color, active_color, margin }: ToggleableTextButtonOptions + { variant, color, active_color, margin }: ToggleableTextButtonOptions = {} ) { if (!color) color = "base" diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 8f8b8e504f..b8969e2b9a 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -8,12 +8,16 @@ import { import { interactive, toggleable } from "../element" import { useTheme } from "../theme" import collab_modals from "./collab_modals" +import { text_button } from "../component/text_button" +import { toggleable_icon_button } from "../component/icon_button" export default function contacts_panel(): any { const theme = useTheme() - const name_margin = 8 - const side_padding = 12 + const NAME_MARGIN = 6 as const + const SPACING = 12 as const + const INDENT_SIZE = 8 as const + const ITEM_HEIGHT = 28 as const const layer = theme.middle @@ -24,6 +28,7 @@ export default function contacts_panel(): any { button_width: 16, corner_radius: 8, } + const project_row = { guest_avatar_spacing: 4, height: 24, @@ -32,186 +37,111 @@ export default function contacts_panel(): any { width: 14, }, name: { - ...text(layer, "mono", { size: "sm" }), + ...text(layer, "ui_sans", { size: "sm" }), margin: { - left: name_margin, - right: 6, + left: NAME_MARGIN, + right: 4, }, }, guests: { margin: { - left: name_margin, - right: name_margin, + left: NAME_MARGIN, + right: NAME_MARGIN, }, }, padding: { - left: side_padding, - right: side_padding, + left: SPACING, + right: SPACING, }, } - const headerButton = toggleable({ - state: { - inactive: interactive({ - base: { - corner_radius: 6, - padding: { - top: 2, - bottom: 2, - left: 4, - right: 4, - }, - icon_width: 14, - icon_height: 14, - button_width: 20, - button_height: 16, - color: foreground(layer, "on"), - }, - state: { - default: { - }, - hovered: { - background: background(layer, "base", "hovered"), - }, - clicked: { - background: background(layer, "base", "pressed"), - }, - }, - }), - active: interactive({ - base: { - corner_radius: 6, - padding: { - top: 2, - bottom: 2, - left: 4, - right: 4, - }, - icon_width: 14, - icon_height: 14, - button_width: 20, - button_height: 16, - color: foreground(layer, "on"), - }, - state: { - default: { - background: background(layer, "base", "active"), - }, - clicked: { - background: background(layer, "base", "active"), - }, - }, - }), - }, + const icon_style = { + color: foreground(layer, "variant"), + width: 14, + } + + const header_icon_button = toggleable_icon_button(theme, { + layer: theme.middle, + variant: "ghost", }) - - return { - ...collab_modals(), - log_in_button: interactive({ + const subheader_row = toggleable({ + base: interactive({ base: { - background: background(theme.middle), - border: border(theme.middle, "active"), - corner_radius: 4, - margin: { - top: 16, - left: 16, - right: 16, - }, + ...text(layer, "ui_sans", { size: "sm" }), padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, + left: SPACING, + right: SPACING, }, - ...text(theme.middle, "sans", "default", { size: "sm" }), }, state: { hovered: { - ...text(theme.middle, "sans", "default", { size: "sm" }), - background: background(theme.middle, "hovered"), - border: border(theme.middle, "active"), + background: background(layer, "hovered"), }, clicked: { - ...text(theme.middle, "sans", "default", { size: "sm" }), - background: background(theme.middle, "pressed"), - border: border(theme.middle, "active"), + background: background(layer, "pressed"), }, }, }), + state: { + active: { + default: { + ...text(layer, "ui_sans", "active", { size: "sm" }), + background: background(layer, "active"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }, + }) + + const filter_input = { + background: background(layer, "on"), + corner_radius: 6, + text: text(layer, "ui_sans", "base"), + placeholder_text: text(layer, "ui_sans", "base", "disabled", { + size: "xs", + }), + selection: theme.players[0], + border: border(layer, "on"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: SPACING, + right: SPACING, + }, + } + + return { + ...collab_modals(), + log_in_button: text_button(), background: background(layer), padding: { - top: 12, - }, - user_query_editor: { - background: background(layer, "on"), - corner_radius: 6, - text: text(layer, "mono", "on"), - placeholder_text: text(layer, "mono", "on", "disabled", { - size: "xs", - }), - selection: theme.players[0], - border: border(layer, "on"), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: side_padding, - right: side_padding, - }, - }, - channel_hash: { - color: foreground(layer, "on"), - width: 14, + top: SPACING, }, + user_query_editor: filter_input, + channel_hash: icon_style, user_query_editor_height: 33, - add_contact_button: headerButton, - add_channel_button: headerButton, - leave_call_button: headerButton, - row_height: 28, - channel_indent: 10, + add_contact_button: header_icon_button, + add_channel_button: header_icon_button, + leave_call_button: header_icon_button, + row_height: ITEM_HEIGHT, + channel_indent: INDENT_SIZE, section_icon_size: 8, header_row: { - ...text(layer, "mono", { size: "sm", weight: "bold" }), - margin: { top: 14 }, + ...text(layer, "ui_sans", { size: "sm", weight: "bold" }), + margin: { top: SPACING }, padding: { - left: side_padding, - right: side_padding, + left: SPACING, + right: SPACING, }, }, - subheader_row: toggleable({ - base: interactive({ - base: { - ...text(layer, "mono", { size: "sm" }), - padding: { - left: side_padding, - right: side_padding, - }, - }, - state: { - hovered: { - background: background(layer, "hovered"), - }, - clicked: { - background: background(layer, "pressed"), - }, - }, - }), - state: { - active: { - default: { - ...text(layer, "mono", "active", { size: "sm" }), - background: background(layer, "active"), - }, - clicked: { - background: background(layer, "pressed"), - }, - }, - }, - }), + subheader_row, leave_call: interactive({ base: { background: background(layer), @@ -240,8 +170,8 @@ export default function contacts_panel(): any { base: interactive({ base: { padding: { - left: side_padding, - right: side_padding, + left: SPACING, + right: SPACING, }, }, state: { @@ -258,7 +188,7 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(layer, "mono", "active", { size: "sm" }), + ...text(layer, "ui_sans", "active", { size: "sm" }), background: background(layer, "active"), }, clicked: { @@ -280,7 +210,7 @@ export default function contacts_panel(): any { base: interactive({ base: { ...text(layer, "ui_sans", "variant", { size: "sm" }), - padding: side_padding + padding: SPACING }, state: { @@ -323,12 +253,12 @@ export default function contacts_panel(): any { background: foreground(layer, "negative"), }, contact_username: { - ...text(layer, "mono", { size: "sm" }), + ...text(layer, "ui_sans", { size: "sm" }), margin: { - left: name_margin, + left: NAME_MARGIN, }, }, - contact_button_spacing: name_margin, + contact_button_spacing: NAME_MARGIN, contact_button: interactive({ base: { ...contact_button }, state: { @@ -369,9 +299,8 @@ export default function contacts_panel(): any { base: interactive({ base: { ...project_row, - // background: background(layer), icon: { - margin: { left: name_margin }, + margin: { left: NAME_MARGIN }, color: foreground(layer, "variant"), width: 12, }, @@ -395,7 +324,7 @@ export default function contacts_panel(): any { face_overlap: 8, channel_editor: { padding: { - left: name_margin, + left: NAME_MARGIN, } } } From a5534bb30f4bc99bd19d15ac52823d29ddcf397c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 15:50:42 -0400 Subject: [PATCH 200/326] Add new icons --- assets/icons/ai.svg | 27 +++++++++++++++++++++++++++ assets/icons/arrow_left.svg | 3 +++ assets/icons/arrow_right.svg | 3 +++ assets/icons/chevron_down.svg | 3 +++ assets/icons/chevron_left.svg | 3 +++ assets/icons/chevron_right.svg | 3 +++ assets/icons/chevron_up.svg | 3 +++ assets/icons/conversations.svg | 4 ++++ assets/icons/copilot.svg | 9 +++++++++ assets/icons/copy.svg | 5 +++++ assets/icons/error.svg | 4 ++++ assets/icons/exit.svg | 4 ++++ assets/icons/feedback.svg | 6 ++++++ assets/icons/filter.svg | 3 +++ assets/icons/kebab.svg | 5 +++++ assets/icons/magnifying_glass.svg | 3 +++ assets/icons/match_case.svg | 5 +++++ assets/icons/match_word.svg | 5 +++++ assets/icons/maximize.svg | 4 ++++ assets/icons/microphone.svg | 5 +++++ assets/icons/minimize.svg | 4 ++++ assets/icons/plus.svg | 3 +++ assets/icons/project.svg | 5 +++++ assets/icons/replace.svg | 11 +++++++++++ assets/icons/replace_all.svg | 5 +++++ assets/icons/replace_next.svg | 5 +++++ assets/icons/screen.svg | 4 ++++ assets/icons/split.svg | 5 +++++ assets/icons/success.svg | 4 ++++ assets/icons/terminal.svg | 5 +++++ assets/icons/warning.svg | 5 +++++ assets/icons/x.svg | 3 +++ 32 files changed, 166 insertions(+) create mode 100644 assets/icons/ai.svg create mode 100644 assets/icons/arrow_left.svg create mode 100644 assets/icons/arrow_right.svg create mode 100644 assets/icons/chevron_down.svg create mode 100644 assets/icons/chevron_left.svg create mode 100644 assets/icons/chevron_right.svg create mode 100644 assets/icons/chevron_up.svg create mode 100644 assets/icons/conversations.svg create mode 100644 assets/icons/copilot.svg create mode 100644 assets/icons/copy.svg create mode 100644 assets/icons/error.svg create mode 100644 assets/icons/exit.svg create mode 100644 assets/icons/feedback.svg create mode 100644 assets/icons/filter.svg create mode 100644 assets/icons/kebab.svg create mode 100644 assets/icons/magnifying_glass.svg create mode 100644 assets/icons/match_case.svg create mode 100644 assets/icons/match_word.svg create mode 100644 assets/icons/maximize.svg create mode 100644 assets/icons/microphone.svg create mode 100644 assets/icons/minimize.svg create mode 100644 assets/icons/plus.svg create mode 100644 assets/icons/project.svg create mode 100644 assets/icons/replace.svg create mode 100644 assets/icons/replace_all.svg create mode 100644 assets/icons/replace_next.svg create mode 100644 assets/icons/screen.svg create mode 100644 assets/icons/split.svg create mode 100644 assets/icons/success.svg create mode 100644 assets/icons/terminal.svg create mode 100644 assets/icons/warning.svg create mode 100644 assets/icons/x.svg diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg new file mode 100644 index 0000000000..fa046c6050 --- /dev/null +++ b/assets/icons/ai.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg new file mode 100644 index 0000000000..186c9c7457 --- /dev/null +++ b/assets/icons/arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg new file mode 100644 index 0000000000..7bae7f4801 --- /dev/null +++ b/assets/icons/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg new file mode 100644 index 0000000000..b971555cfa --- /dev/null +++ b/assets/icons/chevron_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg new file mode 100644 index 0000000000..8e61beed5d --- /dev/null +++ b/assets/icons/chevron_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg new file mode 100644 index 0000000000..fcd9d83fc2 --- /dev/null +++ b/assets/icons/chevron_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg new file mode 100644 index 0000000000..171cdd61c0 --- /dev/null +++ b/assets/icons/chevron_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/conversations.svg b/assets/icons/conversations.svg new file mode 100644 index 0000000000..fe8ad03dda --- /dev/null +++ b/assets/icons/conversations.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg new file mode 100644 index 0000000000..06dbf178ae --- /dev/null +++ b/assets/icons/copilot.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 0000000000..4aa44979c3 --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/error.svg b/assets/icons/error.svg new file mode 100644 index 0000000000..82b9401d08 --- /dev/null +++ b/assets/icons/error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg new file mode 100644 index 0000000000..7e45535773 --- /dev/null +++ b/assets/icons/exit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/feedback.svg b/assets/icons/feedback.svg new file mode 100644 index 0000000000..2703f70119 --- /dev/null +++ b/assets/icons/feedback.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg new file mode 100644 index 0000000000..80ce656f57 --- /dev/null +++ b/assets/icons/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/kebab.svg b/assets/icons/kebab.svg new file mode 100644 index 0000000000..1858c65520 --- /dev/null +++ b/assets/icons/kebab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg new file mode 100644 index 0000000000..0b539adb6c --- /dev/null +++ b/assets/icons/magnifying_glass.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/match_case.svg b/assets/icons/match_case.svg new file mode 100644 index 0000000000..82f4529c1b --- /dev/null +++ b/assets/icons/match_case.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/match_word.svg b/assets/icons/match_word.svg new file mode 100644 index 0000000000..69ba8eb9e6 --- /dev/null +++ b/assets/icons/match_word.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg new file mode 100644 index 0000000000..4dc7755714 --- /dev/null +++ b/assets/icons/maximize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg new file mode 100644 index 0000000000..8974fd939d --- /dev/null +++ b/assets/icons/microphone.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg new file mode 100644 index 0000000000..d8941ee1f0 --- /dev/null +++ b/assets/icons/minimize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 0000000000..a54dd0ad66 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/project.svg b/assets/icons/project.svg new file mode 100644 index 0000000000..525109db4c --- /dev/null +++ b/assets/icons/project.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg new file mode 100644 index 0000000000..af10921891 --- /dev/null +++ b/assets/icons/replace.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/replace_all.svg b/assets/icons/replace_all.svg new file mode 100644 index 0000000000..4838e82242 --- /dev/null +++ b/assets/icons/replace_all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg new file mode 100644 index 0000000000..ba751411af --- /dev/null +++ b/assets/icons/replace_next.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg new file mode 100644 index 0000000000..49e097b023 --- /dev/null +++ b/assets/icons/screen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/split.svg b/assets/icons/split.svg new file mode 100644 index 0000000000..4c131466c2 --- /dev/null +++ b/assets/icons/split.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/success.svg b/assets/icons/success.svg new file mode 100644 index 0000000000..85450cdc43 --- /dev/null +++ b/assets/icons/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/terminal.svg b/assets/icons/terminal.svg new file mode 100644 index 0000000000..15dd705b0b --- /dev/null +++ b/assets/icons/terminal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg new file mode 100644 index 0000000000..6b3d0fd41e --- /dev/null +++ b/assets/icons/warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/x.svg b/assets/icons/x.svg new file mode 100644 index 0000000000..31c5aa31a6 --- /dev/null +++ b/assets/icons/x.svg @@ -0,0 +1,3 @@ + + + From 27bf01c3a82efe050d43358c6c35e7f1e038595f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 22:42:22 +0300 Subject: [PATCH 201/326] Strip off inlay hints data that should be resolved --- crates/lsp/src/lsp.rs | 4 ++- crates/project/src/lsp_command.rs | 56 ++++++------------------------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c..e0ae64d806 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f1..a8692257d8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,51 +1963,17 @@ impl LspCommand for InlayHints { position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } - }), - }), kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } + // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. + tooltip: None, + // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. + label: Some(proto::InlayHintLabel { + label: Some(proto::inlay_hint_label::Label::Value( + match response_hint.label { + InlayHintLabel::String(s) => s, + InlayHintLabel::LabelParts(_) => response_hint.text(), + }, + )), }), }) .collect(), From f2d46e0ff954d14ca6ed40569ffef2412ea9ae9b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 15:57:31 -0400 Subject: [PATCH 202/326] Use new icons in channel panel --- assets/icons/hash.svg | 6 ++++++ assets/icons/html.svg | 5 +++++ assets/icons/lock.svg | 6 ++++++ crates/collab_ui/src/collab_panel.rs | 26 +++++++++++++------------- styles/src/style_tree/collab_panel.ts | 2 +- 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 assets/icons/hash.svg create mode 100644 assets/icons/html.svg create mode 100644 assets/icons/lock.svg diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg new file mode 100644 index 0000000000..f685245ed3 --- /dev/null +++ b/assets/icons/hash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/html.svg b/assets/icons/html.svg new file mode 100644 index 0000000000..1e676fe313 --- /dev/null +++ b/assets/icons/html.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg new file mode 100644 index 0000000000..652f45a7e8 --- /dev/null +++ b/assets/icons/lock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 274eeb9f2d..8c63649ef9 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1189,7 +1189,7 @@ impl CollabPanel { .collab_panel .leave_call_button .style_for(is_selected, state), - "icons/radix/exit.svg", + "icons/exit.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) @@ -1233,7 +1233,7 @@ impl CollabPanel { .collab_panel .add_contact_button .style_for(is_selected, state), - "icons/plus_16.svg", + "icons/plus.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) @@ -1266,9 +1266,9 @@ impl CollabPanel { .with_children(if can_collapse { Some( Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" + "icons/chevron_right.svg" } else { - "icons/chevron_down_8.svg" + "icons/chevron_down.svg" }) .with_color(header_style.text.color) .constrained() @@ -1364,7 +1364,7 @@ impl CollabPanel { cx, |mouse_state, _| { let button_style = theme.contact_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x_mark_8.svg") + render_icon_button(button_style, "icons/x.svg") .aligned() .flex_float() }, @@ -1415,7 +1415,7 @@ impl CollabPanel { let style = theme.list_empty_state.style_for(is_selected, state); Flex::row() .with_child( - Svg::new("icons/plus_16.svg") + Svg::new("icons/plus.svg") .with_color(theme.list_empty_icon.color) .constrained() .with_width(theme.list_empty_icon.width) @@ -1446,7 +1446,7 @@ impl CollabPanel { ) -> AnyElement { Flex::row() .with_child( - Svg::new("icons/channel_hash.svg") + Svg::new("icons/hash.svg") .with_color(theme.collab_panel.channel_hash.color) .constrained() .with_width(theme.collab_panel.channel_hash.width) @@ -1506,7 +1506,7 @@ impl CollabPanel { MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() .with_child( - Svg::new("icons/channel_hash.svg") + Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) .constrained() .with_width(theme.channel_hash.width) @@ -1572,7 +1572,7 @@ impl CollabPanel { Flex::row() .with_child( - Svg::new("icons/channel_hash.svg") + Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) .constrained() .with_width(theme.channel_hash.width) @@ -1597,7 +1597,7 @@ impl CollabPanel { } else { theme.contact_button.style_for(mouse_state) }; - render_icon_button(button_style, "icons/x_mark_8.svg").aligned() + render_icon_button(button_style, "icons/x.svg").aligned() }, ) .with_cursor_style(CursorStyle::PointingHand) @@ -1686,7 +1686,7 @@ impl CollabPanel { } else { theme.contact_button.style_for(mouse_state) }; - render_icon_button(button_style, "icons/x_mark_8.svg").aligned() + render_icon_button(button_style, "icons/x.svg").aligned() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1720,7 +1720,7 @@ impl CollabPanel { } else { theme.contact_button.style_for(mouse_state) }; - render_icon_button(button_style, "icons/x_mark_8.svg") + render_icon_button(button_style, "icons/x.svg") .aligned() .flex_float() }) @@ -2340,7 +2340,7 @@ impl Panel for CollabPanel { fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { settings::get::(cx) .button - .then(|| "icons/speech_bubble_12.svg") + .then(|| "icons/conversations.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index b8969e2b9a..648fa141a5 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -132,7 +132,7 @@ export default function contacts_panel(): any { leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, channel_indent: INDENT_SIZE, - section_icon_size: 8, + section_icon_size: 14, header_row: { ...text(layer, "ui_sans", { size: "sm", weight: "bold" }), margin: { top: SPACING }, From e0d73842d2c7aeece689f96f9ad6973e0b87b104 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 16:12:39 -0400 Subject: [PATCH 203/326] Continue panel styles --- styles/src/component/indicator.ts | 9 +++++++ styles/src/style_tree/collab_panel.ts | 35 ++++++++++++--------------- 2 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 styles/src/component/indicator.ts diff --git a/styles/src/component/indicator.ts b/styles/src/component/indicator.ts new file mode 100644 index 0000000000..3a078fb53f --- /dev/null +++ b/styles/src/component/indicator.ts @@ -0,0 +1,9 @@ +import { background } from "../style_tree/components" +import { Layer, StyleSets } from "../theme" + +export const indicator = ({ layer, color }: { layer: Layer, color: StyleSets }) => ({ + corner_radius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: background(layer, color), +}) diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 648fa141a5..6cf6f9b095 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -10,6 +10,7 @@ import { useTheme } from "../theme" import collab_modals from "./collab_modals" import { text_button } from "../component/text_button" import { toggleable_icon_button } from "../component/icon_button" +import { indicator } from "../component/indicator" export default function contacts_panel(): any { const theme = useTheme() @@ -24,7 +25,7 @@ export default function contacts_panel(): any { const contact_button = { background: background(layer, "on"), color: foreground(layer, "on"), - icon_width: 8, + icon_width: 14, button_width: 16, corner_radius: 8, } @@ -199,19 +200,23 @@ export default function contacts_panel(): any { }), list_empty_label_container: { margin: { - left: 5, + left: NAME_MARGIN, } }, list_empty_icon: { - color: foreground(layer, "on"), - width: 16, + color: foreground(layer, "variant"), + width: 14, }, list_empty_state: toggleable({ base: interactive({ base: { ...text(layer, "ui_sans", "variant", { size: "sm" }), - padding: SPACING - + padding: { + top: SPACING / 2, + bottom: SPACING / 2, + left: SPACING, + right: SPACING + }, }, state: { clicked: { @@ -238,20 +243,10 @@ export default function contacts_panel(): any { }), contact_avatar: { corner_radius: 10, - width: 18, - }, - contact_status_free: { - corner_radius: 4, - padding: 4, - margin: { top: 12, left: 12 }, - background: foreground(layer, "positive"), - }, - contact_status_busy: { - corner_radius: 4, - padding: 4, - margin: { top: 12, left: 12 }, - background: foreground(layer, "negative"), + width: 20, }, + contact_status_free: indicator({ layer, color: "positive" }), + contact_status_busy: indicator({ layer, color: "negative" }), contact_username: { ...text(layer, "ui_sans", { size: "sm" }), margin: { @@ -302,7 +297,7 @@ export default function contacts_panel(): any { icon: { margin: { left: NAME_MARGIN }, color: foreground(layer, "variant"), - width: 12, + width: 14, }, name: { ...project_row.name, From b4b044ccbf7da62311f978d543433239b61b17e8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 17:01:34 -0400 Subject: [PATCH 204/326] Initial modal styles --- styles/src/component/input.ts | 3 -- styles/src/component/tab.ts | 73 ++++++++++++++++++++++++++ styles/src/style_tree/collab_modals.ts | 38 ++++++++------ 3 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 styles/src/component/tab.ts diff --git a/styles/src/component/input.ts b/styles/src/component/input.ts index 52d0b42d97..cadfcc8d4a 100644 --- a/styles/src/component/input.ts +++ b/styles/src/component/input.ts @@ -13,9 +13,6 @@ export const input = () => { selection: theme.players[0], text: text(theme.highest, "mono", "default"), border: border(theme.highest), - margin: { - right: 12, - }, padding: { top: 3, bottom: 3, diff --git a/styles/src/component/tab.ts b/styles/src/component/tab.ts new file mode 100644 index 0000000000..9938fb9311 --- /dev/null +++ b/styles/src/component/tab.ts @@ -0,0 +1,73 @@ +import { Layer } from "../common" +import { interactive, toggleable } from "../element" +import { Border, text } from "../style_tree/components" + +type TabProps = { + layer: Layer +} + +export const tab = ({ layer }: TabProps) => { + const active_color = text(layer, "sans", "base").color + const inactive_border: Border = { + color: '#FFFFFF00', + width: 1, + bottom: true, + left: false, + right: false, + top: false, + } + const active_border: Border = { + ...inactive_border, + color: active_color, + } + + const base = { + ...text(layer, "sans", "variant"), + padding: { + top: 8, + left: 8, + right: 8, + bottom: 6 + }, + border: inactive_border, + } + + const i = interactive({ + state: { + default: { + ...base + }, + hovered: { + ...base, + ...text(layer, "sans", "base", "hovered") + }, + clicked: { + ...base, + ...text(layer, "sans", "base", "pressed") + }, + } + }) + + return toggleable({ + base: i, + state: { + active: { + default: { + ...i, + ...text(layer, "sans", "base"), + border: active_border, + }, + hovered: { + ...i, + ...text(layer, "sans", "base", "hovered"), + border: active_border + }, + clicked: { + ...i, + ...text(layer, "sans", "base", "pressed"), + border: active_border + }, + } + } + }) +} diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index 95690b5d85..c0bf358e71 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -2,13 +2,16 @@ import { useTheme } from "../theme" import { background, border, foreground, text } from "./components" import picker from "./picker" import { input } from "../component/input" -import { toggleable_text_button } from "../component/text_button" import contact_finder from "./contact_finder" +import { tab } from "../component/tab" export default function channel_modal(): any { const theme = useTheme() - const side_margin = 6 + const SPACING = 12 as const + const BUTTON_OFFSET = 6 as const + const ITEM_HEIGHT = 36 as const + const contact_button = { background: background(theme.middle, "variant"), color: foreground(theme.middle, "variant"), @@ -26,20 +29,16 @@ export default function channel_modal(): any { return { contact_finder: contact_finder(), tabbed_modal: { - tab_button: toggleable_text_button(theme, { - variant: "ghost", - layer: theme.middle, - active_color: "accent", - margin: { - top: 8, - bottom: 8, - right: 4 - } - }), - row_height: 28, + tab_button: tab({ layer: theme.middle }), + row_height: ITEM_HEIGHT, header: { - background: background(theme.middle, "accent"), + background: background(theme.lowest), border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), + padding: { + top: SPACING, + left: SPACING - BUTTON_OFFSET, + right: SPACING - BUTTON_OFFSET, + }, corner_radii: { top_right: 12, top_left: 12, @@ -47,6 +46,13 @@ export default function channel_modal(): any { }, body: { background: background(theme.middle), + padding: { + top: SPACING - 4, + left: SPACING, + right: SPACING, + bottom: SPACING, + + }, corner_radii: { bottom_right: 12, bottom_left: 12, @@ -69,14 +75,14 @@ export default function channel_modal(): any { title: { ...text(theme.middle, "sans", "on", { size: "lg" }), padding: { - left: 6, + left: BUTTON_OFFSET, } }, picker: { empty_container: {}, item: { ...picker_style.item, - margin: { left: side_margin, right: side_margin }, + margin: { left: SPACING, right: SPACING }, }, no_matches: picker_style.no_matches, input_editor: picker_input, From 5b37cdcb042dc4487543c51416af75c78f6b8bec Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 14 Aug 2023 15:03:16 -0600 Subject: [PATCH 205/326] Better tests --- crates/editor/src/element.rs | 102 ++++++++++++++++++++++++++++-- crates/editor/src/multi_buffer.rs | 19 ++++++ 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6d0161e086..e2cd15f7ee 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -81,11 +81,13 @@ impl SelectionLayout { let mut active_rows = map.prev_line_boundary(point_selection.start).1.row() ..map.next_line_boundary(point_selection.end).1.row(); + // vim visual line mode if line_mode { let point_range = map.expand_to_line(point_selection.range()); range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map); } + // any vim visual mode (including line mode) if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed { if head.column() > 0 { head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left) @@ -94,8 +96,8 @@ impl SelectionLayout { DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)), Bias::Left, ); - - // updating range.end is a no-op unless you're on a multi-buffer divider + // updating range.end is a no-op unless you're cursor is + // on the newline containing a multi-buffer divider // in which case the clip_point may have moved the head up // an additional row. range.end = DisplayPoint::new(head.row() + 1, 0); @@ -2996,7 +2998,7 @@ mod tests { use language::language_settings; use log::info; use std::{num::NonZeroU32, sync::Arc}; - use util::test::sample_text; + use util::test::{generate_marked_text, sample_text}; #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { @@ -3018,7 +3020,7 @@ mod tests { } #[gpui::test] - fn test_vim_visual_selections(cx: &mut TestAppContext) { + async fn test_vim_visual_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); let (_, editor) = cx.add_window(|cx| { @@ -3053,26 +3055,112 @@ mod tests { let local_selections = &state.selections[0].1; assert_eq!(local_selections.len(), 3); // moves cursor back one line + assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); assert_eq!( local_selections[0].range, - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 6) + DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) ); + // moves cursor back one column assert_eq!( local_selections[1].range, - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 2) + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) ); + assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); + // leaves cursor on the max point assert_eq!( local_selections[2].range, DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) ); + assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); - // active lines does not include 1 + // active lines does not include 1 (even though the range of the selection does) assert_eq!( state.active_rows.keys().cloned().collect::>(), vec![0, 3, 5, 6] ); + + // multi-buffer support + // in DisplayPoint co-ordinates, this is what we're dealing with: + // 0: [[file + // 1: header]] + // 2: aaaaaa + // 3: bbbbbb + // 4: cccccc + // 5: + // 6: ... + // 7: ffffff + // 8: gggggg + // 9: hhhhhh + // 10: + // 11: [[file + // 12: header]] + // 13: bbbbbb + // 14: cccccc + // 15: dddddd + let (_, editor) = cx.add_window(|cx| { + let buffer = MultiBuffer::build_multi( + [ + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![ + Point::new(0, 0)..Point::new(3, 0), + Point::new(4, 0)..Point::new(7, 0), + ], + ), + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![Point::new(1, 0)..Point::new(3, 0)], + ), + ], + cx, + ); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }); + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, state) = editor.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), + DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), + ]); + }); + let mut new_parents = Default::default(); + let mut notify_views_if_parents_change = Default::default(); + let mut layout_cx = LayoutContext::new( + cx, + &mut new_parents, + &mut notify_views_if_parents_change, + false, + ); + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + &mut layout_cx, + ) + }); + + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 2); + + // moves cursor on excerpt boundary back a line + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[0].range, + DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); + + // moves cursor on buffer boundary back two lines + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[1].range, + DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); } #[gpui::test] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 31af03f768..8417c411f2 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1565,6 +1565,25 @@ impl MultiBuffer { cx.add_model(|cx| Self::singleton(buffer, cx)) } + pub fn build_multi( + excerpts: [(&str, Vec>); COUNT], + cx: &mut gpui::AppContext, + ) -> ModelHandle { + let multi = cx.add_model(|_| Self::new(0)); + for (text, ranges) in excerpts { + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { + context: range, + primary: None, + }); + multi.update(cx, |multi, cx| { + multi.push_excerpts(buffer, excerpt_ranges, cx) + }); + } + + multi + } + pub fn build_from_buffer( buffer: ModelHandle, cx: &mut gpui::AppContext, From ef73e77d3d6eda1fd17da4a4a450e55bf46c7a77 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 14 Aug 2023 17:15:25 -0400 Subject: [PATCH 206/326] Update some status bar icons and states --- crates/feedback/src/deploy_feedback_button.rs | 2 +- crates/project_panel/src/project_panel.rs | 2 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- styles/src/style_tree/status_bar.ts | 33 ++++++++++--------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index d197f57fa5..4b9768c07c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -44,7 +44,7 @@ impl View for DeployFeedbackButton { .in_state(active) .style_for(state); - Svg::new("icons/feedback_16.svg") + Svg::new("icons/feedback.svg") .with_color(style.icon_color) .constrained() .with_width(style.icon_size) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b7e1259b2c..12dfe59864 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1658,7 +1658,7 @@ impl workspace::dock::Panel for ProjectPanel { } fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { - Some("icons/folder_tree_16.svg") + Some("icons/project.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2277fd5dfb..7141cda172 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -394,7 +394,7 @@ impl Panel for TerminalPanel { } fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { - Some("icons/terminal_12.svg") + Some("icons/terminal.svg") } fn icon_tooltip(&self) -> (String, Option>) { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 970e0115df..08a1d08633 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -667,7 +667,7 @@ impl Item for TerminalView { Flex::row() .with_child( - gpui::elements::Svg::new("icons/terminal_12.svg") + gpui::elements::Svg::new("icons/terminal.svg") .with_color(tab_theme.label.text.color) .constrained() .with_width(tab_theme.type_icon_width) diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index d35b721c6c..2d3b81f7c2 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -28,16 +28,16 @@ export default function status_bar(): any { right: 6, }, border: border(layer, { top: true, overlay: true }), - cursor_position: text(layer, "sans", "variant", { size: "xs" }), + cursor_position: text(layer, "sans", "base", { size: "xs" }), vim_mode_indicator: { margin: { left: 6 }, - ...text(layer, "mono", "variant", { size: "xs" }), + ...text(layer, "mono", "base", { size: "xs" }), }, active_language: text_button({ - color: "variant" + color: "base" }), - auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }), - auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }), + auto_update_progress_message: text(layer, "sans", "base", { size: "xs" }), + auto_update_done_message: text(layer, "sans", "base", { size: "xs" }), lsp_status: interactive({ base: { ...diagnostic_status_container, @@ -64,11 +64,11 @@ export default function status_bar(): any { diagnostic_summary: interactive({ base: { height: 20, - icon_width: 16, + icon_width: 14, icon_spacing: 2, summary_spacing: 6, text: text(layer, "sans", { size: "sm" }), - icon_color_ok: foreground(layer, "variant"), + icon_color_ok: foreground(layer, "base"), icon_color_warning: foreground(layer, "warning"), icon_color_error: foreground(layer, "negative"), container_ok: { @@ -111,8 +111,9 @@ export default function status_bar(): any { base: interactive({ base: { ...status_container, - icon_size: 16, - icon_color: foreground(layer, "variant"), + icon_size: 14, + icon_color: foreground(layer, "base"), + background: background(layer, "default"), label: { margin: { left: 6 }, ...text(layer, "sans", { size: "xs" }), @@ -120,23 +121,25 @@ export default function status_bar(): any { }, state: { hovered: { - icon_color: foreground(layer, "hovered"), - background: background(layer, "variant"), + background: background(layer, "hovered"), }, + clicked: { + background: background(layer, "pressed"), + } }, }), state: { active: { default: { - icon_color: foreground(layer, "active"), - background: background(layer, "active"), + icon_color: foreground(layer, "accent", "default"), + background: background(layer, "default"), }, hovered: { - icon_color: foreground(layer, "hovered"), + icon_color: foreground(layer, "accent", "hovered"), background: background(layer, "hovered"), }, clicked: { - icon_color: foreground(layer, "pressed"), + icon_color: foreground(layer, "accent", "pressed"), background: background(layer, "pressed"), }, }, From 1af7425059f2ec5128ac292396203811b54ffca2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 14 Aug 2023 15:51:00 -0600 Subject: [PATCH 207/326] Fix vim escape in normal mode Fixes: zed-industries/community#1857 --- assets/keymaps/vim.json | 16 ++++++++++++++++ crates/vim/src/test.rs | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 94a271f037..3771c5c07a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -236,6 +236,14 @@ "ctrl-w ctrl-q": "pane::CloseAllItems" } }, + { + // escape is in its own section so that it cancels a pending count. + "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", + "bindings": { + "escape": "editor::Cancel", + "ctrl+[": "editor::Cancel" + } + }, { "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting", "bindings": { @@ -389,6 +397,14 @@ "vim::SwitchMode", "Normal" ], + "escape": [ + "vim::SwitchMode", + "Normal" + ], + "ctrl+[": [ + "vim::SwitchMode", + "Normal" + ], ">": "editor::Indent", "<": "editor::Outdent" } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 474f2128fc..b95dab91bc 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -157,6 +157,16 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { cx.assert_state("aˇbc\n", Mode::Insert); } +#[gpui::test] +async fn test_escape_cancels(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aˇbˇc", Mode::Normal); + cx.simulate_keystrokes(["escape"]); + + cx.assert_state("aˇbc", Mode::Normal); +} + #[gpui::test] async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; From d7f21a9155419b104317a57c48739ae0d0052341 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 16:27:35 -0700 Subject: [PATCH 208/326] Ensure channels are sorted alphabetically Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 186 +++++---- crates/client/src/channel_store_tests.rs | 92 ++++- crates/collab/src/tests/channel_tests.rs | 474 ++++++++++++----------- crates/collab_ui/src/collab_panel.rs | 72 ++-- 4 files changed, 477 insertions(+), 347 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 206423579a..8217e6cbc8 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -14,7 +14,8 @@ pub type ChannelId = u64; pub type UserId = u64; pub struct ChannelStore { - channels: Vec>, + channels_by_id: HashMap>, + channel_paths: Vec>, channel_invitations: Vec>, channel_participants: HashMap>>, channels_with_admin_privileges: HashSet, @@ -29,8 +30,6 @@ pub struct ChannelStore { pub struct Channel { pub id: ChannelId, pub name: String, - pub parent_id: Option, - pub depth: usize, } pub struct ChannelMembership { @@ -69,10 +68,11 @@ impl ChannelStore { if matches!(status, Status::ConnectionLost | Status::SignedOut) { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.channels.clear(); + this.channels_by_id.clear(); this.channel_invitations.clear(); this.channel_participants.clear(); this.channels_with_admin_privileges.clear(); + this.channel_paths.clear(); this.outgoing_invites.clear(); cx.notify(); }); @@ -83,8 +83,9 @@ impl ChannelStore { } }); Self { - channels: vec![], - channel_invitations: vec![], + channels_by_id: HashMap::default(), + channel_invitations: Vec::default(), + channel_paths: Vec::default(), channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), @@ -95,31 +96,43 @@ impl ChannelStore { } } - pub fn channels(&self) -> &[Arc] { - &self.channels + pub fn channel_count(&self) -> usize { + self.channel_paths.len() + } + + pub fn channels(&self) -> impl '_ + Iterator)> { + self.channel_paths.iter().map(move |path| { + let id = path.last().unwrap(); + let channel = self.channel_for_id(*id).unwrap(); + (path.len() - 1, channel) + }) + } + + pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc)> { + let path = self.channel_paths.get(ix)?; + let id = path.last().unwrap(); + let channel = self.channel_for_id(*id).unwrap(); + Some((path.len() - 1, channel)) } pub fn channel_invitations(&self) -> &[Arc] { &self.channel_invitations } - pub fn channel_for_id(&self, channel_id: ChannelId) -> Option> { - self.channels.iter().find(|c| c.id == channel_id).cloned() + pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc> { + self.channels_by_id.get(&channel_id) } - pub fn is_user_admin(&self, mut channel_id: ChannelId) -> bool { - loop { - if self.channels_with_admin_privileges.contains(&channel_id) { - return true; + pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { + self.channel_paths.iter().any(|path| { + if let Some(ix) = path.iter().position(|id| *id == channel_id) { + path[..=ix] + .iter() + .any(|id| self.channels_with_admin_privileges.contains(id)) + } else { + false } - if let Some(channel) = self.channel_for_id(channel_id) { - if let Some(parent_id) = channel.parent_id { - channel_id = parent_id; - continue; - } - } - return false; - } + }) } pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc] { @@ -373,69 +386,78 @@ impl ChannelStore { payload: proto::UpdateChannels, cx: &mut ModelContext, ) { - self.channels - .retain(|channel| !payload.remove_channels.contains(&channel.id)); - self.channel_invitations - .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); - self.channel_participants - .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); - self.channels_with_admin_privileges - .retain(|channel_id| !payload.remove_channels.contains(channel_id)); - - for channel in payload.channel_invitations { - if let Some(existing_channel) = self - .channel_invitations - .iter_mut() - .find(|c| c.id == channel.id) - { - let existing_channel = Arc::make_mut(existing_channel); - existing_channel.name = channel.name; - continue; - } - - self.channel_invitations.insert( - 0, - Arc::new(Channel { - id: channel.id, - name: channel.name, - parent_id: None, - depth: 0, - }), - ); + if !payload.remove_channel_invitations.is_empty() { + self.channel_invitations + .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); } - - for channel in payload.channels { - if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) { - let existing_channel = Arc::make_mut(existing_channel); - existing_channel.name = channel.name; - continue; - } - - if let Some(parent_id) = channel.parent_id { - if let Some(ix) = self.channels.iter().position(|c| c.id == parent_id) { - let parent_channel = &self.channels[ix]; - let depth = parent_channel.depth + 1; - self.channels.insert( - ix + 1, - Arc::new(Channel { - id: channel.id, - name: channel.name, - parent_id: Some(parent_id), - depth, - }), - ); - } - } else { - self.channels.insert( - 0, + for channel in payload.channel_invitations { + match self + .channel_invitations + .binary_search_by_key(&channel.id, |c| c.id) + { + Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name, + Err(ix) => self.channel_invitations.insert( + ix, + Arc::new(Channel { + id: channel.id, + name: channel.name, + }), + ), + } + } + + let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty(); + if channels_changed { + if !payload.remove_channels.is_empty() { + self.channels_by_id + .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); + self.channel_participants + .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); + self.channels_with_admin_privileges + .retain(|channel_id| !payload.remove_channels.contains(channel_id)); + } + + for channel in payload.channels { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) { + let existing_channel = Arc::make_mut(existing_channel); + existing_channel.name = channel.name; + continue; + } + self.channels_by_id.insert( + channel.id, Arc::new(Channel { id: channel.id, name: channel.name, - parent_id: None, - depth: 0, }), ); + + if let Some(parent_id) = channel.parent_id { + let mut ix = 0; + while ix < self.channel_paths.len() { + let path = &self.channel_paths[ix]; + if path.ends_with(&[parent_id]) { + let mut new_path = path.clone(); + new_path.push(channel.id); + self.channel_paths.insert(ix + 1, new_path); + ix += 1; + } + ix += 1; + } + } else { + self.channel_paths.push(vec![channel.id]); + } } + + self.channel_paths.sort_by(|a, b| { + let a = Self::channel_path_sorting_key(a, &self.channels_by_id); + let b = Self::channel_path_sorting_key(b, &self.channels_by_id); + a.cmp(b) + }); + self.channel_paths.dedup(); + self.channel_paths.retain(|path| { + path.iter() + .all(|channel_id| self.channels_by_id.contains_key(channel_id)) + }); } for permission in payload.channel_permissions { @@ -492,4 +514,12 @@ impl ChannelStore { cx.notify(); } + + fn channel_path_sorting_key<'a>( + path: &'a [ChannelId], + channels_by_id: &'a HashMap>, + ) -> impl 'a + Iterator> { + path.iter() + .map(|id| Some(channels_by_id.get(id)?.name.as_str())) + } } diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs index f74169eb2a..3a3f3842eb 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/client/src/channel_store_tests.rs @@ -36,8 +36,8 @@ fn test_update_channels(cx: &mut AppContext) { &channel_store, &[ // - (0, "a", false), - (0, "b", true), + (0, "a".to_string(), false), + (0, "b".to_string(), true), ], cx, ); @@ -64,15 +64,76 @@ fn test_update_channels(cx: &mut AppContext) { assert_channels( &channel_store, &[ - (0, "a", false), - (1, "y", false), - (0, "b", true), - (1, "x", true), + (0, "a".to_string(), false), + (1, "y".to_string(), false), + (0, "b".to_string(), true), + (1, "x".to_string(), true), ], cx, ); } +#[gpui::test] +fn test_dangling_channel_paths(cx: &mut AppContext) { + let http = FakeHttpClient::with_404_response(); + let client = Client::new(http.clone(), cx); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + + let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx)); + + update_channels( + &channel_store, + proto::UpdateChannels { + channels: vec![ + proto::Channel { + id: 0, + name: "a".to_string(), + parent_id: None, + }, + proto::Channel { + id: 1, + name: "b".to_string(), + parent_id: Some(0), + }, + proto::Channel { + id: 2, + name: "c".to_string(), + parent_id: Some(1), + }, + ], + channel_permissions: vec![proto::ChannelPermission { + channel_id: 0, + is_admin: true, + }], + ..Default::default() + }, + cx, + ); + // Sanity check + assert_channels( + &channel_store, + &[ + // + (0, "a".to_string(), true), + (1, "b".to_string(), true), + (2, "c".to_string(), true), + ], + cx, + ); + + update_channels( + &channel_store, + proto::UpdateChannels { + remove_channels: vec![1, 2], + ..Default::default() + }, + cx, + ); + + // Make sure that the 1/2/3 path is gone + assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx); +} + fn update_channels( channel_store: &ModelHandle, message: proto::UpdateChannels, @@ -84,15 +145,20 @@ fn update_channels( #[track_caller] fn assert_channels( channel_store: &ModelHandle, - expected_channels: &[(usize, &str, bool)], + expected_channels: &[(usize, String, bool)], cx: &AppContext, ) { - channel_store.read_with(cx, |store, _| { - let actual = store + let actual = channel_store.read_with(cx, |store, _| { + store .channels() - .iter() - .map(|c| (c.depth, c.name.as_str(), store.is_user_admin(c.id))) - .collect::>(); - assert_eq!(actual, expected_channels); + .map(|(depth, channel)| { + ( + depth, + channel.name.to_string(), + store.is_user_admin(channel.id), + ) + }) + .collect::>() }); + assert_eq!(actual, expected_channels); } diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 0dc6d478d1..f1157ce7ae 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -3,8 +3,8 @@ use crate::{ tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; -use client::{Channel, ChannelMembership, User}; -use gpui::{executor::Deterministic, TestAppContext}; +use client::{ChannelId, ChannelMembership, ChannelStore, User}; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use std::sync::Arc; @@ -35,31 +35,28 @@ async fn test_core_channels( .unwrap(); deterministic.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_eq!( - channels.channels(), - &[ - Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - }), - Arc::new(Channel { - id: channel_b_id, - name: "channel-b".to_string(), - parent_id: Some(channel_a_id), - depth: 1, - }) - ] - ); - assert!(channels.is_user_admin(channel_a_id)); - assert!(channels.is_user_admin(channel_b_id)); - }); + assert_channels( + client_a.channel_store(), + cx_a, + &[ + ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: true, + }, + ExpectedChannel { + id: channel_b_id, + name: "channel-b".to_string(), + depth: 1, + user_is_admin: true, + }, + ], + ); - client_b - .channel_store() - .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); + client_b.channel_store().read_with(cx_b, |channels, _| { + assert!(channels.channels().collect::>().is_empty()) + }); // Invite client B to channel A as client A. client_a @@ -78,17 +75,16 @@ async fn test_core_channels( // Client A sees that B has been invited. deterministic.run_until_parked(); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channel_invitations(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - })] - ) - }); + assert_channel_invitations( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: false, + }], + ); let members = client_a .channel_store() @@ -125,28 +121,25 @@ async fn test_core_channels( deterministic.run_until_parked(); // Client B now sees that they are a member of channel A and its existing subchannels. - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!(channels.channel_invitations(), &[]); - assert_eq!( - channels.channels(), - &[ - Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - }), - Arc::new(Channel { - id: channel_b_id, - name: "channel-b".to_string(), - parent_id: Some(channel_a_id), - depth: 1, - }) - ] - ); - assert!(!channels.is_user_admin(channel_a_id)); - assert!(!channels.is_user_admin(channel_b_id)); - }); + assert_channel_invitations(client_b.channel_store(), cx_b, &[]); + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + user_is_admin: false, + depth: 0, + }, + ExpectedChannel { + id: channel_b_id, + name: "channel-b".to_string(), + user_is_admin: false, + depth: 1, + }, + ], + ); let channel_c_id = client_a .channel_store() @@ -157,31 +150,30 @@ async fn test_core_channels( .unwrap(); deterministic.run_until_parked(); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channels(), - &[ - Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - }), - Arc::new(Channel { - id: channel_b_id, - name: "channel-b".to_string(), - parent_id: Some(channel_a_id), - depth: 1, - }), - Arc::new(Channel { - id: channel_c_id, - name: "channel-c".to_string(), - parent_id: Some(channel_b_id), - depth: 2, - }), - ] - ) - }); + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + user_is_admin: false, + depth: 0, + }, + ExpectedChannel { + id: channel_b_id, + name: "channel-b".to_string(), + user_is_admin: false, + depth: 1, + }, + ExpectedChannel { + id: channel_c_id, + name: "channel-c".to_string(), + user_is_admin: false, + depth: 2, + }, + ], + ); // Update client B's membership to channel A to be an admin. client_a @@ -195,34 +187,31 @@ async fn test_core_channels( // Observe that client B is now an admin of channel A, and that // their admin priveleges extend to subchannels of channel A. - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!(channels.channel_invitations(), &[]); - assert_eq!( - channels.channels(), - &[ - Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - }), - Arc::new(Channel { - id: channel_b_id, - name: "channel-b".to_string(), - parent_id: Some(channel_a_id), - depth: 1, - }), - Arc::new(Channel { - id: channel_c_id, - name: "channel-c".to_string(), - parent_id: Some(channel_b_id), - depth: 2, - }), - ] - ); - - assert!(channels.is_user_admin(channel_c_id)) - }); + assert_channel_invitations(client_b.channel_store(), cx_b, &[]); + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: true, + }, + ExpectedChannel { + id: channel_b_id, + name: "channel-b".to_string(), + depth: 1, + user_is_admin: true, + }, + ExpectedChannel { + id: channel_c_id, + name: "channel-c".to_string(), + depth: 2, + user_is_admin: true, + }, + ], + ); // Client A deletes the channel, deletion also deletes subchannels. client_a @@ -234,30 +223,26 @@ async fn test_core_channels( .unwrap(); deterministic.run_until_parked(); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - - depth: 0, - })] - ) - }); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - - depth: 0, - })] - ) - }); + assert_channels( + client_a.channel_store(), + cx_a, + &[ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: true, + }], + ); + assert_channels( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: true, + }], + ); // Remove client B client_a @@ -271,46 +256,38 @@ async fn test_core_channels( deterministic.run_until_parked(); // Client A still has their channel - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - })] - ) - }); + assert_channels( + client_a.channel_store(), + cx_a, + &[ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: true, + }], + ); - // Client B is gone - client_b - .channel_store() - .read_with(cx_b, |channels, _| assert_eq!(channels.channels(), &[])); + // Client B no longer has access to the channel + assert_channels(client_b.channel_store(), cx_b, &[]); // When disconnected, client A sees no channels. server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_eq!(channels.channels(), &[]); - assert!(!channels.is_user_admin(channel_a_id)); - }); + assert_channels(client_a.channel_store(), cx_a, &[]); server.allow_connections(); deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: channel_a_id, - name: "channel-a".to_string(), - parent_id: None, - depth: 0, - })] - ); - assert!(channels.is_user_admin(channel_a_id)); - }); + assert_channels( + client_a.channel_store(), + cx_a, + &[ExpectedChannel { + id: channel_a_id, + name: "channel-a".to_string(), + depth: 0, + user_is_admin: true, + }], + ); } fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u64]) { @@ -404,20 +381,21 @@ async fn test_channel_room( ); }); + assert_channels( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + id: zed_id, + name: "zed".to_string(), + depth: 0, + user_is_admin: false, + }], + ); client_b.channel_store().read_with(cx_b, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap()], ); - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: zed_id, - name: "zed".to_string(), - parent_id: None, - depth: 0, - })] - ) }); client_c.channel_store().read_with(cx_c, |channels, _| { @@ -629,20 +607,17 @@ async fn test_permissions_update_while_invited( deterministic.run_until_parked(); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channel_invitations(), - &[Arc::new(Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - - depth: 0, - })], - ); - - assert_eq!(channels.channels(), &[],); - }); + assert_channel_invitations( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + depth: 0, + id: rust_id, + name: "rust".to_string(), + user_is_admin: false, + }], + ); + assert_channels(client_b.channel_store(), cx_b, &[]); // Update B's invite before they've accepted it client_a @@ -655,20 +630,17 @@ async fn test_permissions_update_while_invited( deterministic.run_until_parked(); - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channel_invitations(), - &[Arc::new(Channel { - id: rust_id, - name: "rust".to_string(), - parent_id: None, - - depth: 0, - })], - ); - - assert_eq!(channels.channels(), &[],); - }); + assert_channel_invitations( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + depth: 0, + id: rust_id, + name: "rust".to_string(), + user_is_admin: false, + }], + ); + assert_channels(client_b.channel_store(), cx_b, &[]); } #[gpui::test] @@ -695,34 +667,78 @@ async fn test_channel_rename( .await .unwrap(); - let rust_archive_id = rust_id; deterministic.run_until_parked(); // Client A sees the channel with its new name. - client_a.channel_store().read_with(cx_a, |channels, _| { - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: rust_archive_id, - name: "rust-archive".to_string(), - parent_id: None, - - depth: 0, - })], - ); - }); + assert_channels( + client_a.channel_store(), + cx_a, + &[ExpectedChannel { + depth: 0, + id: rust_id, + name: "rust-archive".to_string(), + user_is_admin: true, + }], + ); // Client B sees the channel with its new name. - client_b.channel_store().read_with(cx_b, |channels, _| { - assert_eq!( - channels.channels(), - &[Arc::new(Channel { - id: rust_archive_id, - name: "rust-archive".to_string(), - parent_id: None, - - depth: 0, - })], - ); - }); + assert_channels( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + depth: 0, + id: rust_id, + name: "rust-archive".to_string(), + user_is_admin: false, + }], + ); +} + +#[derive(Debug, PartialEq)] +struct ExpectedChannel { + depth: usize, + id: ChannelId, + name: String, + user_is_admin: bool, +} + +#[track_caller] +fn assert_channel_invitations( + channel_store: &ModelHandle, + cx: &TestAppContext, + expected_channels: &[ExpectedChannel], +) { + let actual = channel_store.read_with(cx, |store, _| { + store + .channel_invitations() + .iter() + .map(|channel| ExpectedChannel { + depth: 0, + name: channel.name.clone(), + id: channel.id, + user_is_admin: store.is_user_admin(channel.id), + }) + .collect::>() + }); + assert_eq!(actual, expected_channels); +} + +#[track_caller] +fn assert_channels( + channel_store: &ModelHandle, + cx: &TestAppContext, + expected_channels: &[ExpectedChannel], +) { + let actual = channel_store.read_with(cx, |store, _| { + store + .channels() + .map(|(depth, channel)| ExpectedChannel { + depth, + name: channel.name.clone(), + id: channel.id, + user_is_admin: store.is_user_admin(channel.id), + }) + .collect::>() + }); + assert_eq!(actual, expected_channels); } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8c63649ef9..563cc942da 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -194,7 +194,10 @@ enum ListEntry { IncomingRequest(Arc), OutgoingRequest(Arc), ChannelInvite(Arc), - Channel(Arc), + Channel { + channel: Arc, + depth: usize, + }, ChannelEditor { depth: usize, }, @@ -315,9 +318,10 @@ impl CollabPanel { cx, ) } - ListEntry::Channel(channel) => { + ListEntry::Channel { channel, depth } => { let channel_row = this.render_channel( &*channel, + *depth, &theme.collab_panel, is_selected, cx, @@ -438,7 +442,7 @@ impl CollabPanel { if this.take_editing_state(cx) { this.update_entries(false, cx); this.selection = this.entries.iter().position(|entry| { - if let ListEntry::Channel(channel) = entry { + if let ListEntry::Channel { channel, .. } = entry { channel.id == *channel_id } else { false @@ -621,17 +625,19 @@ impl CollabPanel { if self.include_channels_section(cx) { self.entries.push(ListEntry::Header(Section::Channels, 0)); - let channels = channel_store.channels(); - if !(channels.is_empty() && self.channel_editing_state.is_none()) { + if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { self.match_candidates.clear(); self.match_candidates - .extend(channels.iter().enumerate().map(|(ix, channel)| { - StringMatchCandidate { - id: ix, - string: channel.name.clone(), - char_bag: channel.name.chars().collect(), - } - })); + .extend( + channel_store + .channels() + .enumerate() + .map(|(ix, (_, channel))| StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + }), + ); let matches = executor.block(match_strings( &self.match_candidates, &query, @@ -652,26 +658,30 @@ impl CollabPanel { } } for mat in matches { - let channel = &channels[mat.candidate_id]; + let (depth, channel) = + channel_store.channel_at_index(mat.candidate_id).unwrap(); match &self.channel_editing_state { Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => { - self.entries.push(ListEntry::Channel(channel.clone())); - self.entries.push(ListEntry::ChannelEditor { - depth: channel.depth + 1, + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, }); + self.entries + .push(ListEntry::ChannelEditor { depth: depth + 1 }); } Some(ChannelEditingState::Rename { channel_id, .. }) if *channel_id == channel.id => { - self.entries.push(ListEntry::ChannelEditor { - depth: channel.depth, - }); + self.entries.push(ListEntry::ChannelEditor { depth }); } _ => { - self.entries.push(ListEntry::Channel(channel.clone())); + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + }); } } } @@ -1497,6 +1507,7 @@ impl CollabPanel { fn render_channel( &self, channel: &Channel, + depth: usize, theme: &theme::CollabPanel, is_selected: bool, cx: &mut ViewContext, @@ -1542,7 +1553,7 @@ impl CollabPanel { .with_style(*theme.contact_row.style_for(is_selected, state)) .with_padding_left( theme.contact_row.default_style().padding.left - + theme.channel_indent * channel.depth as f32, + + theme.channel_indent * depth as f32, ) }) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1884,7 +1895,7 @@ impl CollabPanel { }); } } - ListEntry::Channel(channel) => { + ListEntry::Channel { channel, .. } => { self.join_channel(channel.id, cx); } ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), @@ -2031,7 +2042,7 @@ impl CollabPanel { if !channel_store.is_user_admin(action.channel_id) { return; } - if let Some(channel) = channel_store.channel_for_id(action.channel_id) { + if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { self.channel_editing_state = Some(ChannelEditingState::Rename { channel_id: action.channel_id, pending_name: None, @@ -2058,7 +2069,7 @@ impl CollabPanel { self.selection .and_then(|ix| self.entries.get(ix)) .and_then(|entry| match entry { - ListEntry::Channel(channel) => Some(channel), + ListEntry::Channel { channel, .. } => Some(channel), _ => None, }) } @@ -2395,9 +2406,16 @@ impl PartialEq for ListEntry { return peer_id_1 == peer_id_2; } } - ListEntry::Channel(channel_1) => { - if let ListEntry::Channel(channel_2) = other { - return channel_1.id == channel_2.id; + ListEntry::Channel { + channel: channel_1, + depth: depth_1, + } => { + if let ListEntry::Channel { + channel: channel_2, + depth: depth_2, + } = other + { + return channel_1.id == channel_2.id && depth_1 == depth_2; } } ListEntry::ChannelInvite(channel_1) => { From 5af8ee71aa9d35d594f4777f2049f3c8a70a7dbd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 16:38:21 -0700 Subject: [PATCH 209/326] Fix clicking outside of modals to dismiss them Co-authored-by: Mikayla --- crates/workspace/src/workspace.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f875c71fe6..60488d04cf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3760,20 +3760,19 @@ impl View for Workspace { ) })) .with_children(self.modal.as_ref().map(|modal| { + // Prevent clicks within the modal from falling + // through to the rest of the workspace. enum ModalBackground {} MouseEventHandler::::new( 0, cx, - |_, cx| { - ChildView::new(modal.view.as_any(), cx) - .contained() - .with_style(theme.workspace.modal) - .aligned() - .top() - }, + |_, cx| ChildView::new(modal.view.as_any(), cx), ) .on_click(MouseButton::Left, |_, _, _| {}) - // Consume click events to stop focus dropping through + .contained() + .with_style(theme.workspace.modal) + .aligned() + .top() })) .with_children(self.render_notifications(&theme.workspace, cx)), )) From 13982fe2f4a47fe75a98360f2d70cd9bea9af53d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 16:47:26 -0700 Subject: [PATCH 210/326] Display intended mute status while still connecting to a room Co-authored-by: Mikayla --- crates/call/src/room.rs | 6 +++--- crates/collab_ui/src/collab_titlebar_item.rs | 4 ++-- crates/collab_ui/src/collab_ui.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 683ff6f4df..5a4bc8329f 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1116,11 +1116,11 @@ impl Room { }) } - pub fn is_muted(&self) -> bool { + pub fn is_muted(&self, cx: &AppContext) -> bool { self.live_kit .as_ref() .and_then(|live_kit| match &live_kit.microphone_track { - LocalTrack::None => Some(true), + LocalTrack::None => Some(settings::get::(cx).mute_on_join), LocalTrack::Pending { muted, .. } => Some(*muted), LocalTrack::Published { muted, .. } => Some(*muted), }) @@ -1310,7 +1310,7 @@ impl Room { } pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { - let should_mute = !self.is_muted(); + let should_mute = !self.is_muted(cx); if let Some(live_kit) = self.live_kit.as_mut() { if matches!(live_kit.microphone_track, LocalTrack::None) { return Ok(self.share_microphone(cx)); diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 97881f9a50..22f294d3fc 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -90,7 +90,7 @@ impl View for CollabTitlebarItem { right_container .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); right_container.add_child(self.render_leave_call(&theme, cx)); - let muted = room.read(cx).is_muted(); + let muted = room.read(cx).is_muted(cx); let speaking = room.read(cx).is_speaking(); left_container.add_child( self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx), @@ -544,7 +544,7 @@ impl CollabTitlebarItem { ) -> AnyElement { let icon; let tooltip; - let is_muted = room.read(cx).is_muted(); + let is_muted = room.read(cx).is_muted(cx); if is_muted { icon = "icons/radix/mic-mute.svg"; tooltip = "Unmute microphone"; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 1e48026f46..f2ba35967f 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -64,7 +64,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { if let Some(room) = call.room().cloned() { let client = call.client(); room.update(cx, |room, cx| { - if room.is_muted() { + if room.is_muted(cx) { ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx); } else { ActiveCall::report_call_event_for_room( From 71454ba27cfa2135cf20e403fd87def27ebca408 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 17:11:03 -0700 Subject: [PATCH 211/326] Limit number of participants shown in channel face piles Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 45 +++++++++++++++++++-------- crates/collab_ui/src/face_pile.rs | 1 + crates/theme/src/theme.rs | 1 + styles/src/style_tree/collab_panel.ts | 9 ++++++ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 563cc942da..e4838df939 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1514,6 +1514,8 @@ impl CollabPanel { ) -> AnyElement { let channel_id = channel.id; + const FACEPILE_LIMIT: usize = 4; + MouseEventHandler::::new(channel.id as usize, cx, |state, cx| { Flex::row() .with_child( @@ -1532,20 +1534,37 @@ impl CollabPanel { .left() .flex(1., true), ) - .with_child( - FacePile::new(theme.face_overlap).with_children( - self.channel_store - .read(cx) - .channel_participants(channel_id) - .iter() - .filter_map(|user| { - Some( - Image::from_data(user.avatar.clone()?) - .with_style(theme.contact_avatar), + .with_children({ + let participants = self.channel_store.read(cx).channel_participants(channel_id); + if !participants.is_empty() { + let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + Some( + FacePile::new(theme.face_overlap) + .with_children( + participants + .iter() + .filter_map(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(theme.contact_avatar), + ) + }) + .take(FACEPILE_LIMIT), ) - }), - ), - ) + .with_children((extra_count > 0).then(|| { + Label::new( + format!("+{}", extra_count), + theme.extra_participant_label.text.clone(), + ) + .contained() + .with_style(theme.extra_participant_label.container) + })), + ) + } else { + None + } + }) .align_children_center() .constrained() .with_height(theme.row_height) diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index ba9b61c98b..a86b257686 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -68,6 +68,7 @@ impl Element for FacePile { for face in self.faces.iter_mut().rev() { let size = face.size(); origin_x -= size.x(); + let origin_y = origin_y + (bounds.height() - size.y()) / 2.0; scene.paint_layer(None, |scene| { face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx); }); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4919eb93c7..e081b70047 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -241,6 +241,7 @@ pub struct CollabPanel { pub project_row: Toggleable>, pub tree_branch: Toggleable>, pub contact_avatar: ImageStyle, + pub extra_participant_label: ContainedText, pub contact_status_free: ContainerStyle, pub contact_status_busy: ContainerStyle, pub contact_username: ContainedText, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 6cf6f9b095..1d1e09075e 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -245,6 +245,15 @@ export default function contacts_panel(): any { corner_radius: 10, width: 20, }, + extra_participant_label: { + corner_radius: 10, + padding: { + left: 10, + right: 4, + }, + background: background(layer, "hovered"), + ...text(layer, "ui_sans", "hovered", { size: "xs" }) + }, contact_status_free: indicator({ layer, color: "positive" }), contact_status_busy: indicator({ layer, color: "negative" }), contact_username: { From cbf497bc12dee7227ade37c410993deafba69595 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 14 Aug 2023 17:36:35 -0700 Subject: [PATCH 212/326] Fix race condition when UpdateChannel message is received while fetching participants for previous update Co-authored-by: Mikayla --- crates/client/src/channel_store.rs | 45 ++++++++++++++++++------ crates/client/src/channel_store_tests.rs | 3 +- crates/collab/src/rpc.rs | 5 --- crates/collab/src/tests/channel_tests.rs | 2 ++ crates/collab_ui/src/collab_panel.rs | 2 -- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 8217e6cbc8..e2c18a63a9 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -4,11 +4,13 @@ use anyhow::anyhow; use anyhow::Result; use collections::HashMap; use collections::HashSet; +use futures::channel::mpsc; use futures::Future; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; +use util::ResultExt; pub type ChannelId = u64; pub type UserId = u64; @@ -20,10 +22,12 @@ pub struct ChannelStore { channel_participants: HashMap>>, channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, + update_channels_tx: mpsc::UnboundedSender, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, _watch_connection_status: Task<()>, + _update_channels: Task<()>, } #[derive(Clone, Debug, PartialEq)] @@ -62,6 +66,7 @@ impl ChannelStore { let rpc_subscription = client.add_message_handler(cx.handle(), Self::handle_update_channels); + let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); 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 { @@ -89,10 +94,23 @@ impl ChannelStore { channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), + update_channels_tx, client, user_store, _rpc_subscription: rpc_subscription, _watch_connection_status: watch_connection_status, + _update_channels: cx.spawn_weak(|this, mut cx| async move { + while let Some(update_channels) = update_channels_rx.next().await { + if let Some(this) = this.upgrade(&cx) { + let update_task = this.update(&mut cx, |this, cx| { + this.update_channels(update_channels, cx) + }); + if let Some(update_task) = update_task { + update_task.await.log_err(); + } + } + } + }), } } @@ -159,13 +177,14 @@ impl ChannelStore { let channel_id = channel.id; this.update(&mut cx, |this, cx| { - this.update_channels( + let task = this.update_channels( proto::UpdateChannels { channels: vec![channel], ..Default::default() }, cx, ); + assert!(task.is_none()); // This event is emitted because the collab panel wants to clear the pending edit state // before this frame is rendered. But we can't guarantee that the collab panel's future @@ -287,13 +306,14 @@ impl ChannelStore { .channel .ok_or_else(|| anyhow!("missing channel in response"))?; this.update(&mut cx, |this, cx| { - this.update_channels( + let task = this.update_channels( proto::UpdateChannels { channels: vec![channel], ..Default::default() }, cx, ); + assert!(task.is_none()); // This event is emitted because the collab panel wants to clear the pending edit state // before this frame is rendered. But we can't guarantee that the collab panel's future @@ -375,8 +395,10 @@ impl ChannelStore { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.update_channels(message.payload, cx); + this.update(&mut cx, |this, _| { + this.update_channels_tx + .unbounded_send(message.payload) + .unwrap(); }); Ok(()) } @@ -385,7 +407,7 @@ impl ChannelStore { &mut self, payload: proto::UpdateChannels, cx: &mut ModelContext, - ) { + ) -> Option>> { if !payload.remove_channel_invitations.is_empty() { self.channel_invitations .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id)); @@ -470,6 +492,11 @@ impl ChannelStore { } } + cx.notify(); + if payload.channel_participants.is_empty() { + return None; + } + let mut all_user_ids = Vec::new(); let channel_participants = payload.channel_participants; for entry in &channel_participants { @@ -480,11 +507,10 @@ impl ChannelStore { } } - // TODO: Race condition if an update channels messages comes in while resolving avatars let users = self .user_store .update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx)); - cx.spawn(|this, mut cx| async move { + Some(cx.spawn(|this, mut cx| async move { let users = users.await?; this.update(&mut cx, |this, cx| { @@ -509,10 +535,7 @@ impl ChannelStore { cx.notify(); }); anyhow::Ok(()) - }) - .detach(); - - cx.notify(); + })) } fn channel_path_sorting_key<'a>( diff --git a/crates/client/src/channel_store_tests.rs b/crates/client/src/channel_store_tests.rs index 3a3f3842eb..51e819349e 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/client/src/channel_store_tests.rs @@ -139,7 +139,8 @@ fn update_channels( message: proto::UpdateChannels, cx: &mut AppContext, ) { - channel_store.update(cx, |store, cx| store.update_channels(message, cx)); + let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx)); + assert!(task.is_none()); } #[track_caller] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index f9f2d4a2e2..2396085a01 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1156,7 +1156,6 @@ async fn rejoin_room( channel_members = mem::take(&mut rejoined_room.channel_members); } - //TODO: move this into the room guard if let Some(channel_id) = channel_id { channel_updated( channel_id, @@ -2453,9 +2452,6 @@ async fn join_channel( joined_room.clone() }; - // TODO - do this while still holding the room guard, - // currently there's a possible race condition if someone joins the channel - // after we've dropped the lock but before we finish sending these updates channel_updated( channel_id, &joined_room.room, @@ -2748,7 +2744,6 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { return Ok(()); } - // TODO - do this while holding the room guard. if let Some(channel_id) = channel_id { channel_updated( channel_id, diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index f1157ce7ae..d4cf6423f0 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -290,6 +290,7 @@ async fn test_core_channels( ); } +#[track_caller] fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u64]) { assert_eq!( participants.iter().map(|p| p.id).collect::>(), @@ -297,6 +298,7 @@ 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)], diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index e4838df939..eaa3560b9b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2037,8 +2037,6 @@ impl CollabPanel { self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); } - // TODO: Make join into a toggle - // TODO: Make enter work on channel editor fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.remove_channel(channel.id, cx) From e5eed29c72c0029ad1d635f34b76e9edf1213ea3 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 03:06:43 -0700 Subject: [PATCH 213/326] Add components example Re-arrange generics on mouse event handler Add TypeTag struct for dynamically tagged components --- .../src/activity_indicator.rs | 2 +- crates/ai/src/assistant.rs | 18 +- crates/auto_update/src/update_notification.rs | 4 +- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- crates/collab_ui/src/collab_titlebar_item.rs | 30 +- crates/collab_ui/src/contact_list.rs | 20 +- crates/collab_ui/src/contacts_popover.rs | 2 +- .../src/incoming_call_notification.rs | 4 +- crates/collab_ui/src/notifications.rs | 4 +- .../src/project_shared_notification.rs | 4 +- .../collab_ui/src/sharing_status_indicator.rs | 2 +- crates/context_menu/src/context_menu.rs | 4 +- crates/copilot/src/sign_in.rs | 2 +- crates/copilot_button/src/copilot_button.rs | 2 +- crates/diagnostics/src/items.rs | 4 +- crates/drag_and_drop/src/drag_and_drop.rs | 6 +- crates/editor/src/editor.rs | 10 +- crates/editor/src/element.rs | 2 +- crates/editor/src/hover_popover.rs | 4 +- crates/feedback/src/deploy_feedback_button.rs | 2 +- crates/feedback/src/feedback_info_text.rs | 2 +- crates/feedback/src/submit_feedback_button.rs | 2 +- crates/gpui/examples/components.rs | 335 ++++++++++++++++++ crates/gpui/src/app.rs | 38 +- crates/gpui/src/elements.rs | 4 +- crates/gpui/src/elements/container.rs | 9 + .../gpui/src/elements/mouse_event_handler.rs | 63 +++- crates/gpui/src/elements/tooltip.rs | 2 +- crates/gpui/src/fonts.rs | 7 + crates/gpui/src/platform.rs | 11 + crates/gpui/src/scene/mouse_region.rs | 46 +-- crates/gpui/src/views/select.rs | 4 +- .../src/active_buffer_language.rs | 2 +- crates/language_tools/src/lsp_log.rs | 8 +- crates/language_tools/src/syntax_tree_view.rs | 8 +- crates/picker/src/picker.rs | 2 +- crates/project_panel/src/project_panel.rs | 6 +- crates/search/src/buffer_search.rs | 8 +- crates/search/src/project_search.rs | 8 +- crates/theme/src/ui.rs | 14 +- crates/vcs_menu/src/lib.rs | 2 +- crates/workspace/src/dock.rs | 3 +- crates/workspace/src/notifications.rs | 4 +- crates/workspace/src/pane.rs | 10 +- .../src/pane/dragged_item_receiver.rs | 4 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/shared_screen.rs | 2 +- crates/workspace/src/toolbar.rs | 2 +- crates/workspace/src/workspace.rs | 4 +- 49 files changed, 585 insertions(+), 155 deletions(-) create mode 100644 crates/gpui/examples/components.rs diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8b46d7cfc5..6d1db5ada5 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -318,7 +318,7 @@ impl View for ActivityIndicator { on_click, } = self.content_to_render(cx); - let mut element = MouseEventHandler::::new(0, cx, |state, cx| { + let mut element = MouseEventHandler::new::(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar.lsp_status; let style = if state.hovered() && on_click.is_some() { theme.hovered.as_ref().unwrap_or(&theme.default) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 0a18266d2a..bb4e9f6db4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -348,7 +348,7 @@ impl AssistantPanel { enum History {} let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.hamburger_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -380,7 +380,7 @@ impl AssistantPanel { fn render_split_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.split_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -404,7 +404,7 @@ impl AssistantPanel { fn render_assist_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.assist_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -422,7 +422,7 @@ impl AssistantPanel { fn render_quote_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.quote_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -450,7 +450,7 @@ impl AssistantPanel { fn render_plus_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.plus_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -480,7 +480,7 @@ impl AssistantPanel { &theme.assistant.zoom_in_button }; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -506,7 +506,7 @@ impl AssistantPanel { ) -> impl Element { let conversation = &self.saved_conversations[index]; let path = conversation.path.clone(); - MouseEventHandler::::new(index, cx, move |state, cx| { + MouseEventHandler::new::(index, cx, move |state, cx| { let style = &theme::current(cx).assistant.saved_conversation; Flex::row() .with_child( @@ -1818,7 +1818,7 @@ impl ConversationEditor { let theme = theme::current(cx); let style = &theme.assistant; let message_id = message.id; - let sender = MouseEventHandler::::new( + let sender = MouseEventHandler::new::( message_id.0, cx, |state, _| match message.role { @@ -2044,7 +2044,7 @@ impl ConversationEditor { ) -> impl Element { enum Model {} - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let style = style.model.style_for(state); Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) .contained() diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index cd2e53905d..8397fa0745 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -31,7 +31,7 @@ impl View for UpdateNotification { let app_name = cx.global::().display_name(); - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { Flex::column() .with_child( Flex::row() @@ -48,7 +48,7 @@ impl View for UpdateNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 433dbed29b..615e238648 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -82,7 +82,7 @@ impl View for Breadcrumbs { .into_any(); } - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); crumbs.with_style(style.container) }) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index fefb1c608f..bda11796e0 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -226,7 +226,7 @@ impl CollabTitlebarItem { let mut ret = Flex::row().with_child( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |mouse_state, cx| { + MouseEventHandler::new::(0, cx, |mouse_state, cx| { let style = project_style .in_state(self.project_popover.is_some()) .style_for(mouse_state); @@ -266,7 +266,7 @@ impl CollabTitlebarItem { .with_child( Stack::new() .with_child( - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |mouse_state, cx| { @@ -398,7 +398,7 @@ impl CollabTitlebarItem { self.branch_popover.as_ref().map(|child| { let theme = theme::current(cx).clone(); let child = ChildView::new(child, cx); - let child = MouseEventHandler::::new(0, cx, |_, _| { + let child = MouseEventHandler::new::(0, cx, |_, _| { child .flex(1., true) .contained() @@ -433,7 +433,7 @@ impl CollabTitlebarItem { self.project_popover.as_ref().map(|child| { let theme = theme::current(cx).clone(); let child = ChildView::new(child, cx); - let child = MouseEventHandler::::new(0, cx, |_, _| { + let child = MouseEventHandler::new::(0, cx, |_, _| { child .flex(1., true) .contained() @@ -560,7 +560,7 @@ impl CollabTitlebarItem { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_contacts_button .in_state(self.contacts_popover.is_some()) @@ -610,7 +610,7 @@ impl CollabTitlebarItem { let active = room.read(cx).is_screen_sharing(); let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .screen_share_button .in_state(active) @@ -659,7 +659,7 @@ impl CollabTitlebarItem { } let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_microphone_button .in_state(is_muted) @@ -712,7 +712,7 @@ impl CollabTitlebarItem { } let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_speakers_button .in_state(is_deafened) @@ -747,7 +747,7 @@ impl CollabTitlebarItem { let tooltip = "Leave call"; let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar.leave_call_button.style_for(state); Svg::new(icon) .with_color(style.color) @@ -801,7 +801,7 @@ impl CollabTitlebarItem { Some( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { //TODO: Ensure this button has consistent width for both text variations let style = titlebar.share_button.inactive_state().style_for(state); Label::new(label, style.text.clone()) @@ -847,7 +847,7 @@ impl CollabTitlebarItem { let avatar_style = &user_menu_button_style.avatar; Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = user_menu_button_style .user_menu .inactive_state() @@ -907,7 +907,7 @@ impl CollabTitlebarItem { fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar.sign_in_button.inactive_state().style_for(state); Label::new("Sign In", style.text.clone()) .contained() @@ -1142,7 +1142,7 @@ impl CollabTitlebarItem { if let Some(replica_id) = replica_id { enum ToggleFollow {} - content = MouseEventHandler::::new( + content = MouseEventHandler::new::( replica_id.into(), cx, move |_, _| content, @@ -1173,7 +1173,7 @@ impl CollabTitlebarItem { enum JoinProject {} let user_id = user.id; - content = MouseEventHandler::::new( + content = MouseEventHandler::new::( peer_id.as_u64() as usize, cx, move |_, _| content, @@ -1261,7 +1261,7 @@ impl CollabTitlebarItem { .into_any(), ), client::Status::UpgradeRequired => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Label::new( "Please update Zed to collaborate", theme.titlebar.outdated_warning.text.clone(), diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index b8024e2bfd..83f3bd97b2 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -810,7 +810,7 @@ impl ContactList { worktree_root_names.join(", ") }; - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -904,7 +904,7 @@ impl ContactList { let baseline_offset = row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - MouseEventHandler::::new( + MouseEventHandler::new::( peer_id.as_u64() as usize, cx, |mouse_state, _| { @@ -1006,7 +1006,7 @@ impl ContactList { }; let leave_call = if section == Section::ActiveCall { Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.leave_call.style_for(state); Label::new("Leave Call", style.text.clone()) .contained() @@ -1024,7 +1024,7 @@ impl ContactList { }; let icon_size = theme.section_icon_size; - MouseEventHandler::::new(section as usize, cx, |_, _| { + MouseEventHandler::new::(section as usize, cx, |_, _| { Flex::row() .with_child( Svg::new(if is_collapsed { @@ -1075,7 +1075,7 @@ impl ContactList { let github_login = contact.user.github_login.clone(); let initial_project = project.clone(); let mut event_handler = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { + MouseEventHandler::new::(contact.user.id as usize, cx, |_, cx| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { let status_badge = if contact.online { @@ -1114,7 +1114,7 @@ impl ContactList { .flex(1., true), ) .with_child( - MouseEventHandler::::new( + MouseEventHandler::new::( contact.user.id as usize, cx, |mouse_state, _| { @@ -1208,7 +1208,7 @@ impl ContactList { if is_incoming { row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1231,7 +1231,7 @@ impl ContactList { ); row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1254,7 +1254,7 @@ impl ContactList { ); } else { row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1333,7 +1333,7 @@ impl View for ContactList { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { render_icon_button( &theme.contact_list.add_contact_button, "icons/user_plus_16.svg", diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 1d6d1c84c7..39ab9c621c 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -113,7 +113,7 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child, cx), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Flex::column() .with_child(child.flex(1., true)) .contained() diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 6f86a74300..410adbf862 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -173,7 +173,7 @@ impl IncomingCallNotification { let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.incoming_call_notification; Label::new("Accept", theme.accept_button.text.clone()) .aligned() @@ -187,7 +187,7 @@ impl IncomingCallNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.incoming_call_notification; Label::new("Decline", theme.decline_button.text.clone()) .aligned() diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index cbd072fe89..9258ad3ab1 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -52,7 +52,7 @@ where .flex(1., true), ) .with_child( - MouseEventHandler::::new(user.id as usize, cx, |state, _| { + MouseEventHandler::new::(user.id as usize, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -92,7 +92,7 @@ where Flex::row() .with_children(buttons.into_iter().enumerate().map( |(ix, (message, handler))| { - MouseEventHandler::::new(ix, cx, |state, _| { + MouseEventHandler::new::(ix, cx, |state, _| { let button = theme.button.style_for(state); Label::new(message, button.text.clone()) .contained() diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 63922f2b65..500599db59 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -170,7 +170,7 @@ impl ProjectSharedNotification { let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.project_shared_notification; Label::new("Open", theme.open_button.text.clone()) .aligned() @@ -182,7 +182,7 @@ impl ProjectSharedNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.project_shared_notification; Label::new("Dismiss", theme.dismiss_button.text.clone()) .aligned() diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index a39ffc457a..9fcd15aa18 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -47,7 +47,7 @@ impl View for SharingStatusIndicator { Appearance::Dark | Appearance::VibrantDark => Color::white(), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Svg::new("icons/disable_screen_sharing_12.svg") .with_color(color) .constrained() diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index a5534b6262..89df86beef 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -439,14 +439,14 @@ impl ContextMenu { let style = theme::current(cx).context_menu.clone(); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { label, action } => { let action = action.clone(); let view_id = self.parent_view_id; - MouseEventHandler::::new(ix, cx, |state, _| { + MouseEventHandler::new::(ix, cx, |state, _| { let style = style.item.in_state(self.selected_index == Some(ix)); let style = style.style_for(state); let keystroke = match &action { diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index d03a2d393b..ac3b81f0c6 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -113,7 +113,7 @@ impl CopilotCodeVerification { let device_code_style = &style.auth.prompting.device_code; - MouseEventHandler::::new(0, cx, |state, _cx| { + MouseEventHandler::new::(0, cx, |state, _cx| { Flex::row() .with_child( Label::new(data.user_code.clone(), device_code_style.text.clone()) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index eae1746a01..f73f854927 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -62,7 +62,7 @@ impl View for CopilotButton { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, { + MouseEventHandler::new::(0, cx, { let theme = theme.clone(); let status = status.clone(); move |state, _cx| { diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 0ae55e99d9..d1a32c72f1 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -94,7 +94,7 @@ impl View for DiagnosticIndicator { let tooltip_style = theme::current(cx).tooltip.clone(); let in_progress = !self.in_progress_checks.is_empty(); let mut element = Flex::row().with_child( - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let theme = theme::current(cx); let style = theme .workspace @@ -195,7 +195,7 @@ impl View for DiagnosticIndicator { } else if let Some(diagnostic) = &self.current_diagnostic { let message_style = style.diagnostic_message.clone(); element.add_child( - MouseEventHandler::::new(1, cx, |state, _| { + MouseEventHandler::new::(1, cx, |state, _| { Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), message_style.style_for(state).text.clone(), diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index ddfed0c858..59b0bc89e2 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -202,7 +202,7 @@ impl DragAndDrop { let position = (position - region_offset).round(); Some( Overlay::new( - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |_, cx| render(payload, cx), @@ -235,7 +235,7 @@ impl DragAndDrop { } State::Canceled => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Empty::new().constrained().with_width(0.).with_height(0.) }) .on_up(MouseButton::Left, |_, _, cx| { @@ -301,7 +301,7 @@ pub trait Draggable { Self: Sized; } -impl Draggable for MouseEventHandler { +impl Draggable for MouseEventHandler { fn as_draggable( self, payload: P, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d8b77ea95..d9bccdfea3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -867,7 +867,7 @@ impl CompletionsMenu { let completion = &completions[mat.candidate_id]; let item_ix = start_ix + ix; items.push( - MouseEventHandler::::new( + MouseEventHandler::new::( mat.candidate_id, cx, |state, _| { @@ -1044,7 +1044,7 @@ impl CodeActionsMenu { for (ix, action) in actions[range].iter().enumerate() { let item_ix = start_ix + ix; items.push( - MouseEventHandler::::new(item_ix, cx, |state, _| { + MouseEventHandler::new::(item_ix, cx, |state, _| { let item_style = if item_ix == selected_item { style.autocomplete.selected_item } else if state.hovered() { @@ -3547,7 +3547,7 @@ impl Editor { if self.available_code_actions.is_some() { enum CodeActions {} Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { Svg::new("icons/bolt_8.svg").with_color( style .code_actions @@ -3594,7 +3594,7 @@ impl Editor { fold_data .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - MouseEventHandler::::new( + MouseEventHandler::new::( ix as usize, cx, |mouse_state, _| { @@ -8663,7 +8663,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); let anchor_x = cx.anchor_x; enum BlockContextToolip {} - MouseEventHandler::::new(cx.block_id, cx, |_, _| { + MouseEventHandler::new::(cx.block_id, cx, |_, _| { Flex::column() .with_children(highlighted_lines.iter().map(|(line, highlights)| { Label::new( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 13856cc8ef..0e9938cf47 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1637,7 +1637,7 @@ impl EditorElement { let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); enum JumpIcon {} - MouseEventHandler::::new((*id).into(), cx, |state, _| { + MouseEventHandler::new::((*id).into(), cx, |state, _| { let style = style.jump_icon.style_for(state); Svg::new("icons/arrow_up_right_8.svg") .with_color(style.color) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index afc13f983d..e4509a765c 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -565,7 +565,7 @@ impl InfoPopover { ) }); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let mut region_id = 0; let view_id = cx.view_id(); @@ -654,7 +654,7 @@ impl DiagnosticPopover { let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { text.with_soft_wrap(true) .contained() .with_style(container_style) diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index d197f57fa5..ad2a40b60c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -35,7 +35,7 @@ impl View for DeployFeedbackButton { let theme = theme::current(cx).clone(); Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = &theme .workspace .status_bar diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs index 6c55b7a713..91ff22e904 100644 --- a/crates/feedback/src/feedback_info_text.rs +++ b/crates/feedback/src/feedback_info_text.rs @@ -41,7 +41,7 @@ impl View for FeedbackInfoText { .aligned(), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let contained_text = if state.hovered() { &theme.feedback.link_text_hover } else { diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 2133296e25..df59cf143f 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -52,7 +52,7 @@ impl View for SubmitFeedbackButton { .map_or(true, |i| i.read(cx).allow_submission); enum SubmitFeedbackButton {} - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let text; let style = if allow_submission { text = "Submit as Markdown"; diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs new file mode 100644 index 0000000000..50ace5eee0 --- /dev/null +++ b/crates/gpui/examples/components.rs @@ -0,0 +1,335 @@ +use button_component::Button; + +use component::AdaptComponent; +use gpui::{ + color::Color, + elements::{ContainerStyle, Flex, Label, ParentElement}, + fonts::{self, TextStyle}, + platform::WindowOptions, + AnyElement, App, Element, Entity, View, ViewContext, +}; +use log::LevelFilter; +use pathfinder_geometry::vector::vec2f; +use simplelog::SimpleLogger; +use theme::Toggleable; +use toggleable_button::ToggleableButton; + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + App::new(()).unwrap().run(|cx| { + cx.platform().activate(true); + cx.add_window(WindowOptions::with_bounds(vec2f(300., 200.)), |_| { + TestView { + count: 0, + is_doubling: false, + } + }); + }); +} + +pub struct TestView { + count: usize, + is_doubling: bool, +} + +impl TestView { + fn increase_count(&mut self) { + if self.is_doubling { + self.count *= 2; + } else { + self.count += 1; + } + } +} + +impl Entity for TestView { + type Event = (); +} + +type ButtonStyle = ContainerStyle; + +impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + fonts::with_font_cache(cx.font_cache.to_owned(), || { + Flex::column() + .with_child(Label::new( + format!("Count: {}", self.count), + TextStyle::for_color(Color::red()), + )) + .with_child( + Button::new(move |_, v: &mut Self, cx| { + v.increase_count(); + cx.notify(); + }) + .with_text( + "Hello from a counting BUTTON", + TextStyle::for_color(Color::blue()), + ) + .with_style(ButtonStyle::fill(Color::yellow())) + .into_element(), + ) + .with_child( + ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { + v.is_doubling = !v.is_doubling; + cx.notify(); + }) + .with_text("Double the count?", TextStyle::for_color(Color::black())) + .with_style(Toggleable { + inactive: ButtonStyle::fill(Color::red()), + active: ButtonStyle::fill(Color::green()), + }) + .into_element(), + ) + .expanded() + .contained() + .with_background_color(Color::white()) + .into_any() + }) + } +} + +mod theme { + pub struct Toggleable { + pub inactive: T, + pub active: T, + } + + impl Toggleable { + pub fn style_for(&self, active: bool) -> &T { + if active { + &self.active + } else { + &self.inactive + } + } + } +} + +// Component creation: +mod toggleable_button { + use gpui::{ + elements::{ContainerStyle, LabelStyle}, + scene::MouseClick, + EventContext, View, + }; + + use crate::{button_component::Button, component::Component, theme::Toggleable}; + + pub struct ToggleableButton { + active: bool, + style: Option>, + button: Button, + } + + impl ToggleableButton { + pub fn new(active: bool, on_click: F) -> Self + where + F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, + { + Self { + active, + button: Button::new(on_click), + style: None, + } + } + + pub fn with_text(self, text: &str, style: impl Into) -> ToggleableButton { + ToggleableButton { + active: self.active, + style: self.style, + button: self.button.with_text(text, style), + } + } + + pub fn with_style(self, style: Toggleable) -> ToggleableButton { + ToggleableButton { + active: self.active, + style: Some(style), + button: self.button, + } + } + } + + impl Component for ToggleableButton { + type View = V; + + fn render( + self, + v: &mut Self::View, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + let button = if let Some(style) = self.style { + self.button.with_style(*style.style_for(self.active)) + } else { + self.button + }; + button.render(v, cx) + } + } +} + +mod button_component { + + use gpui::{ + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler}, + platform::MouseButton, + scene::MouseClick, + AnyElement, Element, EventContext, TypeTag, View, ViewContext, + }; + + use crate::component::Component; + + type ClickHandler = Box)>; + + pub struct Button { + click_handler: ClickHandler, + tag: TypeTag, + contents: Option>, + style: Option, + } + + impl Button { + pub fn new) + 'static>(handler: F) -> Self { + Self { + click_handler: Box::new(handler), + tag: TypeTag::new::(), + style: None, + contents: None, + } + } + + pub fn with_text(mut self, text: &str, style: impl Into) -> Self { + self.contents = Some(Label::new(text.to_string(), style).into_any()); + self + } + + pub fn _with_contents>(mut self, contents: E) -> Self { + self.contents = Some(contents.into_any()); + self + } + + pub fn with_style(mut self, style: ContainerStyle) -> Self { + self.style = Some(style); + self + } + } + + impl Component for Button { + type View = V; + + fn render(self, _: &mut Self::View, cx: &mut ViewContext) -> AnyElement { + let click_handler = self.click_handler; + + let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| { + self.contents + .unwrap_or_else(|| gpui::elements::Empty::new().into_any()) + }) + .on_click(MouseButton::Left, move |click, v, cx| { + click_handler(click, v, cx); + }) + .contained(); + + let result = if let Some(style) = self.style { + result.with_style(style) + } else { + result + }; + + result.into_any() + } + } +} + +mod component { + + use gpui::{AnyElement, Element, View, ViewContext}; + use pathfinder_geometry::vector::Vector2F; + + // Public API: + pub trait Component { + type View: View; + + fn render( + self, + v: &mut Self::View, + cx: &mut ViewContext, + ) -> AnyElement; + } + + pub struct ComponentAdapter { + component: Option, + } + + impl ComponentAdapter { + pub fn new(e: E) -> Self { + Self { component: Some(e) } + } + } + + pub trait AdaptComponent: Sized { + fn into_element(self) -> ComponentAdapter { + ComponentAdapter::new(self) + } + } + + impl AdaptComponent for C {} + + impl Element for ComponentAdapter { + type LayoutState = AnyElement; + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + view: &mut C::View, + cx: &mut gpui::LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let component = self.component.take().unwrap(); + let mut element = component.render(view, cx.view_context()); + let constraint = element.layout(constraint, view, cx); + (constraint, element) + } + + fn paint( + &mut self, + scene: &mut gpui::SceneBuilder, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + view: &mut C::View, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + } + + fn rect_for_text_range( + &self, + _: std::ops::Range, + _: gpui::geometry::rect::RectF, + _: gpui::geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &C::View, + _: &ViewContext, + ) -> Option { + todo!() + } + + fn debug( + &self, + _: gpui::geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &C::View, + _: &ViewContext, + ) -> serde_json::Value { + todo!() + } + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2a9d9f4768..8e6d43a45d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3280,7 +3280,11 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn mouse_state(&self, region_id: usize) -> MouseState { - let region_id = MouseRegionId::new::(self.view_id, region_id); + self.mouse_state_dynamic(TypeTag::new::(), region_id) + } + + pub fn mouse_state_dynamic(&self, tag: TypeTag, region_id: usize) -> MouseState { + let region_id = MouseRegionId::new(tag, self.view_id, region_id); MouseState { hovered: self.window.hovered_region_ids.contains(®ion_id), clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region { @@ -3321,6 +3325,36 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TypeTag { + tag: TypeId, + #[cfg(debug_assertions)] + tag_type_name: &'static str, +} + +impl TypeTag { + pub fn new() -> Self { + Self { + tag: TypeId::of::(), + #[cfg(debug_assertions)] + tag_type_name: std::any::type_name::(), + } + } + + pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self { + Self { + tag, + #[cfg(debug_assertions)] + tag_type_name: type_name, + } + } + + #[cfg(debug_assertions)] + pub(crate) fn type_name(&self) -> &'static str { + self.tag_type_name + } +} + impl BorrowAppContext for ViewContext<'_, '_, V> { fn read_with T>(&self, f: F) -> T { BorrowAppContext::read_with(&*self.window_context, f) @@ -5171,7 +5205,7 @@ mod tests { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum Handler {} let mouse_down_count = self.mouse_down_count.clone(); - MouseEventHandler::::new(0, cx, |_, _| Empty::new()) + MouseEventHandler::new::(0, cx, |_, _| Empty::new()) .on_down(MouseButton::Left, move |_, _, _| { mouse_down_count.fetch_add(1, SeqCst); }) diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 56a712802b..16c750ea8e 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -193,11 +193,11 @@ pub trait Element: 'static { Resizable::new(self.into_any(), side, size, on_resize) } - fn mouse(self, region_id: usize) -> MouseEventHandler + fn mouse(self, region_id: usize) -> MouseEventHandler where Self: Sized, { - MouseEventHandler::for_child(self.into_any(), region_id) + MouseEventHandler::for_child::(self.into_any(), region_id) } } diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 698100ab29..bb1366b4e7 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -38,6 +38,15 @@ pub struct ContainerStyle { pub cursor: Option, } +impl ContainerStyle { + pub fn fill(color: Color) -> Self { + Self { + background_color: Some(color), + ..Default::default() + } + } +} + pub struct Container { child: AnyElement, style: ContainerStyle, diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 6005277f73..2ed0f1720f 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -11,12 +11,12 @@ use crate::{ MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, }, AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext, - SceneBuilder, SizeConstraint, View, ViewContext, + SceneBuilder, SizeConstraint, TypeTag, View, ViewContext, }; use serde_json::json; -use std::{marker::PhantomData, ops::Range}; +use std::ops::Range; -pub struct MouseEventHandler { +pub struct MouseEventHandler { child: AnyElement, region_id: usize, cursor_style: Option, @@ -26,13 +26,13 @@ pub struct MouseEventHandler { notify_on_click: bool, above: bool, padding: Padding, - _tag: PhantomData, + tag: TypeTag, } /// Element which provides a render_child callback with a MouseState and paints a mouse /// region under (or above) it for easy mouse event handling. -impl MouseEventHandler { - pub fn for_child(child: impl Element, region_id: usize) -> Self { +impl MouseEventHandler { + pub fn for_child(child: impl Element, region_id: usize) -> Self { Self { child: child.into_any(), region_id, @@ -43,16 +43,19 @@ impl MouseEventHandler { hoverable: false, above: false, padding: Default::default(), - _tag: PhantomData, + tag: TypeTag::new::(), } } - pub fn new(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn new( + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> E, + ) -> Self where E: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> E, { - let mut mouse_state = cx.mouse_state::(region_id); + let mut mouse_state = cx.mouse_state_dynamic(TypeTag::new::(), region_id); let child = render_child(&mut mouse_state, cx).into_any(); let notify_on_hover = mouse_state.accessed_hovered(); let notify_on_click = mouse_state.accessed_clicked(); @@ -66,19 +69,46 @@ impl MouseEventHandler { hoverable: true, above: false, padding: Default::default(), - _tag: PhantomData, + tag: TypeTag::new::(), + } + } + + pub fn new_dynamic( + tag: TypeTag, + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> AnyElement, + ) -> Self { + let mut mouse_state = cx.mouse_state_dynamic(tag, region_id); + let child = render_child(&mut mouse_state, cx); + let notify_on_hover = mouse_state.accessed_hovered(); + let notify_on_click = mouse_state.accessed_clicked(); + Self { + child, + region_id, + cursor_style: None, + handlers: Default::default(), + notify_on_hover, + notify_on_click, + hoverable: true, + above: false, + padding: Default::default(), + tag, } } /// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful /// for drag and drop handling and similar events which should be captured before the child /// gets the opportunity - pub fn above(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn above( + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> D, + ) -> Self where D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, { - let mut handler = Self::new(region_id, cx, render_child); + let mut handler = Self::new::(region_id, cx, render_child); handler.above = true; handler } @@ -223,7 +253,8 @@ impl MouseEventHandler { }); } scene.push_mouse_region( - MouseRegion::from_handlers::( + MouseRegion::from_handlers( + self.tag, cx.view_id(), self.region_id, hit_bounds, @@ -236,7 +267,7 @@ impl MouseEventHandler { } } -impl Element for MouseEventHandler { +impl Element for MouseEventHandler { type LayoutState = (); type PaintState = (); diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 14f3809e67..0ba0110303 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -95,7 +95,7 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::, _>::new(id, cx, |_, _| child) + let child = MouseEventHandler::new::, _>(id, cx, |_, _| child) .on_hover(move |e, _, cx| { let position = e.position; if e.started { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index b003042866..8858e1a316 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -72,6 +72,13 @@ pub struct TextStyle { } impl TextStyle { + pub fn for_color(color: Color) -> Self { + Self { + color, + ..Default::default() + } + } + pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle { TextStyle { color: refinement.color.unwrap_or(self.color), diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1d93a45fc7..9f6e303cb7 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -24,6 +24,7 @@ use crate::{ use anyhow::{anyhow, bail, Result}; use async_task::Runnable; pub use event::*; +use pathfinder_geometry::vector::vec2f; use postage::oneshot; use schemars::JsonSchema; use serde::Deserialize; @@ -180,6 +181,16 @@ pub struct WindowOptions<'a> { pub screen: Option>, } +impl<'a> WindowOptions<'a> { + pub fn with_bounds(bounds: Vector2F) -> Self { + Self { + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), bounds)), + center: true, + ..Default::default() + } + } +} + #[derive(Debug)] pub struct TitlebarOptions<'a> { pub title: Option<&'a str>, diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index cf39ac782f..1632b494a3 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -1,13 +1,10 @@ -use crate::{platform::MouseButton, window::WindowContext, EventContext, View, ViewContext}; +use crate::{ + platform::MouseButton, window::WindowContext, EventContext, TypeTag, View, ViewContext, +}; use collections::HashMap; use pathfinder_geometry::rect::RectF; use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - fmt::Debug, - mem::Discriminant, - rc::Rc, -}; +use std::{any::Any, fmt::Debug, mem::Discriminant, rc::Rc}; use super::{ mouse_event::{ @@ -33,14 +30,27 @@ impl MouseRegion { /// should pass a different (consistent) region_id. If you have one big region that covers your /// whole component, just pass the view_id again. pub fn new(view_id: usize, region_id: usize, bounds: RectF) -> Self { - Self::from_handlers::(view_id, region_id, bounds, Default::default()) + Self::from_handlers( + TypeTag::new::(), + view_id, + region_id, + bounds, + Default::default(), + ) } pub fn handle_all(view_id: usize, region_id: usize, bounds: RectF) -> Self { - Self::from_handlers::(view_id, region_id, bounds, HandlerSet::capture_all()) + Self::from_handlers( + TypeTag::new::(), + view_id, + region_id, + bounds, + HandlerSet::capture_all(), + ) } - pub fn from_handlers( + pub fn from_handlers( + tag: TypeTag, view_id: usize, region_id: usize, bounds: RectF, @@ -49,10 +59,8 @@ impl MouseRegion { Self { id: MouseRegionId { view_id, - tag: TypeId::of::(), + tag, region_id, - #[cfg(debug_assertions)] - tag_type_name: std::any::type_name::(), }, bounds, handlers, @@ -180,20 +188,16 @@ impl MouseRegion { #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub struct MouseRegionId { view_id: usize, - tag: TypeId, + tag: TypeTag, region_id: usize, - #[cfg(debug_assertions)] - tag_type_name: &'static str, } impl MouseRegionId { - pub(crate) fn new(view_id: usize, region_id: usize) -> Self { + pub(crate) fn new(tag: TypeTag, view_id: usize, region_id: usize) -> Self { MouseRegionId { view_id, region_id, - tag: TypeId::of::(), - #[cfg(debug_assertions)] - tag_type_name: std::any::type_name::(), + tag, } } @@ -203,7 +207,7 @@ impl MouseRegionId { #[cfg(debug_assertions)] pub fn tag_type_name(&self) -> &'static str { - self.tag_type_name + self.tag.type_name() } } diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index f3be9de3ec..f76fab738e 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -106,7 +106,7 @@ impl View for Select { Default::default() }; let mut result = Flex::column().with_child( - MouseEventHandler::::new(self.handle.id(), cx, |mouse_state, cx| { + MouseEventHandler::new::(self.handle.id(), cx, |mouse_state, cx| { (self.render_item)( self.selected_item_ix, ItemType::Header, @@ -130,7 +130,7 @@ impl View for Select { let selected_item_ix = this.selected_item_ix; range.end = range.end.min(this.item_count); items.extend(range.map(|ix| { - MouseEventHandler::::new(ix, cx, |mouse_state, cx| { + MouseEventHandler::new::(ix, cx, |mouse_state, cx| { (this.render_item)( ix, if ix == selected_item_ix { diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index b97417580f..5ffcb13fba 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -53,7 +53,7 @@ impl View for ActiveBufferLanguage { "Unknown".to_string() }; - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar; let style = theme.active_language.style_for(state); Label::new(active_language_text, style.text.clone()) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 0dc594a13f..cc2bf37d4a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -573,7 +573,7 @@ impl View for LspLogToolbarItemView { .with_children(if self.menu_open { Some( Overlay::new( - MouseEventHandler::::new(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(menu_rows.into_iter().map(|row| { Self::render_language_server_menu_item( @@ -672,7 +672,7 @@ impl LspLogToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} - MouseEventHandler::::new(0, cx, move |state, cx| { + MouseEventHandler::new::(0, cx, move |state, cx| { let label: Cow = current_server .and_then(|row| { let worktree = row.worktree.read(cx); @@ -728,7 +728,7 @@ impl LspLogToolbarItemView { .with_height(theme.toolbar_dropdown_menu.row_height) }) .with_child( - MouseEventHandler::::new(id.0, cx, move |state, _| { + MouseEventHandler::new::(id.0, cx, move |state, _| { let style = theme .toolbar_dropdown_menu .item @@ -746,7 +746,7 @@ impl LspLogToolbarItemView { }), ) .with_child( - MouseEventHandler::::new(id.0, cx, move |state, cx| { + MouseEventHandler::new::(id.0, cx, move |state, cx| { let style = theme .toolbar_dropdown_menu .item diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 3e6727bbf4..60788d034e 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -389,7 +389,7 @@ impl View for SyntaxTreeView { { let layer = layer.clone(); let theme = editor_theme.clone(); - return MouseEventHandler::::new(0, cx, move |state, cx| { + return MouseEventHandler::new::(0, cx, move |state, cx| { let list_hovered = state.hovered(); UniformList::new( self.list_state.clone(), @@ -505,7 +505,7 @@ impl SyntaxTreeToolbarItemView { .with_child(Self::render_header(&theme, &active_layer, cx)) .with_children(self.menu_open.then(|| { Overlay::new( - MouseEventHandler::::new(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| { @@ -564,7 +564,7 @@ impl SyntaxTreeToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} - MouseEventHandler::::new(0, cx, move |state, _| { + MouseEventHandler::new::(0, cx, move |state, _| { let style = theme.toolbar_dropdown_menu.header.style_for(state); Flex::row() .with_child( @@ -596,7 +596,7 @@ impl SyntaxTreeToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ActivateLayer {} - MouseEventHandler::::new(layer_ix, cx, move |state, _| { + MouseEventHandler::new::(layer_ix, cx, move |state, _| { let is_selected = layer.node() == active_layer.node(); let style = theme .toolbar_dropdown_menu diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 6efa33e961..a3b8672f9f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -112,7 +112,7 @@ impl View for Picker { let selected_ix = this.delegate.selected_index(); range.end = cmp::min(range.end, this.delegate.match_count()); items.extend(range.map(move |ix| { - MouseEventHandler::::new(ix, cx, |state, cx| { + MouseEventHandler::new::(ix, cx, |state, cx| { this.delegate.render_match(ix, state, ix == selected_ix, cx) }) // Capture mouse events diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f7582f1764..4acc539263 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1407,7 +1407,7 @@ impl ProjectPanel { let show_editor = details.is_editing && !details.is_processing; - MouseEventHandler::::new(entry_id.to_usize(), cx, |state, cx| { + MouseEventHandler::new::(entry_id.to_usize(), cx, |state, cx| { let mut style = entry_style .in_state(details.is_selected) .style_for(state) @@ -1519,7 +1519,7 @@ impl View for ProjectPanel { if has_worktree { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { UniformList::new( self.list.clone(), self.visible_entries @@ -1563,7 +1563,7 @@ impl View for ProjectPanel { } else { Flex::column() .with_child( - MouseEventHandler::::new(2, cx, { + MouseEventHandler::new::(2, cx, { let button_style = theme.open_project_button.clone(); let context_menu_item_style = theme::current(cx).context_menu.item.clone(); move |state, cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c3b4f5caa6..36c9d3becd 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -416,7 +416,7 @@ impl BufferSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.search_options.contains(option); Some( - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + MouseEventHandler::new::(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -463,7 +463,7 @@ impl BufferSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { + MouseEventHandler::new::(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme.search.option_button.inactive_state().style_for(state); Label::new(icon, style.text.clone()) @@ -497,7 +497,7 @@ impl BufferSearchBar { let action_type_id = 0_usize; enum ActionButton {} - MouseEventHandler::::new(action_type_id, cx, |state, cx| { + MouseEventHandler::new::(action_type_id, cx, |state, cx| { let theme = theme::current(cx); let style = theme.search.action_button.style_for(state); Label::new(icon, style.text.clone()) @@ -527,7 +527,7 @@ impl BufferSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); enum CloseButton {} - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 018ab9cb11..8bc8d7da88 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -328,7 +328,7 @@ impl View for ProjectSearchView { editor.set_placeholder_text(new_placeholder_text, cx); }); - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Label::new(text, theme.search.results_status.clone()) .aligned() .contained() @@ -1103,7 +1103,7 @@ impl ProjectSearchBar { let tooltip_style = theme::current(cx).tooltip.clone(); enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { + MouseEventHandler::new::(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme.search.option_button.inactive_state().style_for(state); Label::new(icon, style.text.clone()) @@ -1134,7 +1134,7 @@ impl ProjectSearchBar { ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.is_option_enabled(option, cx); - MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { + MouseEventHandler::new::(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -1170,7 +1170,7 @@ impl ProjectSearchBar { let region_id = 3; - MouseEventHandler::::new(region_id, cx, |state, cx| { + MouseEventHandler::new::(region_id, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index a16c3cb21e..81663ed6ca 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -34,7 +34,7 @@ pub fn checkbox( id: usize, cx: &mut ViewContext, change: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, V: View, @@ -43,7 +43,7 @@ where let label = Label::new(label, style.label.text.clone()) .contained() .with_style(style.label.container); - checkbox_with_label(label, style, checked, id, cx, change) + checkbox_with_label::(label, style, checked, id, cx, change) } pub fn checkbox_with_label( @@ -53,14 +53,14 @@ pub fn checkbox_with_label( id: usize, cx: &mut ViewContext, change: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, D: Element, V: View, F: 'static + Fn(&mut V, bool, &mut EventContext), { - MouseEventHandler::new(id, cx, |state, _| { + MouseEventHandler::new::(id, cx, |state, _| { let indicator = if checked { svg(&style.icon) } else { @@ -143,14 +143,14 @@ pub fn cta_button( style: &ButtonStyle, cx: &mut ViewContext, f: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, L: Into>, V: View, F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, { - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Label::new(label, style.text.to_owned()) .aligned() @@ -205,7 +205,7 @@ where )) .with_child( // FIXME: Get a better tag type - MouseEventHandler::::new(999999, cx, |state, _cx| { + MouseEventHandler::new::(999999, cx, |state, _cx| { let style = style.close_icon.style_for(state); icon(style) }) diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 384b622469..9009c4e3d3 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -295,7 +295,7 @@ impl PickerDelegate for BranchListDelegate { let style = theme.picker.footer.clone(); enum BranchCreateButton {} Some( - Flex::row().with_child(MouseEventHandler::::new(0, cx, |state, _| { + Flex::row().with_child(MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Label::new("Create branch", style.label.clone()) .contained() diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index e33c0a5391..641eae081e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -497,9 +497,8 @@ impl View for PanelButtons { }; Stack::new() .with_child( - MouseEventHandler::::new(panel_ix, cx, |state, cx| { + MouseEventHandler::new::(panel_ix, cx, |state, cx| { let style = button_style.in_state(is_active); - let style = style.style_for(state); Flex::row() .with_child( diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 09cfb4d5d8..55b44e9673 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -290,7 +290,7 @@ pub mod simple_message_notification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -319,7 +319,7 @@ pub mod simple_message_notification { .with_children({ click_message .map(|click_message| { - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |state, _| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 06f13cd52d..4aca96374a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1211,7 +1211,7 @@ impl Pane { enum Tab {} let mouse_event_handler = - MouseEventHandler::::new(ix, cx, |_, cx| { + MouseEventHandler::new::(ix, cx, |_, cx| { Self::render_tab( &item, pane.clone(), @@ -1420,7 +1420,7 @@ impl Pane { let item_id = item.id(); enum TabCloseButton {} let icon = Svg::new("icons/x_mark_8.svg"); - MouseEventHandler::::new(item_id, cx, |mouse_state, _| { + MouseEventHandler::new::(item_id, cx, |mouse_state, _| { if mouse_state.hovered() { icon.with_color(tab_style.icon_close_active) } else { @@ -1485,7 +1485,7 @@ impl Pane { ) -> AnyElement { enum TabBarButton {} - let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { + let mut button = MouseEventHandler::new::(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; let style = theme.pane_button.in_state(is_active).style_for(mouse_state); Svg::new(icon) @@ -1547,7 +1547,7 @@ impl View for Pane { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum MouseNavigationHandler {} - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let active_item_index = self.active_item_index; if let Some(active_item) = self.active_item() { @@ -1559,7 +1559,7 @@ impl View for Pane { enum TabBarEventHandler {} stack.add_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Empty::new() .contained() .with_style(theme.workspace.tab_bar.container) diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 165537a1af..1cbf3e4f50 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -19,7 +19,7 @@ pub fn dragged_item_receiver( split_margin: Option, cx: &mut ViewContext, render_child: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, D: Element, @@ -39,7 +39,7 @@ where None }; - let mut handler = MouseEventHandler::::above(region_id, cx, |state, cx| { + let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { // Observing hovered will cause a render when the mouse enters regardless // of if mouse position was accessed before let drag_position = if state.hovered() { drag_position } else { None }; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index dfda5092ca..7528fb7468 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -212,7 +212,7 @@ impl Member { let leader_user_id = leader.user.id; let app_state = Arc::downgrade(app_state); Some( - MouseEventHandler::::new( + MouseEventHandler::new::( pane.id(), cx, |_, _| { diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 977e167f60..aea03df5e0 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -72,7 +72,7 @@ impl View for SharedScreen { enum Focus {} let frame = self.frame.clone(); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Canvas::new(move |scene, bounds, _, _, _| { if let Some(frame) = frame.clone() { let size = constrain_size_preserving_aspect_ratio( diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 945ac7b0f5..0516f3a145 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -223,7 +223,7 @@ fn nav_button action_name: &'static str, cx: &mut ViewContext, ) -> AnyElement { - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = if enabled { style.style_for(state) } else { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ab4f7286dc..a449c58de3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2560,7 +2560,7 @@ impl Workspace { }; enum TitleBar {} - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Stack::new() .with_children( self.titlebar_item @@ -2649,7 +2649,7 @@ impl Workspace { if self.project.read(cx).is_read_only() { enum DisconnectedOverlay {} Some( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let theme = &theme::current(cx); Label::new( "Your connection to the remote project has been lost.", From 695e6d2f2e547c50f534e062cb57bf8dd7e16b73 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 15 Aug 2023 15:05:39 +0100 Subject: [PATCH 214/326] fix spacing on editor magnifying glass icon --- styles/src/style_tree/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 76452ddec3..11c69490a4 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -197,7 +197,7 @@ export default function search(): any { }, container: { margin: { right: 6 }, - padding: { left: 4 } + padding: { left: 2, right: 2 }, } }, mode_button: toggleable({ From 1cc0798aea36f0587ac93bf96857d495501a3761 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 15 Aug 2023 08:48:01 -0600 Subject: [PATCH 215/326] Add a VisualBlock mode Instead of trying to extend the Mode::Visual special case, just split out into three different modes. --- crates/vim/src/mode_indicator.rs | 5 +- crates/vim/src/motion.rs | 2 +- crates/vim/src/normal/case.rs | 4 +- crates/vim/src/normal/substitute.rs | 4 +- crates/vim/src/object.rs | 2 +- crates/vim/src/state.rs | 12 ++- crates/vim/src/test.rs | 2 +- .../src/test/neovim_backed_test_context.rs | 2 +- crates/vim/src/test/neovim_connection.rs | 7 +- crates/vim/src/vim.rs | 8 +- crates/vim/src/visual.rs | 18 ++-- .../test_enter_visual_line_mode.json | 6 +- .../vim/test_data/test_enter_visual_mode.json | 10 +- ...ltiline_surrounding_character_objects.json | 4 +- crates/vim/test_data/test_visual_delete.json | 2 +- .../test_data/test_visual_word_object.json | 96 +++++++++---------- 16 files changed, 94 insertions(+), 90 deletions(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 48cae9f4ae..79a3bbd051 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -86,8 +86,9 @@ impl View for ModeIndicator { let text = match mode { Mode::Normal => "-- NORMAL --", Mode::Insert => "-- INSERT --", - Mode::Visual { line: false } => "-- VISUAL --", - Mode::Visual { line: true } => "VISUAL LINE", + Mode::Visual => "-- VISUAL --", + Mode::VisualLine => "VISUAL LINE", + Mode::VisualBlock => "VISUAL BLOCK", }; Label::new(text, theme.vim_mode_indicator.text.clone()) .contained() diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index acf9d46ad3..e04457d65c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -149,7 +149,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { let operator = Vim::read(cx).active_operator(); match Vim::read(cx).state.mode { Mode::Normal => normal_motion(motion, operator, times, cx), - Mode::Visual { .. } => visual_motion(motion, times, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx), Mode::Insert => { // Shouldn't execute a motion in insert mode. Ignoring } diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index b3e101262d..0ec0eeba84 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -14,14 +14,14 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext(cx) { match vim.state.mode { - Mode::Visual { line: true } => { + Mode::VisualLine => { let start = Point::new(selection.start.row, 0); let end = Point::new(selection.end.row, snapshot.line_len(selection.end.row)); ranges.push(start..end); cursor_positions.push(start..start); } - Mode::Visual { line: false } => { + Mode::Visual | Mode::VisualBlock => { ranges.push(selection.start..selection.end); cursor_positions.push(selection.start..selection.start); } diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index d2429433fe..cf4e5a63d6 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,7 +4,7 @@ use language::Point; use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { - let line_mode = vim.state.mode == Mode::Visual { line: true }; + let line_mode = vim.state.mode == Mode::VisualLine; vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -52,7 +52,7 @@ mod test { cx.assert_editor_state("xˇbc\n"); // supports a selection - cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false }); + cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual); cx.assert_editor_state("a«bcˇ»\n"); cx.simulate_keystrokes(["s", "x"]); cx.assert_editor_state("axˇ\n"); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 85e6eab692..e1e21e4e3b 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -64,7 +64,7 @@ pub fn init(cx: &mut AppContext) { fn object(object: Object, cx: &mut WindowContext) { match Vim::read(cx).state.mode { Mode::Normal => normal_object(object, cx), - Mode::Visual { .. } => visual_object(object, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx), Mode::Insert => { // Shouldn't execute a text object in insert mode. Ignoring } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 905bd5fd2a..b38dac4aa8 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -9,14 +9,16 @@ use crate::motion::Motion; pub enum Mode { Normal, Insert, - Visual { line: bool }, + Visual, + VisualLine, + VisualBlock, } impl Mode { pub fn is_visual(&self) -> bool { match self { Mode::Normal | Mode::Insert => false, - Mode::Visual { .. } => true, + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true, } } } @@ -74,7 +76,7 @@ impl VimState { CursorShape::Underscore } } - Mode::Visual { .. } => CursorShape::Block, + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -89,7 +91,7 @@ impl VimState { pub fn clip_at_line_ends(&self) -> bool { match self.mode { - Mode::Insert | Mode::Visual { .. } => false, + Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => false, Mode::Normal => true, } } @@ -101,7 +103,7 @@ impl VimState { "vim_mode", match self.mode { Mode::Normal => "normal", - Mode::Visual { .. } => "visual", + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", Mode::Insert => "insert", }, ); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index eb2e6e3a5f..772d7a2033 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -241,7 +241,7 @@ async fn test_status_indicator( deterministic.run_until_parked(); assert_eq!( cx.workspace(|_, cx| mode_indicator.read(cx).mode), - Some(Mode::Visual { line: false }) + Some(Mode::Visual) ); // hides if vim mode is disabled diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 023ed880d2..1c7559e440 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -116,7 +116,7 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle { let mode = if marked_text.contains("»") { - Mode::Visual { line: false } + Mode::Visual } else { Mode::Normal }; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index dd9be10723..e983d5ceec 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -261,8 +261,9 @@ impl NeovimConnection { let mode = match nvim_mode_text.as_ref() { "i" => Some(Mode::Insert), "n" => Some(Mode::Normal), - "v" => Some(Mode::Visual { line: false }), - "V" => Some(Mode::Visual { line: true }), + "v" => Some(Mode::Visual), + "V" => Some(Mode::VisualLine), + "CTRL-V" => Some(Mode::VisualBlock), _ => None, }; @@ -270,7 +271,7 @@ impl NeovimConnection { // Zed uses the index of the positions between the characters, so we need // to add one to the end in visual mode. match mode { - Some(Mode::Visual { .. }) => { + Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => { if selection_col > cursor_col { let selection_line_length = self.read_position("echo strlen(getline(line('v')))").await; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e8d69d696c..038e47659d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -164,7 +164,7 @@ impl Vim { let newest_selection_empty = editor.selections.newest::(cx).is_empty(); if editor_mode == EditorMode::Full && !newest_selection_empty { - self.switch_mode(Mode::Visual { line: false }, true, cx); + self.switch_mode(Mode::Visual, true, cx); } } @@ -270,7 +270,7 @@ impl Vim { } Some(Operator::Replace) => match Vim::read(cx).state.mode { Mode::Normal => normal_replace(text, cx), - Mode::Visual { .. } => visual_replace(text, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, _ => {} @@ -317,7 +317,7 @@ impl Vim { editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); - editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); + editor.selections.line_mode = matches!(state.mode, Mode::VisualLine); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer, cx); } else { @@ -368,7 +368,7 @@ impl Setting for VimModeSetting { fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { - vim.switch_mode(Mode::Visual { line: false }, false, cx) + vim.switch_mode(Mode::Visual, false, cx) } }) } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1716e2d1a5..e5f9d8c459 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -138,10 +138,10 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| match vim.state.mode { - Mode::Normal | Mode::Insert | Mode::Visual { line: true } => { - vim.switch_mode(Mode::Visual { line: false }, false, cx); + Mode::Normal | Mode::Insert | Mode::VisualLine | Mode::VisualBlock => { + vim.switch_mode(Mode::Visual, false, cx); } - Mode::Visual { line: false } => { + Mode::Visual => { vim.switch_mode(Mode::Normal, false, cx); } }) @@ -153,10 +153,10 @@ pub fn toggle_visual_line( cx: &mut ViewContext, ) { Vim::update(cx, |vim, cx| match vim.state.mode { - Mode::Normal | Mode::Insert | Mode::Visual { line: false } => { - vim.switch_mode(Mode::Visual { line: true }, false, cx); + Mode::Normal | Mode::Insert | Mode::Visual | Mode::VisualBlock => { + vim.switch_mode(Mode::VisualLine, false, cx); } - Mode::Visual { line: true } => { + Mode::VisualLine => { vim.switch_mode(Mode::Normal, false, cx); } }) @@ -701,7 +701,7 @@ mod test { The quick brown fox «jumpsˇ» over the lazy dog"}, - Mode::Visual { line: false }, + Mode::Visual, ); cx.simulate_keystroke("y"); cx.set_state( @@ -725,7 +725,7 @@ mod test { The quick brown fox ju«mˇ»ps over the lazy dog"}, - Mode::Visual { line: true }, + Mode::VisualLine, ); cx.simulate_keystroke("d"); cx.assert_state( @@ -738,7 +738,7 @@ mod test { indoc! {" The quick brown the «lazyˇ» dog"}, - Mode::Visual { line: false }, + Mode::Visual, ); cx.simulate_keystroke("p"); cx.assert_state( diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json index 6769145412..bf14ae2495 100644 --- a/crates/vim/test_data/test_enter_visual_line_mode.json +++ b/crates/vim/test_data/test_enter_visual_line_mode.json @@ -1,15 +1,15 @@ {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} {"Key":"shift-v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualLine"}} {"Key":"x"} {"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}} {"Put":{"state":"a\nˇ\nb"}} {"Key":"shift-v"} -{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}} +{"Get":{"state":"a\n«\nˇ»b","mode":"VisualLine"}} {"Key":"x"} {"Get":{"state":"a\nˇb","mode":"Normal"}} {"Put":{"state":"a\nb\nˇ"}} {"Key":"shift-v"} -{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}} +{"Get":{"state":"a\nb\nˇ","mode":"VisualLine"}} {"Key":"x"} {"Get":{"state":"a\nˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json index 4fdb4c7667..090e35cc5d 100644 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ b/crates/vim/test_data/test_enter_visual_mode.json @@ -1,20 +1,20 @@ {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} {"Key":"v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}} {"Key":"w"} {"Key":"j"} -{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":"Visual"}} {"Key":"escape"} {"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} {"Key":"v"} {"Key":"k"} {"Key":"b"} -{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":"Visual"}} {"Put":{"state":"a\nˇ\nb\n"}} {"Key":"v"} -{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"a\n«\nˇ»b\n","mode":"Visual"}} {"Key":"v"} {"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}} {"Put":{"state":"a\nb\nˇ"}} {"Key":"v"} -{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"a\nb\nˇ","mode":"Visual"}} diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json index f683c0a314..cff3ab80e2 100644 --- a/crates/vim/test_data/test_multiline_surrounding_character_objects.json +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -2,9 +2,9 @@ {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":"Visual"}} {"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_delete.json b/crates/vim/test_data/test_visual_delete.json index df025f48a0..d9f8055600 100644 --- a/crates/vim/test_data/test_visual_delete.json +++ b/crates/vim/test_data/test_visual_delete.json @@ -1,7 +1,7 @@ {"Put":{"state":"The quick ˇbrown"}} {"Key":"v"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ»","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ»","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown"}} {"Key":"v"} {"Key":"w"} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index b1c43bf9a2..0041baf969 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -1,236 +1,236 @@ {"Put":{"state":"The quick ˇbrown\nfox"}} {"Key":"v"} -{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «bˇ»rown\nfox","mode":"Visual"}} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ»\nfox","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} From 9e99b74fceea361b2dfc63b91f92133f33b1b564 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 10:45:03 -0700 Subject: [PATCH 216/326] Add the channel name into the current call --- crates/collab_ui/src/collab_panel.rs | 40 +++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 2cba729111..665779fb98 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -34,9 +34,9 @@ use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use staff_mode::StaffMode; -use std::{mem, sync::Arc}; +use std::{borrow::Cow, mem, sync::Arc}; use theme::IconButton; -use util::{ResultExt, TryFutureExt}; +use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::ItemHandle, @@ -1181,13 +1181,35 @@ impl CollabPanel { let tooltip_style = &theme.tooltip; let text = match section { - Section::ActiveCall => "Current Call", - Section::ContactRequests => "Requests", - Section::Contacts => "Contacts", - Section::Channels => "Channels", - Section::ChannelInvites => "Invites", - Section::Online => "Online", - Section::Offline => "Offline", + Section::ActiveCall => { + let channel_name = iife!({ + let channel_id = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + let name = self + .channel_store + .read(cx) + .channel_for_id(channel_id)? + .name + .as_str(); + + Some(name) + }); + + if let Some(name) = channel_name { + Cow::Owned(format!("Current Call - #{}", name)) + } else { + Cow::Borrowed("Current Call") + } + } + Section::ContactRequests => Cow::Borrowed("Requests"), + Section::Contacts => Cow::Borrowed("Contacts"), + Section::Channels => Cow::Borrowed("Channels"), + Section::ChannelInvites => Cow::Borrowed("Invites"), + Section::Online => Cow::Borrowed("Online"), + Section::Offline => Cow::Borrowed("Offline"), }; enum AddContact {} From e36dfa09462029e800b1ec469c241140ce071b90 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 10:53:30 -0700 Subject: [PATCH 217/326] Add active styling --- crates/collab_ui/src/collab_panel.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 665779fb98..0665ecf75b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1535,6 +1535,15 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let is_active = iife!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + Some(call_channel == channel_id) + }) + .unwrap_or(false); const FACEPILE_LIMIT: usize = 4; @@ -1591,7 +1600,7 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style(*theme.contact_row.style_for(is_selected, state)) + .with_style(*theme.contact_row.style_for(is_selected || is_active, state)) .with_padding_left( theme.contact_row.default_style().padding.left + theme.channel_indent * depth as f32, From d95b036fde3e3b35ba6c52b7f97fdb7719b30a28 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 10:58:24 -0700 Subject: [PATCH 218/326] Fix cursor style co-authored-by: Nate --- crates/collab_ui/src/collab_panel.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0665ecf75b..2b79f5a125 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1283,7 +1283,7 @@ impl CollabPanel { let can_collapse = depth > 0; let icon_size = (&theme.collab_panel).section_icon_size; - MouseEventHandler::new::(section as usize, cx, |state, _| { + let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { let header_style = if can_collapse { theme .collab_panel @@ -1328,14 +1328,19 @@ impl CollabPanel { .with_height(theme.collab_panel.row_height) .contained() .with_style(header_style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if can_collapse { - this.toggle_expanded(section, cx); - } - }) - .into_any() + }); + + if can_collapse { + result = result + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if can_collapse { + this.toggle_expanded(section, cx); + } + }) + } + + result.into_any() } fn render_contact( @@ -1612,6 +1617,7 @@ impl CollabPanel { .on_click(MouseButton::Right, move |e, this, cx| { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) + .with_cursor_style(CursorStyle::PointingHand) .into_any() } From d05e8852d300d54505cac0383759e0422f3a67e0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 11:02:18 -0700 Subject: [PATCH 219/326] Add dismiss on escape --- crates/collab_ui/src/collab_panel/channel_modal.rs | 5 +++++ crates/collab_ui/src/collab_panel/contact_finder.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 48d3a7a0ec..3e4f274f23 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -27,6 +27,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ChannelModal::toggle_mode); cx.add_action(ChannelModal::toggle_member_admin); cx.add_action(ChannelModal::remove_member); + cx.add_action(ChannelModal::dismiss); } pub struct ChannelModal { @@ -131,6 +132,10 @@ impl ChannelModal { picker.delegate_mut().remove_selected_member(cx); }); } + + fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(PickerEvent::Dismiss); + } } impl Entity for ChannelModal { diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 4cc7034f49..539e041ae7 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -9,6 +9,7 @@ use workspace::Modal; pub fn init(cx: &mut AppContext) { Picker::::init(cx); + cx.add_action(ContactFinder::dismiss) } pub struct ContactFinder { @@ -43,6 +44,10 @@ impl ContactFinder { picker.set_query(query, cx); }); } + + fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(PickerEvent::Dismiss); + } } impl Entity for ContactFinder { From 1e8a7c7caa0928011d7ce036db913327a6f9f7df Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 15 Aug 2023 20:04:14 +0100 Subject: [PATCH 220/326] refactor buffer_search to reduce redundancy and simplying height management Co-authored-by: maxbrunsfeld --- crates/search/src/buffer_search.rs | 123 ++++++++++------------------ crates/search/src/project_search.rs | 6 +- crates/search/src/search_bar.rs | 8 +- styles/src/style_tree/search.ts | 31 +++---- 4 files changed, 63 insertions(+), 105 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 702bafe975..c634a71297 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -221,38 +221,23 @@ impl View for BufferSearchBar { }; let icon_style = theme.search.editor_icon.clone(); - let nav_column = Flex::column() - .with_child( - Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .aligned(), - ) - .with_children(match_count) - .aligned() - .left() - .top() - .flex(1., true) - .constrained() - .with_max_height(theme.search.search_bar_row_height), - ) - .contained(); + let side_column_min_width = 165.; + let button_height = 32.; + let nav_column = Flex::row() + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .with_children(match_count) + .constrained() + .with_height(theme.search.search_bar_row_height) + .with_min_width(side_column_min_width); let query = Flex::row() .with_child( Svg::for_style(icon_style.icon) .contained() - .with_style(icon_style.container) - .constrained(), - ) - .with_child( - ChildView::new(&self.query_editor, cx) - .constrained() - .flex(1., true), + .with_style(icon_style.container), ) + .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( Flex::row() .with_children(render_search_option( @@ -271,76 +256,53 @@ impl View for BufferSearchBar { .contained(), ) .align_children_center() - .aligned() - .left() .flex(1., true); let row_spacing = theme.workspace.toolbar.container.padding.bottom; - let editor_column = Flex::column() - .align_children_center() + let editor_column = Flex::row() .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child(query) - .contained() - .with_style(query_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_max_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(self.render_action_button("Select All", cx)) - .aligned(), - ) + query .contained() - .with_margin_bottom(row_spacing) + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) .flex(1., false), ) + .with_child(self.render_action_button("Select All", cx)) .contained() + .constrained() + .with_height(theme.search.search_bar_row_height) .aligned() .top() .flex(1., false); - let mode_column = Flex::column().with_child( - Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .aligned() - .left() - .contained() - .with_style(theme.search.modes_container), - ) - .with_child( - super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - ) - .aligned() - .right(), - ) - .constrained() - .with_height(theme.search.search_bar_row_height) - .aligned() - .right() - .top() - .flex(1., true), - ); + let mode_column = Flex::row() + .with_child( + Flex::row() + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) + .contained() + .with_style(theme.search.modes_container), + ) + .with_child(super::search_bar::render_close_button( + "Dismiss Buffer Search", + &theme.search, + cx, + |_, this, cx| this.dismiss(&Default::default(), cx), + Some(Box::new(Dismiss)), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .aligned() + .right() + .constrained() + .with_min_width(side_column_min_width) + .flex_float(); Flex::row() .with_child(nav_column) .with_child(editor_column) .with_child(mode_column) .contained() .with_style(theme.search.container) - .flex_float() .into_any_named("search bar") } } @@ -536,6 +498,7 @@ impl BufferSearchBar { let theme = theme::current(cx); let style = theme.search.action_button.style_for(state); Label::new(icon, style.text.clone()) + .aligned() .contained() .with_style(style.container) }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f216efb46b..338e52eedd 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1556,17 +1556,13 @@ impl View for ProjectSearchBar { Flex::row() .align_children_center() .with_child( - Flex::row() + Flex::row().align_children_center() .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .aligned(), ) .with_children(matches) .aligned() - .top() - .left() - .constrained() - .with_height(theme.search.search_bar_row_height), ) .flex(1., true); let editor_column = Flex::column() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 088a96b279..47892c0ca6 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -77,7 +77,7 @@ pub(super) fn render_nav_button( .style_for(state) .clone(); let mut container_style = style.container.clone(); - let label = Label::new(icon, style.label.clone()).contained(); + let label = Label::new(icon, style.label.clone()).aligned().contained(); container_style.corner_radii = match direction { Direction::Prev => CornerRadii { bottom_right: 0., @@ -128,7 +128,9 @@ pub(crate) fn render_search_mode_button( style.container.border.left = mode.border_left(); style.container.border.right = mode.border_right(); - let label = Label::new(mode.label(), style.text.clone()).contained(); + let label = Label::new(mode.label(), style.text.clone()) + .aligned() + .contained(); let mut container_style = style.container.clone(); if let Some(button_side) = mode.button_side() { if button_side == Side::Left { @@ -150,6 +152,8 @@ pub(crate) fn render_search_mode_button( container_style.corner_radii = CornerRadii::default(); label.with_style(container_style) } + .constrained() + .with_height(theme.search.search_bar_row_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 11c69490a4..24d0930863 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -17,7 +17,7 @@ export default function search(): any { text: text(theme.highest, "mono", "default"), border: border(theme.highest), margin: { - right: 12, + right: 9, }, padding: { top: 4, @@ -51,10 +51,8 @@ export default function search(): any { width: 1., color: background(theme.highest, "on") }, padding: { - bottom: 4, left: 4, right: 4, - top: 4, }, }, state: { @@ -97,14 +95,11 @@ export default function search(): any { background: background(theme.highest, "on"), corner_radius: 6, border: border(theme.highest, "on"), - margin: { - right: 4, - }, padding: { - bottom: 2, + // bottom: 2, left: 10, right: 10, - top: 2, + // top: 2, }, }, state: { @@ -140,8 +135,8 @@ export default function search(): any { padding: { left: 12, right: 12, - top: 3, - bottom: 3, + // top: 3, + // bottom: 3, }, }, include_exclude_inputs: { @@ -165,8 +160,8 @@ export default function search(): any { button_width: 32, corner_radius: 6, padding: { - top: 10, - bottom: 10, + // // top: 10, + // bottom: 10, left: 10, right: 10, }, @@ -213,10 +208,10 @@ export default function search(): any { }, padding: { - bottom: 4, + // bottom: 4, left: 10, right: 10, - top: 5, + // top: 5, }, corner_radius: 6, }, @@ -264,10 +259,10 @@ export default function search(): any { }, padding: { - bottom: 3, + bottom: 0, left: 10, right: 10, - top: 3, + top: 0, }, }, state: { @@ -286,10 +281,10 @@ export default function search(): any { }, padding: { - bottom: 3, + bottom: 0, left: 10, right: 10, - top: 3, + top: 0, }, }, state: { From d13cedb248f7f3e5d577638714b70a714f940960 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 12:12:30 -0700 Subject: [PATCH 221/326] seperate out channel styles in theme --- crates/collab_ui/src/collab_panel.rs | 12 ++++---- crates/theme/src/theme.rs | 3 ++ styles/src/style_tree/collab_panel.ts | 43 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 2b79f5a125..3f303da2af 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1550,7 +1550,7 @@ impl CollabPanel { }) .unwrap_or(false); - const FACEPILE_LIMIT: usize = 4; + const FACEPILE_LIMIT: usize = 3; MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { Flex::row() @@ -1563,9 +1563,9 @@ impl CollabPanel { .left(), ) .with_child( - Label::new(channel.name.clone(), theme.contact_username.text.clone()) + Label::new(channel.name.clone(), theme.channel_name.text.clone()) .contained() - .with_style(theme.contact_username.container) + .with_style(theme.channel_name.container) .aligned() .left() .flex(1., true), @@ -1583,7 +1583,7 @@ impl CollabPanel { .filter_map(|user| { Some( Image::from_data(user.avatar.clone()?) - .with_style(theme.contact_avatar), + .with_style(theme.channel_avatar), ) }) .take(FACEPILE_LIMIT), @@ -1605,9 +1605,9 @@ impl CollabPanel { .constrained() .with_height(theme.row_height) .contained() - .with_style(*theme.contact_row.style_for(is_selected || is_active, state)) + .with_style(*theme.channel_row.style_for(is_selected || is_active, state)) .with_padding_left( - theme.contact_row.default_style().padding.left + theme.channel_row.default_style().padding.left + theme.channel_indent * depth as f32, ) }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e081b70047..912ca0e8b8 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -237,10 +237,13 @@ pub struct CollabPanel { pub subheader_row: Toggleable>, pub leave_call: Interactive, pub contact_row: Toggleable>, + pub channel_row: Toggleable>, + pub channel_name: ContainedText, pub row_height: f32, pub project_row: Toggleable>, pub tree_branch: Toggleable>, pub contact_avatar: ImageStyle, + pub channel_avatar: ImageStyle, pub extra_participant_label: ContainedText, pub contact_status_free: ContainerStyle, pub contact_status_busy: ContainerStyle, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 1d1e09075e..c65887e17c 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -118,6 +118,38 @@ export default function contacts_panel(): any { }, } + const item_row = toggleable({ + base: interactive({ + base: { + padding: { + left: SPACING, + right: SPACING, + }, + }, + state: { + clicked: { + background: background(layer, "pressed"), + }, + }, + }), + state: { + inactive: { + hovered: { + background: background(layer, "hovered"), + }, + }, + active: { + default: { + ...text(layer, "ui_sans", "active", { size: "sm" }), + background: background(layer, "active"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }, + }) + return { ...collab_modals(), log_in_button: text_button(), @@ -198,6 +230,13 @@ export default function contacts_panel(): any { }, }, }), + channel_row: item_row, + channel_name: { + ...text(layer, "ui_sans", { size: "sm" }), + margin: { + left: NAME_MARGIN, + }, + }, list_empty_label_container: { margin: { left: NAME_MARGIN, @@ -245,6 +284,10 @@ export default function contacts_panel(): any { corner_radius: 10, width: 20, }, + channel_avatar: { + corner_radius: 10, + width: 20, + }, extra_participant_label: { corner_radius: 10, padding: { From 9d60e550bed8b6b20cbe57943f81d814b5272149 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 15 Aug 2023 15:32:14 -0400 Subject: [PATCH 222/326] Additional status bar styles --- assets/icons/check.svg | 3 +++ assets/icons/check_circle.svg | 4 ++++ crates/diagnostics/src/items.rs | 6 +++--- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 assets/icons/check.svg create mode 100644 assets/icons/check_circle.svg diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 0000000000..77b180892c --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg new file mode 100644 index 0000000000..85ba2e1f37 --- /dev/null +++ b/assets/icons/check_circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index d1a32c72f1..89b4469d42 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -105,7 +105,7 @@ impl View for DiagnosticIndicator { let mut summary_row = Flex::row(); if self.summary.error_count > 0 { summary_row.add_child( - Svg::new("icons/circle_x_mark_16.svg") + Svg::new("icons/error.svg") .with_color(style.icon_color_error) .constrained() .with_width(style.icon_width) @@ -121,7 +121,7 @@ impl View for DiagnosticIndicator { if self.summary.warning_count > 0 { summary_row.add_child( - Svg::new("icons/triangle_exclamation_16.svg") + Svg::new("icons/warning.svg") .with_color(style.icon_color_warning) .constrained() .with_width(style.icon_width) @@ -142,7 +142,7 @@ impl View for DiagnosticIndicator { if self.summary.error_count == 0 && self.summary.warning_count == 0 { summary_row.add_child( - Svg::new("icons/circle_check_16.svg") + Svg::new("icons/check_circle.svg") .with_color(style.icon_color_ok) .constrained() .with_width(style.icon_width) From 46928fa871aae366ecff130cf12fd6867e66c5ca Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Aug 2023 13:08:44 -0700 Subject: [PATCH 223/326] Reword channel-creation tooltips --- crates/collab_ui/src/collab_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 3f303da2af..7ad7a8883d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1272,7 +1272,7 @@ impl CollabPanel { .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) .with_tooltip::( 0, - "Add or join a channel", + "Create a channel", None, tooltip_style.clone(), cx, @@ -1836,7 +1836,7 @@ impl CollabPanel { gpui::elements::AnchorCorner::BottomLeft }, vec![ - ContextMenuItem::action("New Channel", NewChannel { channel_id }), + ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), ContextMenuItem::action("Manage members", ManageMembers { channel_id }), ContextMenuItem::action("Invite members", InviteMembers { channel_id }), From ddf3642d47d3fcf2ad6e7abc40d0272387cb8bf1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Aug 2023 13:18:56 -0700 Subject: [PATCH 224/326] Avoid flicker when moving between channels --- crates/call/src/call.rs | 6 +----- crates/call/src/room.rs | 19 +++++++++++-------- crates/collab/src/rpc.rs | 1 + 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 6e58be4f15..17540062e4 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -279,21 +279,17 @@ impl ActiveCall { channel_id: u64, cx: &mut ModelContext, ) -> Task> { - let leave_room; if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { return Task::ready(Ok(())); } else { - leave_room = room.update(cx, |room, cx| room.leave(cx)); + room.update(cx, |room, cx| room.clear_state(cx)); } - } else { - leave_room = Task::ready(Ok(())); } let join = Room::join_channel(channel_id, self.client.clone(), self.user_store.clone(), cx); cx.spawn(|this, mut cx| async move { - leave_room.await?; let room = join.await?; this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) .await?; diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 5a4bc8329f..a4ffa8866e 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -347,7 +347,18 @@ impl Room { } log::info!("leaving room"); + Audio::play_sound(Sound::Leave, cx); + self.clear_state(cx); + + let leave_room = self.client.request(proto::LeaveRoom {}); + cx.background().spawn(async move { + leave_room.await?; + anyhow::Ok(()) + }) + } + + pub(crate) fn clear_state(&mut self, cx: &mut AppContext) { for project in self.shared_projects.drain() { if let Some(project) = project.upgrade(cx) { project.update(cx, |project, cx| { @@ -364,8 +375,6 @@ impl Room { } } - Audio::play_sound(Sound::Leave, cx); - self.status = RoomStatus::Offline; self.remote_participants.clear(); self.pending_participants.clear(); @@ -374,12 +383,6 @@ impl Room { self.live_kit.take(); self.pending_room_update.take(); self.maintain_connection.take(); - - let leave_room = self.client.request(proto::LeaveRoom {}); - cx.background().spawn(async move { - leave_room.await?; - anyhow::Ok(()) - }) } async fn maintain_connection( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2396085a01..183aab8496 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2415,6 +2415,7 @@ async fn join_channel( let channel_id = ChannelId::from_proto(request.channel_id); let joined_room = { + leave_room_for_session(&session).await?; let db = session.db().await; let room_id = db.room_id_for_channel(channel_id).await?; From 13cf3ada39473416cd4ef96930071aa732d0b651 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 15 Aug 2023 16:29:01 -0400 Subject: [PATCH 225/326] Update checked icon --- crates/collab_ui/src/collab_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 7ad7a8883d..4c20411549 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1677,7 +1677,7 @@ impl CollabPanel { } else { theme.contact_button.style_for(mouse_state) }; - render_icon_button(button_style, "icons/check_8.svg") + render_icon_button(button_style, "icons/check.svg") .aligned() .flex_float() }) @@ -1762,7 +1762,7 @@ impl CollabPanel { } else { theme.contact_button.style_for(mouse_state) }; - render_icon_button(button_style, "icons/check_8.svg") + render_icon_button(button_style, "icons/check.svg") .aligned() .flex_float() }) From d6ca0a1f241df1de8937a4bcccd49953f26688e7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 15 Aug 2023 16:33:02 -0400 Subject: [PATCH 226/326] Associate extensions with language --- crates/zed/src/languages/cpp/config.toml | 2 +- crates/zed/src/languages/javascript/config.toml | 2 +- crates/zed/src/languages/python/config.toml | 2 +- crates/zed/src/languages/typescript/config.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/zed/src/languages/cpp/config.toml b/crates/zed/src/languages/cpp/config.toml index c719fae1eb..d9b38bca06 100644 --- a/crates/zed/src/languages/cpp/config.toml +++ b/crates/zed/src/languages/cpp/config.toml @@ -1,5 +1,5 @@ name = "C++" -path_suffixes = ["cc", "cpp", "h", "hpp"] +path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"] line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index c23ddcd6e7..8f4670388e 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -1,5 +1,5 @@ name = "JavaScript" -path_suffixes = ["js", "jsx", "mjs"] +path_suffixes = ["js", "jsx", "mjs", "cjs"] first_line_pattern = '^#!.*\bnode\b' line_comment = "// " autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index 80609de0ba..6777f6e60d 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -1,5 +1,5 @@ name = "Python" -path_suffixes = ["py", "pyi"] +path_suffixes = ["py", "pyi", "mpy"] first_line_pattern = '^#!.*\bpython[0-9.]*\b' line_comment = "# " autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/typescript/config.toml b/crates/zed/src/languages/typescript/config.toml index 4f006b342a..a2b764d9fe 100644 --- a/crates/zed/src/languages/typescript/config.toml +++ b/crates/zed/src/languages/typescript/config.toml @@ -1,5 +1,5 @@ name = "TypeScript" -path_suffixes = ["ts"] +path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"] line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ From 943aeb8c09507deb7a44c4529a42bd5d73d7cb8d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Aug 2023 13:42:47 -0700 Subject: [PATCH 227/326] Run until parked when setting editor's state via EditorTestContext Co-authored-by: Mikayla --- crates/vim/src/test/vim_test_context.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ab5d7382c7..ff8d835edc 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -92,6 +92,7 @@ impl<'a> VimTestContext<'a> { vim.switch_mode(mode, true, cx); }) }); + self.cx.foreground().run_until_parked(); context_handle } From de69f08c10767a2b123c6164416bb08bc80795f4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 15 Aug 2023 23:43:32 +0300 Subject: [PATCH 228/326] Support editor::SelectAll in Terminal --- crates/terminal/src/terminal.rs | 8 ++++++++ crates/terminal_view/src/terminal_view.rs | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f6cfe5ae30..3bae06a86d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -987,6 +987,14 @@ impl Terminal { } } + pub fn select_all(&mut self) { + let term = self.term.lock(); + let start = Point::new(term.topmost_line(), Column(0)); + let end = Point::new(term.bottommost_line(), term.last_column()); + drop(term); + self.set_selection(Some((make_selection(&(start..=end)), end))); + } + fn set_selection(&mut self, selection: Option<(Selection, Point)>) { self.events .push_back(InternalEvent::SetSelection(selection)); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 970e0115df..b48597d901 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -80,6 +80,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(TerminalView::paste); cx.add_action(TerminalView::clear); cx.add_action(TerminalView::show_character_palette); + cx.add_action(TerminalView::select_all) } ///A terminal view, maintains the PTY's file handles and communicates with the terminal @@ -312,6 +313,11 @@ impl TerminalView { } } + fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext) { + self.terminal.update(cx, |term, _| term.select_all()); + cx.notify(); + } + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { self.terminal.update(cx, |term, _| term.clear()); cx.notify(); From 1ffde7bddc2d1e06cb849587532fa40f92b22c78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Aug 2023 14:56:54 -0700 Subject: [PATCH 229/326] Implement calling contacts into your current channel Co-authored-by: Mikayla --- crates/call/src/call.rs | 8 ++- crates/call/src/room.rs | 81 ++++++++++++------------ crates/collab/src/db.rs | 36 +++++++++-- crates/collab/src/db/tests.rs | 25 ++------ crates/collab/src/rpc.rs | 40 ++++++++---- crates/collab/src/tests/channel_tests.rs | 74 ++++++++++++++++++++++ crates/collab_ui/src/collab_panel.rs | 7 +- crates/rpc/proto/zed.proto | 3 +- 8 files changed, 187 insertions(+), 87 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 17540062e4..33ba7a2ab9 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -6,7 +6,9 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use call_settings::CallSettings; -use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; +use client::{ + proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, +}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; @@ -75,6 +77,10 @@ impl ActiveCall { } } + pub fn channel_id(&self, cx: &AppContext) -> Option { + self.room()?.read(cx).channel_id() + } + async fn handle_incoming_call( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index a4ffa8866e..6f01b1d757 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -274,26 +274,13 @@ impl Room { user_store: ModelHandle, cx: &mut AppContext, ) -> Task>> { - cx.spawn(|mut cx| async move { - let response = client.request(proto::JoinChannel { channel_id }).await?; - let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.add_model(|cx| { - Self::new( - room_proto.id, - Some(channel_id), - response.live_kit_connection_info, - client, - user_store, - cx, - ) - }); - - room.update(&mut cx, |room, cx| { - room.apply_room_update(room_proto, cx)?; - anyhow::Ok(()) - })?; - - Ok(room) + cx.spawn(|cx| async move { + Self::from_join_response( + client.request(proto::JoinChannel { channel_id }).await?, + client, + user_store, + cx, + ) }) } @@ -303,30 +290,42 @@ impl Room { user_store: ModelHandle, cx: &mut AppContext, ) -> Task>> { - let room_id = call.room_id; - cx.spawn(|mut cx| async move { - let response = client.request(proto::JoinRoom { id: room_id }).await?; - let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; - let room = cx.add_model(|cx| { - Self::new( - room_id, - None, - response.live_kit_connection_info, - client, - user_store, - cx, - ) - }); - room.update(&mut cx, |room, cx| { - room.leave_when_empty = true; - room.apply_room_update(room_proto, cx)?; - anyhow::Ok(()) - })?; - - Ok(room) + let id = call.room_id; + cx.spawn(|cx| async move { + Self::from_join_response( + client.request(proto::JoinRoom { id }).await?, + client, + user_store, + cx, + ) }) } + fn from_join_response( + response: proto::JoinRoomResponse, + client: Arc, + user_store: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; + let room = cx.add_model(|cx| { + Self::new( + room_proto.id, + response.channel_id, + response.live_kit_connection_info, + client, + user_store, + cx, + ) + }); + room.update(&mut cx, |room, cx| { + room.leave_when_empty = room.channel_id.is_none(); + room.apply_room_update(room_proto, cx)?; + anyhow::Ok(()) + })?; + Ok(room) + } + fn should_leave(&self) -> bool { self.leave_when_empty && self.pending_room_update.is_none() diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b7718be118..64349123af 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1376,15 +1376,27 @@ impl Database { &self, room_id: RoomId, user_id: UserId, - channel_id: Option, connection: ConnectionId, ) -> Result> { self.room_transaction(room_id, |tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelId { + ChannelId, + } + let channel_id: Option = room::Entity::find() + .select_only() + .column(room::Column::ChannelId) + .filter(room::Column::Id.eq(room_id)) + .into_values::<_, QueryChannelId>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + if let Some(channel_id) = channel_id { self.check_user_is_channel_member(channel_id, user_id, &*tx) .await?; - room_participant::ActiveModel { + 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)), @@ -1392,15 +1404,23 @@ impl Database { connection.owner_id as i32, ))), answering_connection_lost: ActiveValue::set(false), - // Redundant for the channel join use case, used for channel and call invitations 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, ))), ..Default::default() - } - .insert(&*tx) + }]) + .on_conflict( + OnConflict::columns([room_participant::Column::UserId]) + .update_columns([ + room_participant::Column::AnsweringConnectionId, + room_participant::Column::AnsweringConnectionServerId, + room_participant::Column::AnsweringConnectionLost, + ]) + .to_owned(), + ) + .exec(&*tx) .await?; } else { let result = room_participant::Entity::update_many() @@ -4053,6 +4073,12 @@ impl DerefMut for RoomGuard { } } +impl RoomGuard { + pub fn into_inner(self) -> T { + self.data + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct NewUserParams { pub github_login: String, diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 2680d81aac..dbbf162d12 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -494,14 +494,9 @@ test_both_dbs!( ) .await .unwrap(); - db.join_room( - room_id, - user2.user_id, - None, - ConnectionId { owner_id, id: 1 }, - ) - .await - .unwrap(); + db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) @@ -1113,12 +1108,7 @@ test_both_dbs!( // can join a room with membership to its channel let joined_room = db - .join_room( - room_1, - user_1, - Some(channel_1), - ConnectionId { owner_id, id: 1 }, - ) + .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) .await .unwrap(); assert_eq!(joined_room.room.participants.len(), 1); @@ -1126,12 +1116,7 @@ test_both_dbs!( drop(joined_room); // cannot join a room without membership to its channel assert!(db - .join_room( - room_1, - user_2, - Some(channel_1), - ConnectionId { owner_id, id: 1 } - ) + .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) .await .is_err()); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 183aab8496..521aa3e7b4 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -930,16 +930,26 @@ async fn join_room( session: Session, ) -> Result<()> { let room_id = RoomId::from_proto(request.id); - let room = { + let joined_room = { let room = session .db() .await - .join_room(room_id, session.user_id, None, session.connection_id) + .join_room(room_id, session.user_id, session.connection_id) .await?; room_updated(&room.room, &session.peer); - room.room.clone() + 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 @@ -958,7 +968,10 @@ async fn join_room( let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() { if let Some(token) = live_kit - .room_token(&room.live_kit_room, &session.user_id.to_string()) + .room_token( + &joined_room.room.live_kit_room, + &session.user_id.to_string(), + ) .trace_err() { Some(proto::LiveKitConnectionInfo { @@ -973,7 +986,8 @@ async fn join_room( }; response.send(proto::JoinRoomResponse { - room: Some(room), + room: Some(joined_room.room), + channel_id: joined_room.channel_id.map(|id| id.to_proto()), live_kit_connection_info, })?; @@ -1151,9 +1165,11 @@ async fn rejoin_room( } } - room = mem::take(&mut rejoined_room.room); + let rejoined_room = rejoined_room.into_inner(); + + room = rejoined_room.room; channel_id = rejoined_room.channel_id; - channel_members = mem::take(&mut rejoined_room.channel_members); + channel_members = rejoined_room.channel_members; } if let Some(channel_id) = channel_id { @@ -2421,12 +2437,7 @@ async fn join_channel( let room_id = db.room_id_for_channel(channel_id).await?; let joined_room = db - .join_room( - room_id, - session.user_id, - Some(channel_id), - session.connection_id, - ) + .join_room(room_id, session.user_id, session.connection_id) .await?; let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| { @@ -2445,12 +2456,13 @@ async fn join_channel( response.send(proto::JoinRoomResponse { room: Some(joined_room.room.clone()), + channel_id: joined_room.channel_id.map(|id| id.to_proto()), live_kit_connection_info, })?; room_updated(&joined_room.room, &session.peer); - joined_room.clone() + joined_room.into_inner() }; channel_updated( diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index d4cf6423f0..d778b6a472 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -696,6 +696,80 @@ async fn test_channel_rename( ); } +#[gpui::test] +async fn test_call_from_channel( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let channel_id = server + .make_channel( + "x", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + + // Client A calls client B while in the channel. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_b.user_id().unwrap(), None, cx) + }) + .await + .unwrap(); + + // Client B accepts the call. + deterministic.run_until_parked(); + active_call_b + .update(cx_b, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + + // Client B sees that they are now in the channel + deterministic.run_until_parked(); + active_call_b.read_with(cx_b, |call, cx| { + assert_eq!(call.channel_id(cx), Some(channel_id)); + }); + client_b.channel_store().read_with(cx_b, |channels, _| { + assert_participants_eq( + channels.channel_participants(channel_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }); + + // Clients A and C also see that client B is in the channel. + client_a.channel_store().read_with(cx_a, |channels, _| { + assert_participants_eq( + channels.channel_participants(channel_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }); + client_c.channel_store().read_with(cx_c, |channels, _| { + assert_participants_eq( + channels.channel_participants(channel_id), + &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], + ); + }); +} + #[derive(Debug, PartialEq)] struct ExpectedChannel { depth: usize, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4c20411549..498b278abd 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1183,11 +1183,8 @@ impl CollabPanel { let text = match section { Section::ActiveCall => { let channel_name = iife!({ - let channel_id = ActiveCall::global(cx) - .read(cx) - .room()? - .read(cx) - .channel_id()?; + let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + let name = self .channel_store .read(cx) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index fc9a66753c..caa5efd2cb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -176,7 +176,8 @@ message JoinRoom { message JoinRoomResponse { Room room = 1; - optional LiveKitConnectionInfo live_kit_connection_info = 2; + optional uint64 channel_id = 2; + optional LiveKitConnectionInfo live_kit_connection_info = 3; } message RejoinRoom { From 1b4dd49b1d8618e8149db3a28dbb398bdef1a52c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 15 Aug 2023 13:26:04 -0600 Subject: [PATCH 230/326] Vim visual block mode This isn't quite an exact emulation, as instead of using one selection that is magically in "column mode", we emulate it with a bunch of zed multi-selections (one per line). I think this is better, as it requires fewer changes to the codebase, and lets you see the impact of any changes immediately on all lines. Fixes: zed-industries/community#984 --- assets/keymaps/vim.json | 1 + crates/editor/src/selections_collection.rs | 20 +- crates/vim/src/object.rs | 40 +- crates/vim/src/state.rs | 1 + .../src/test/neovim_backed_test_context.rs | 11 +- crates/vim/src/test/neovim_connection.rs | 171 ++++++--- crates/vim/src/vim.rs | 24 +- crates/vim/src/visual.rs | 354 +++++++++++++++--- .../vim/test_data/test_visual_block_mode.json | 31 ++ 9 files changed, 518 insertions(+), 135 deletions(-) create mode 100644 crates/vim/test_data/test_visual_block_mode.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 02c09b33af..fc54934f2b 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -103,6 +103,7 @@ ], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", + "ctrl-v": "vim::ToggleVisualBlock", "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 1921bc0738..6a21c898ef 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -1,7 +1,7 @@ use std::{ cell::Ref, cmp, iter, mem, - ops::{Deref, Range, Sub}, + ops::{Deref, DerefMut, Range, Sub}, sync::Arc, }; @@ -53,7 +53,7 @@ impl SelectionsCollection { } } - fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { + pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot { self.display_map.update(cx, |map, cx| map.snapshot(cx)) } @@ -250,6 +250,10 @@ impl SelectionsCollection { resolve(self.oldest_anchor(), &self.buffer(cx)) } + pub fn first_anchor(&self) -> Selection { + self.disjoint[0].clone() + } + pub fn first>( &self, cx: &AppContext, @@ -352,7 +356,7 @@ pub struct MutableSelectionsCollection<'a> { } impl<'a> MutableSelectionsCollection<'a> { - fn display_map(&mut self) -> DisplaySnapshot { + pub fn display_map(&mut self) -> DisplaySnapshot { self.collection.display_map(self.cx) } @@ -607,6 +611,10 @@ impl<'a> MutableSelectionsCollection<'a> { self.select_anchors(selections) } + pub fn new_selection_id(&mut self) -> usize { + post_inc(&mut self.next_selection_id) + } + pub fn select_display_ranges(&mut self, ranges: T) where T: IntoIterator>, @@ -831,6 +839,12 @@ impl<'a> Deref for MutableSelectionsCollection<'a> { } } +impl<'a> DerefMut for MutableSelectionsCollection<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.collection + } +} + // Panics if passed selections are not in order pub fn resolve_multiple<'a, D, I>( selections: I, diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index e1e21e4e3b..37476caed5 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -72,6 +72,18 @@ fn object(object: Object, cx: &mut WindowContext) { } impl Object { + pub fn is_multiline(self) -> bool { + match self { + Object::Word { .. } | Object::Quotes | Object::BackQuotes | Object::DoubleQuotes => { + false + } + Object::Sentence + | Object::Parentheses + | Object::AngleBrackets + | Object::CurlyBrackets + | Object::SquareBrackets => true, + } + } pub fn range( self, map: &DisplaySnapshot, @@ -87,13 +99,27 @@ impl Object { } } Object::Sentence => sentence(map, relative_to, around), - Object::Quotes => surrounding_markers(map, relative_to, around, false, '\'', '\''), - Object::BackQuotes => surrounding_markers(map, relative_to, around, false, '`', '`'), - Object::DoubleQuotes => surrounding_markers(map, relative_to, around, false, '"', '"'), - Object::Parentheses => surrounding_markers(map, relative_to, around, true, '(', ')'), - Object::SquareBrackets => surrounding_markers(map, relative_to, around, true, '[', ']'), - Object::CurlyBrackets => surrounding_markers(map, relative_to, around, true, '{', '}'), - Object::AngleBrackets => surrounding_markers(map, relative_to, around, true, '<', '>'), + Object::Quotes => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'') + } + Object::BackQuotes => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`') + } + Object::DoubleQuotes => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') + } + Object::Parentheses => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') + } + Object::SquareBrackets => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') + } + Object::CurlyBrackets => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}') + } + Object::AngleBrackets => { + surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') + } } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index b38dac4aa8..66aaec02b9 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -44,6 +44,7 @@ pub enum Operator { #[derive(Default)] pub struct VimState { pub mode: Mode, + pub last_mode: Mode, pub operator_stack: Vec, pub search: SearchState, diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 1c7559e440..263692b36e 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -160,7 +160,7 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn neovim_state(&mut self) -> String { generate_marked_text( self.neovim.text().await.as_str(), - &vec![self.neovim_selection().await], + &self.neovim_selections().await[..], true, ) } @@ -169,9 +169,12 @@ impl<'a> NeovimBackedTestContext<'a> { self.neovim.mode().await.unwrap() } - async fn neovim_selection(&mut self) -> Range { - let neovim_selection = self.neovim.selection().await; - neovim_selection.to_offset(&self.buffer_snapshot()) + async fn neovim_selections(&mut self) -> Vec> { + let neovim_selections = self.neovim.selections().await; + neovim_selections + .into_iter() + .map(|selection| selection.to_offset(&self.buffer_snapshot())) + .collect() } pub async fn assert_state_matches(&mut self) { diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index e983d5ceec..ddeb26164b 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -1,5 +1,8 @@ #[cfg(feature = "neovim")] -use std::ops::{Deref, DerefMut}; +use std::{ + cmp, + ops::{Deref, DerefMut}, +}; use std::{ops::Range, path::PathBuf}; #[cfg(feature = "neovim")] @@ -135,7 +138,7 @@ impl NeovimConnection { #[cfg(feature = "neovim")] pub async fn set_state(&mut self, marked_text: &str) { - let (text, selection) = parse_state(&marked_text); + let (text, selections) = parse_state(&marked_text); let nvim_buffer = self .nvim @@ -167,6 +170,11 @@ impl NeovimConnection { .await .expect("Could not get neovim window"); + if selections.len() != 1 { + panic!("must have one selection"); + } + let selection = &selections[0]; + let cursor = selection.start; nvim_window .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) @@ -224,7 +232,7 @@ impl NeovimConnection { } #[cfg(feature = "neovim")] - pub async fn state(&mut self) -> (Option, String, Range) { + pub async fn state(&mut self) -> (Option, String, Vec>) { let nvim_buffer = self .nvim .get_current_buf() @@ -263,14 +271,48 @@ impl NeovimConnection { "n" => Some(Mode::Normal), "v" => Some(Mode::Visual), "V" => Some(Mode::VisualLine), - "CTRL-V" => Some(Mode::VisualBlock), + "\x16" => Some(Mode::VisualBlock), _ => None, }; + let mut selections = Vec::new(); // Vim uses the index of the first and last character in the selection // Zed uses the index of the positions between the characters, so we need // to add one to the end in visual mode. match mode { + Some(Mode::VisualBlock) if selection_row != cursor_row => { + // in zed we fake a block selecrtion by using multiple cursors (one per line) + // this code emulates that. + // to deal with casees where the selection is not perfectly rectangular we extract + // the content of the selection via the "a register to get the shape correctly. + self.nvim.input("\"aygv").await.unwrap(); + let content = self.nvim.command_output("echo getreg('a')").await.unwrap(); + let lines = content.split("\n").collect::>(); + let top = cmp::min(selection_row, cursor_row); + let left = cmp::min(selection_col, cursor_col); + for row in top..=cmp::max(selection_row, cursor_row) { + let content = if row - top >= lines.len() as u32 { + "" + } else { + lines[(row - top) as usize] + }; + let line_len = self + .read_position(format!("echo strlen(getline({}))", row + 1).as_str()) + .await; + + if left > line_len { + continue; + } + + let start = Point::new(row, left); + let end = Point::new(row, left + content.len() as u32); + if cursor_col >= selection_col { + selections.push(start..end) + } else { + selections.push(end..start) + } + } + } Some(Mode::Visual) | Some(Mode::VisualLine) | Some(Mode::VisualBlock) => { if selection_col > cursor_col { let selection_line_length = @@ -291,38 +333,37 @@ impl NeovimConnection { cursor_row += 1; } } + selections.push( + Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col), + ) } - Some(Mode::Insert) | Some(Mode::Normal) | None => {} + Some(Mode::Insert) | Some(Mode::Normal) | None => selections + .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), } - let (start, end) = ( - Point::new(selection_row, selection_col), - Point::new(cursor_row, cursor_col), - ); - let state = NeovimData::Get { mode, - state: encode_range(&text, start..end), + state: encode_ranges(&text, &selections), }; if self.data.back() != Some(&state) { self.data.push_back(state.clone()); } - (mode, text, start..end) + (mode, text, selections) } #[cfg(not(feature = "neovim"))] - pub async fn state(&mut self) -> (Option, String, Range) { + pub async fn state(&mut self) -> (Option, String, Vec>) { if let Some(NeovimData::Get { state: text, mode }) = self.data.front() { - let (text, range) = parse_state(text); - (*mode, text, range) + let (text, ranges) = parse_state(text); + (*mode, text, ranges) } else { panic!("operation does not match recorded script. re-record with --features=neovim"); } } - pub async fn selection(&mut self) -> Range { + pub async fn selections(&mut self) -> Vec> { self.state().await.2 } @@ -422,51 +463,63 @@ impl Handler for NvimHandler { } } -fn parse_state(marked_text: &str) -> (String, Range) { +fn parse_state(marked_text: &str) -> (String, Vec>) { let (text, ranges) = util::test::marked_text_ranges(marked_text, true); - let byte_range = ranges[0].clone(); - let mut point_range = Point::zero()..Point::zero(); - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if ix == byte_range.start { - point_range.start = position; - } - if ix == byte_range.end { - point_range.end = position; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - (text, point_range) + let point_ranges = ranges + .into_iter() + .map(|byte_range| { + let mut point_range = Point::zero()..Point::zero(); + let mut ix = 0; + let mut position = Point::zero(); + for c in text.chars().chain(['\0']) { + if ix == byte_range.start { + point_range.start = position; + } + if ix == byte_range.end { + point_range.end = position; + } + let len_utf8 = c.len_utf8(); + ix += len_utf8; + if c == '\n' { + position.row += 1; + position.column = 0; + } else { + position.column += len_utf8 as u32; + } + } + point_range + }) + .collect::>(); + (text, point_ranges) } #[cfg(feature = "neovim")] -fn encode_range(text: &str, range: Range) -> String { - let mut byte_range = 0..0; - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if position == range.start { - byte_range.start = ix; - } - if position == range.end { - byte_range.end = ix; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - util::test::generate_marked_text(text, &[byte_range], true) +fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { + let byte_ranges = point_ranges + .into_iter() + .map(|range| { + let mut byte_range = 0..0; + let mut ix = 0; + let mut position = Point::zero(); + for c in text.chars().chain(['\0']) { + if position == range.start { + byte_range.start = ix; + } + if position == range.end { + byte_range.end = ix; + } + let len_utf8 = c.len_utf8(); + ix += len_utf8; + if c == '\n' { + position.row += 1; + position.column = 0; + } else { + position.column += len_utf8 as u32; + } + } + byte_range + }) + .collect::>(); + let ret = util::test::generate_marked_text(text, &byte_ranges[..], true); + ret } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 038e47659d..df35e951d2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -26,7 +26,7 @@ use serde::Deserialize; use settings::{Setting, SettingsStore}; use state::{Mode, Operator, VimState}; use std::sync::Arc; -use visual::visual_replace; +use visual::{visual_block_motion, visual_replace}; use workspace::{self, Workspace}; struct VimModeSetting(bool); @@ -182,6 +182,8 @@ impl Vim { fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { let last_mode = self.state.mode; + let prior_mode = self.state.last_mode; + self.state.last_mode = last_mode; self.state.mode = mode; self.state.operator_stack.clear(); @@ -196,7 +198,27 @@ impl Vim { // Adjust selections self.update_active_editor(cx, |editor, cx| { + if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock + { + visual_block_motion(true, editor, cx, |_, point, goal| Some((point, goal))) + } + editor.change_selections(None, cx, |s| { + // we cheat with visual block mode and use multiple cursors. + // the cost of this cheat is we need to convert back to a single + // cursor whenever vim would. + if last_mode == Mode::VisualBlock && mode != Mode::VisualBlock { + let tail = s.oldest_anchor().tail(); + let head = s.newest_anchor().head(); + s.select_anchor_ranges(vec![tail..head]); + } else if last_mode == Mode::Insert + && prior_mode == Mode::VisualBlock + && mode != Mode::VisualBlock + { + let pos = s.first_anchor().head(); + s.select_anchor_ranges(vec![pos..pos]) + } + s.move_with(|map, selection| { if last_mode.is_visual() && !mode.is_visual() { let mut point = selection.head(); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index e5f9d8c459..cb4d865dc9 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -2,10 +2,13 @@ use std::{borrow::Cow, sync::Arc}; use collections::HashMap; use editor::{ - display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection, + display_map::{DisplaySnapshot, ToDisplayPoint}, + movement, + scroll::autoscroll::Autoscroll, + Bias, ClipboardSelection, DisplayPoint, Editor, }; use gpui::{actions, AppContext, ViewContext, WindowContext}; -use language::{AutoindentMode, SelectionGoal}; +use language::{AutoindentMode, Selection, SelectionGoal}; use workspace::Workspace; use crate::{ @@ -21,6 +24,7 @@ actions!( [ ToggleVisual, ToggleVisualLine, + ToggleVisualBlock, VisualDelete, VisualYank, VisualPaste, @@ -29,8 +33,17 @@ actions!( ); pub fn init(cx: &mut AppContext) { - cx.add_action(toggle_visual); - cx.add_action(toggle_visual_line); + cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext| { + toggle_mode(Mode::Visual, cx) + }); + cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext| { + toggle_mode(Mode::VisualLine, cx) + }); + cx.add_action( + |_, _: &ToggleVisualBlock, cx: &mut ViewContext| { + toggle_mode(Mode::VisualBlock, cx) + }, + ); cx.add_action(other_end); cx.add_action(delete); cx.add_action(yank); @@ -40,55 +53,169 @@ 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| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let was_reversed = selection.reversed; + if vim.state.mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { + 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) + }) + } else { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let was_reversed = selection.reversed; + let mut current_head = selection.head(); - let mut current_head = selection.head(); + // our motions assume the current character is after the cursor, + // but in (forward) visual mode the current character is just + // before the end of the selection. - // our motions assume the current character is after the cursor, - // but in (forward) visual mode the current character is just - // before the end of the selection. + // If the file ends with a newline (which is common) we don't do this. + // so that if you go to the end of such a file you can use "up" to go + // to the previous line and have it work somewhat as expected. + if !selection.reversed + && !selection.is_empty() + && !(selection.end.column() == 0 && selection.end == map.max_point()) + { + current_head = movement::left(map, selection.end) + } - // If the file ends with a newline (which is common) we don't do this. - // so that if you go to the end of such a file you can use "up" to go - // to the previous line and have it work somewhat as expected. - if !selection.reversed - && !selection.is_empty() - && !(selection.end.column() == 0 && selection.end == map.max_point()) - { - current_head = movement::left(map, selection.end) - } - - let Some((new_head, goal)) = + let Some((new_head, goal)) = motion.move_point(map, current_head, selection.goal, times) else { return }; - selection.set_head(new_head, goal); + selection.set_head(new_head, goal); - // ensure the current character is included in the selection. - if !selection.reversed { - // TODO: maybe try clipping left for multi-buffers - let next_point = movement::right(map, selection.end); + // ensure the current character is included in the selection. + if !selection.reversed { + let next_point = if vim.state.mode == Mode::VisualBlock { + movement::saturating_right(map, selection.end) + } else { + movement::right(map, selection.end) + }; - if !(next_point.column() == 0 && next_point == map.max_point()) { - selection.end = movement::right(map, selection.end) + if !(next_point.column() == 0 && next_point == map.max_point()) { + selection.end = next_point; + } } - } - // vim always ensures the anchor character stays selected. - // if our selection has reversed, we need to move the opposite end - // to ensure the anchor is still selected. - if was_reversed && !selection.reversed { - selection.start = movement::left(map, selection.start); - } else if !was_reversed && selection.reversed { - selection.end = movement::right(map, selection.end); - } + // vim always ensures the anchor character stays selected. + // if our selection has reversed, we need to move the opposite end + // to ensure the anchor is still selected. + if was_reversed && !selection.reversed { + selection.start = movement::left(map, selection.start); + } else if !was_reversed && selection.reversed { + selection.end = movement::right(map, selection.end); + } + }) }); - }); + } }); }); } +pub fn visual_block_motion( + preserve_goal: bool, + editor: &mut Editor, + cx: &mut ViewContext, + mut move_selection: impl FnMut( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> Option<(DisplayPoint, SelectionGoal)>, +) { + 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 mut goal = s.newest_anchor().goal; + + let was_reversed = tail.column() > head.column(); + + if !was_reversed && !(head.column() == 0 && head == map.max_point()) { + head = movement::saturating_left(map, head); + } + + let Some((new_head, new_goal)) = move_selection(&map, head, goal) else { + return + }; + head = new_head; + if goal == SelectionGoal::None { + goal = new_goal; + } + + let mut is_reversed = tail.column() > head.column(); + if was_reversed && !is_reversed { + tail = movement::left(map, tail) + } else if !was_reversed && is_reversed { + tail = movement::right(map, tail) + } + if !is_reversed { + head = movement::saturating_right(map, head) + } + + if !preserve_goal + || !matches!( + goal, + SelectionGoal::ColumnRange { .. } | SelectionGoal::Column(_) + ) + { + goal = SelectionGoal::ColumnRange { + start: tail.column(), + end: head.column(), + } + } + + let mut columns = if let SelectionGoal::ColumnRange { start, end } = goal { + if start > end { + is_reversed = true; + end..start + } else { + is_reversed = false; + start..end + } + } else if let SelectionGoal::Column(column) = goal { + is_reversed = false; + column..(column + 1) + } else { + unreachable!() + }; + + if columns.start >= map.line_len(head.row()) { + columns.start = map.line_len(head.row()).saturating_sub(1); + } + if columns.start >= map.line_len(tail.row()) { + columns.start = map.line_len(tail.row()).saturating_sub(1); + } + + 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 mut selection = Selection { + id: s.new_selection_id(), + start: start.to_point(map), + end: end.to_point(map), + reversed: is_reversed, + goal: goal.clone(), + }; + + selections.push(selection); + } + if row == head.row() { + break; + } + if tail.row() > head.row() { + row -= 1 + } else { + row += 1 + } + } + + s.select(selections); + }) +} + pub fn visual_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if let Some(Operator::Object { around }) = vim.active_operator() { @@ -136,28 +263,12 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { }); } -pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| match vim.state.mode { - Mode::Normal | Mode::Insert | Mode::VisualLine | Mode::VisualBlock => { - vim.switch_mode(Mode::Visual, false, cx); - } - Mode::Visual => { - vim.switch_mode(Mode::Normal, false, cx); - } - }) -} - -pub fn toggle_visual_line( - _: &mut Workspace, - _: &ToggleVisualLine, - cx: &mut ViewContext, -) { - Vim::update(cx, |vim, cx| match vim.state.mode { - Mode::Normal | Mode::Insert | Mode::Visual | Mode::VisualBlock => { - vim.switch_mode(Mode::VisualLine, false, cx); - } - Mode::VisualLine => { +fn toggle_mode(mode: Mode, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + if vim.state.mode == mode { vim.switch_mode(Mode::Normal, false, cx); + } else { + vim.switch_mode(mode, false, cx); } }) } @@ -207,6 +318,9 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) s.move_with(|_, selection| { selection.collapse_to(selection.start, SelectionGoal::None) }); + if vim.state.mode == Mode::VisualBlock { + s.select_anchors(vec![s.first_anchor()]) + } }); }); vim.switch_mode(Mode::Normal, true, cx); @@ -275,7 +392,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext linewise = all_selections_were_entire_line; } - let mut selection = selection.clone(); + let selection = selection.clone(); if !selection.reversed { let adjusted = selection.end; // If the selection is empty, move both the start and end forward one @@ -751,4 +868,119 @@ mod test { Mode::Normal, ); } + + #[gpui::test] + async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v"]).await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["2", "down"]).await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + fox «jˇ»umps over + the «lˇ»azy dog" + }) + .await; + cx.simulate_shared_keystrokes(["e"]).await; + cx.assert_shared_state(indoc! { + "The «quicˇ»k brown + fox «jumpˇ»s over + the «lazyˇ» dog" + }) + .await; + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { + "«ˇThe q»uick brown + «ˇfox j»umps over + «ˇthe l»azy dog" + }) + .await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! { + "The «quick brownˇ» + fox «jumps overˇ» + the «lazy dogˇ»" + }) + .await; + cx.simulate_shared_keystrokes(["shift-f", " "]).await; + cx.assert_shared_state(indoc! { + "The «quickˇ» brown + fox «jumpsˇ» over + the «lazy ˇ»dog" + }) + .await; + + // toggling through visual mode works as expected + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! { + "The «quick brown + fox jumps over + the lazy ˇ»dog" + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v"]).await; + cx.assert_shared_state(indoc! { + "The «quickˇ» brown + fox «jumpsˇ» over + the «lazy ˇ»dog" + }) + .await; + + cx.set_shared_state(indoc! { + "The ˇquick + brown + fox + jumps over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "down"]) + .await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick + brow«nˇ» + fox + jump«sˇ» over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystroke("left").await; + cx.assert_shared_state(indoc! { + "The«ˇ q»uick + bro«ˇwn» + foxˇ + jum«ˇps» over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["s", "o", "escape"]).await; + cx.assert_shared_state(indoc! { + "Theˇouick + broo + foxo + jumo over the + + lazy dog + " + }) + .await; + } } diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json new file mode 100644 index 0000000000..743f7fa76c --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -0,0 +1,31 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"ctrl-v"} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualBlock"}} +{"Key":"2"} +{"Key":"down"} +{"Get":{"state":"The «qˇ»uick brown\nfox «jˇ»umps over\nthe «lˇ»azy dog","mode":"VisualBlock"}} +{"Key":"e"} +{"Get":{"state":"The «quicˇ»k brown\nfox «jumpˇ»s over\nthe «lazyˇ» dog","mode":"VisualBlock"}} +{"Key":"^"} +{"Get":{"state":"«ˇThe q»uick brown\n«ˇfox j»umps over\n«ˇthe l»azy dog","mode":"VisualBlock"}} +{"Key":"$"} +{"Get":{"state":"The «quick brownˇ»\nfox «jumps overˇ»\nthe «lazy dogˇ»","mode":"VisualBlock"}} +{"Key":"shift-f"} +{"Key":" "} +{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} +{"Key":"v"} +{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy ˇ»dog","mode":"Visual"}} +{"Key":"ctrl-v"} +{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} +{"Put":{"state":"The ˇquick\nbrown\nfox\njumps over the\n\nlazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"down"} +{"Key":"down"} +{"Key":"down"} +{"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}} +{"Key":"left"} +{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njum«ˇps» over the\n\nlazy dog\n","mode":"VisualBlock"}} +{"Key":"s"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} From 32bec2e401762ea3a1c7003855b8a9c449404b38 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 15 Aug 2023 23:18:03 +0100 Subject: [PATCH 231/326] left align buffer search with new structure --- crates/search/src/buffer_search.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c634a71297..cbb68ba99f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -224,12 +224,17 @@ impl View for BufferSearchBar { let side_column_min_width = 165.; let button_height = 32.; let nav_column = Flex::row() + .with_child( + Flex::row() + .with_children(match_count) + .constrained() + .with_min_width(100.), + ) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_children(match_count) + .with_child(self.render_action_button("Select All", cx)) .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(side_column_min_width); + .with_height(theme.search.search_bar_row_height); let query = Flex::row() .with_child( @@ -257,7 +262,6 @@ impl View for BufferSearchBar { ) .align_children_center() .flex(1., true); - let row_spacing = theme.workspace.toolbar.container.padding.bottom; let editor_column = Flex::row() .with_child( query @@ -268,12 +272,9 @@ impl View for BufferSearchBar { .with_max_width(theme.search.editor.max_width) .flex(1., false), ) - .with_child(self.render_action_button("Select All", cx)) .contained() .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .top() .flex(1., false); let mode_column = Flex::row() .with_child( @@ -298,8 +299,8 @@ impl View for BufferSearchBar { .with_min_width(side_column_min_width) .flex_float(); Flex::row() - .with_child(nav_column) .with_child(editor_column) + .with_child(nav_column) .with_child(mode_column) .contained() .with_style(theme.search.container) From 3623a9ca5edb0f7e39cf173514e50c8b25b3b0f3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Aug 2023 15:26:02 -0700 Subject: [PATCH 232/326] Simplify Component implementation Co-authored-by: Mikayla --- crates/gpui/examples/components.rs | 82 +++++++++++++----------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index 50ace5eee0..09a732c6c1 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -1,6 +1,6 @@ use button_component::Button; -use component::AdaptComponent; +use component::Component; use gpui::{ color::Color, elements::{ContainerStyle, Flex, Label, ParentElement}, @@ -14,6 +14,8 @@ use simplelog::SimpleLogger; use theme::Toggleable; use toggleable_button::ToggleableButton; +// cargo run -p gpui --example components + fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); @@ -155,14 +157,8 @@ mod toggleable_button { } } - impl Component for ToggleableButton { - type View = V; - - fn render( - self, - v: &mut Self::View, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for ToggleableButton { + fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let button = if let Some(style) = self.style { self.button.with_style(*style.style_for(self.active)) } else { @@ -219,10 +215,8 @@ mod button_component { } } - impl Component for Button { - type View = V; - - fn render(self, _: &mut Self::View, cx: &mut ViewContext) -> AnyElement { + impl Component for Button { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { let click_handler = self.click_handler; let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| { @@ -250,45 +244,41 @@ mod component { use gpui::{AnyElement, Element, View, ViewContext}; use pathfinder_geometry::vector::Vector2F; - // Public API: - pub trait Component { - type View: View; + pub trait Component { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn render( - self, - v: &mut Self::View, - cx: &mut ViewContext, - ) -> AnyElement; - } - - pub struct ComponentAdapter { - component: Option, - } - - impl ComponentAdapter { - pub fn new(e: E) -> Self { - Self { component: Some(e) } - } - } - - pub trait AdaptComponent: Sized { - fn into_element(self) -> ComponentAdapter { + fn into_element(self) -> ComponentAdapter + where + Self: Sized, + { ComponentAdapter::new(self) } } - impl AdaptComponent for C {} + pub struct ComponentAdapter { + component: Option, + phantom: std::marker::PhantomData, + } - impl Element for ComponentAdapter { - type LayoutState = AnyElement; + impl ComponentAdapter { + pub fn new(e: E) -> Self { + Self { + component: Some(e), + phantom: std::marker::PhantomData, + } + } + } + + impl + 'static> Element for ComponentAdapter { + type LayoutState = AnyElement; type PaintState = (); fn layout( &mut self, constraint: gpui::SizeConstraint, - view: &mut C::View, - cx: &mut gpui::LayoutContext, + view: &mut V, + cx: &mut gpui::LayoutContext, ) -> (Vector2F, Self::LayoutState) { let component = self.component.take().unwrap(); let mut element = component.render(view, cx.view_context()); @@ -302,8 +292,8 @@ mod component { bounds: gpui::geometry::rect::RectF, visible_bounds: gpui::geometry::rect::RectF, layout: &mut Self::LayoutState, - view: &mut C::View, - cx: &mut gpui::PaintContext, + view: &mut V, + cx: &mut gpui::PaintContext, ) -> Self::PaintState { layout.paint(scene, bounds.origin(), visible_bounds, view, cx) } @@ -315,8 +305,8 @@ mod component { _: gpui::geometry::rect::RectF, _: &Self::LayoutState, _: &Self::PaintState, - _: &C::View, - _: &ViewContext, + _: &V, + _: &ViewContext, ) -> Option { todo!() } @@ -326,8 +316,8 @@ mod component { _: gpui::geometry::rect::RectF, _: &Self::LayoutState, _: &Self::PaintState, - _: &C::View, - _: &ViewContext, + _: &V, + _: &ViewContext, ) -> serde_json::Value { todo!() } From 28649fb71d4a1bbf598d81806735c73a1fdfcf96 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 15 Aug 2023 18:36:23 -0400 Subject: [PATCH 233/326] Update channel context menu --- crates/collab_ui/src/collab_panel.rs | 11 +++++++---- styles/src/style_tree/context_menu.ts | 12 +----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 498b278abd..ce4ffc8f6b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1834,10 +1834,13 @@ impl CollabPanel { }, vec![ ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), - ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }), - ContextMenuItem::action("Manage members", ManageMembers { channel_id }), - ContextMenuItem::action("Invite members", InviteMembers { channel_id }), - ContextMenuItem::action("Rename Channel", RenameChannel { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Rename", RenameChannel { channel_id }), + ContextMenuItem::action("Manage", ManageMembers { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Delete", RemoveChannel { channel_id }), ], cx, ); diff --git a/styles/src/style_tree/context_menu.ts b/styles/src/style_tree/context_menu.ts index d4266a71fe..84688c0971 100644 --- a/styles/src/style_tree/context_menu.ts +++ b/styles/src/style_tree/context_menu.ts @@ -19,7 +19,7 @@ export default function context_menu(): any { icon_width: 14, padding: { left: 6, right: 6, top: 2, bottom: 2 }, corner_radius: 6, - label: text(theme.middle, "sans", { size: "sm" }), + label: text(theme.middle, "ui_sans", { size: "sm" }), keystroke: { ...text(theme.middle, "sans", "variant", { size: "sm", @@ -31,16 +31,6 @@ export default function context_menu(): any { state: { hovered: { background: background(theme.middle, "hovered"), - label: text(theme.middle, "sans", "hovered", { - size: "sm", - }), - keystroke: { - ...text(theme.middle, "sans", "hovered", { - size: "sm", - weight: "bold", - }), - padding: { left: 3, right: 3 }, - }, }, clicked: { background: background(theme.middle, "pressed"), From a56747af8c6ea205ade7f2c99431d8e55ac89f6b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 15 Aug 2023 18:36:30 -0400 Subject: [PATCH 234/326] Update assistant status bar icon --- assets/icons/ai.svg | 32 ++++++++++++++------------------ crates/ai/src/assistant.rs | 2 +- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg index fa046c6050..5b3faaa9cc 100644 --- a/assets/icons/ai.svg +++ b/assets/icons/ai.svg @@ -1,27 +1,23 @@ - - + + - + - - - - - - - - - - - - - - + + + + + + + + + + - + diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 9dc17e2ec5..e0fe41aebe 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -784,7 +784,7 @@ impl Panel for AssistantPanel { fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { settings::get::(cx) .button - .then(|| "icons/robot_14.svg") + .then(|| "icons/ai.svg") } fn icon_tooltip(&self) -> (String, Option>) { From 7d3ffae47d3753fed1d973811dcc64a81e6676b2 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 15:44:10 -0700 Subject: [PATCH 235/326] move component into gpui --- crates/gpui/examples/components.rs | 96 ++------------------------- crates/gpui/src/elements.rs | 7 +- crates/gpui/src/elements/component.rs | 87 ++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 95 deletions(-) create mode 100644 crates/gpui/src/elements/component.rs diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index 09a732c6c1..cf695ea834 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -1,9 +1,8 @@ use button_component::Button; -use component::Component; use gpui::{ color::Color, - elements::{ContainerStyle, Flex, Label, ParentElement}, + elements::{Component, ContainerStyle, Flex, Label, ParentElement}, fonts::{self, TextStyle}, platform::WindowOptions, AnyElement, App, Element, Entity, View, ViewContext, @@ -115,12 +114,12 @@ mod theme { // Component creation: mod toggleable_button { use gpui::{ - elements::{ContainerStyle, LabelStyle}, + elements::{Component, ContainerStyle, LabelStyle}, scene::MouseClick, EventContext, View, }; - use crate::{button_component::Button, component::Component, theme::Toggleable}; + use crate::{button_component::Button, theme::Toggleable}; pub struct ToggleableButton { active: bool, @@ -172,14 +171,12 @@ mod toggleable_button { mod button_component { use gpui::{ - elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler}, + elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, platform::MouseButton, scene::MouseClick, AnyElement, Element, EventContext, TypeTag, View, ViewContext, }; - use crate::component::Component; - type ClickHandler = Box)>; pub struct Button { @@ -238,88 +235,3 @@ mod button_component { } } } - -mod component { - - use gpui::{AnyElement, Element, View, ViewContext}; - use pathfinder_geometry::vector::Vector2F; - - pub trait Component { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - - fn into_element(self) -> ComponentAdapter - where - Self: Sized, - { - ComponentAdapter::new(self) - } - } - - pub struct ComponentAdapter { - component: Option, - phantom: std::marker::PhantomData, - } - - impl ComponentAdapter { - pub fn new(e: E) -> Self { - Self { - component: Some(e), - phantom: std::marker::PhantomData, - } - } - } - - impl + 'static> Element for ComponentAdapter { - type LayoutState = AnyElement; - - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - view: &mut V, - cx: &mut gpui::LayoutContext, - ) -> (Vector2F, Self::LayoutState) { - let component = self.component.take().unwrap(); - let mut element = component.render(view, cx.view_context()); - let constraint = element.layout(constraint, view, cx); - (constraint, element) - } - - fn paint( - &mut self, - scene: &mut gpui::SceneBuilder, - bounds: gpui::geometry::rect::RectF, - visible_bounds: gpui::geometry::rect::RectF, - layout: &mut Self::LayoutState, - view: &mut V, - cx: &mut gpui::PaintContext, - ) -> Self::PaintState { - layout.paint(scene, bounds.origin(), visible_bounds, view, cx) - } - - fn rect_for_text_range( - &self, - _: std::ops::Range, - _: gpui::geometry::rect::RectF, - _: gpui::geometry::rect::RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> Option { - todo!() - } - - fn debug( - &self, - _: gpui::geometry::rect::RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> serde_json::Value { - todo!() - } - } -} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 16c750ea8e..35ecf0545a 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -1,6 +1,7 @@ mod align; mod canvas; mod clipped; +mod component; mod constrained_box; mod container; mod empty; @@ -21,9 +22,9 @@ mod tooltip; mod uniform_list; pub use self::{ - align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*, - keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*, - stack::*, svg::*, text::*, tooltip::*, uniform_list::*, + align::*, canvas::*, component::*, constrained_box::*, container::*, empty::*, flex::*, + hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, + resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*, }; pub use crate::window::ChildView; diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs new file mode 100644 index 0000000000..1c4359e2c3 --- /dev/null +++ b/crates/gpui/src/elements/component.rs @@ -0,0 +1,87 @@ +use std::marker::PhantomData; + +use pathfinder_geometry::{rect::RectF, vector::Vector2F}; + +use crate::{ + AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, +}; + +pub trait Component { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + + fn into_element(self) -> ComponentAdapter + where + Self: Sized, + { + ComponentAdapter::new(self) + } +} + +pub struct ComponentAdapter { + component: Option, + phantom: PhantomData, +} + +impl ComponentAdapter { + pub fn new(e: E) -> Self { + Self { + component: Some(e), + phantom: PhantomData, + } + } +} + +impl + 'static> Element for ComponentAdapter { + type LayoutState = AnyElement; + + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + view: &mut V, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let component = self.component.take().unwrap(); + let mut element = component.render(view, cx.view_context()); + let constraint = element.layout(constraint, view, cx); + (constraint, element) + } + + fn paint( + &mut self, + scene: &mut SceneBuilder, + bounds: RectF, + visible_bounds: RectF, + layout: &mut Self::LayoutState, + view: &mut V, + cx: &mut PaintContext, + ) -> Self::PaintState { + layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: RectF, + _: RectF, + element: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &ViewContext, + ) -> Option { + element.rect_for_text_range(range_utf16, view, cx) + } + + fn debug( + &self, + _: RectF, + element: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &ViewContext, + ) -> serde_json::Value { + element.debug(view, cx) + } +} From 706227701ec284ff926115d284381ba4cc1be7fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Aug 2023 16:14:24 -0700 Subject: [PATCH 236/326] Keep collab panel focused after deleting a channel --- crates/collab_ui/src/collab_panel.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ce4ffc8f6b..f113f12f9d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2167,7 +2167,7 @@ impl CollabPanel { let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); let window = cx.window(); - cx.spawn(|_, mut cx| async move { + cx.spawn(|this, mut cx| async move { if answer.next().await == Some(0) { if let Err(e) = channel_store .update(&mut cx, |channels, _| channels.remove_channel(channel_id)) @@ -2180,6 +2180,7 @@ impl CollabPanel { &mut cx, ); } + this.update(&mut cx, |_, cx| cx.focus_self()).ok(); } }) .detach(); From 0524abf11478b0a86610fd6c3b9a77eab1f99a50 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 15 Aug 2023 23:19:11 -0700 Subject: [PATCH 237/326] Lazily initialize and destroy the audio handle state on call initiation and end --- crates/audio/src/audio.rs | 40 ++++++++++++++++++++++++++------------- crates/call/src/call.rs | 2 ++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index 233b0f62aa..d80fb6738f 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -39,29 +39,43 @@ pub struct Audio { impl Audio { pub fn new() -> Self { - let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); - Self { - _output_stream, - output_handle, + _output_stream: None, + output_handle: None, } } - pub fn play_sound(sound: Sound, cx: &AppContext) { + fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { + if self.output_handle.is_none() { + let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); + self.output_handle = output_handle; + self._output_stream = _output_stream; + } + + self.output_handle.as_ref() + } + + pub fn play_sound(sound: Sound, cx: &mut AppContext) { if !cx.has_global::() { return; } - let this = cx.global::(); + cx.update_global::(|this, cx| { + let output_handle = this.ensure_output_exists()?; + let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; + output_handle.play_raw(source).log_err()?; + Some(()) + }); + } - let Some(output_handle) = this.output_handle.as_ref() else { + pub fn end_call(cx: &mut AppContext) { + if !cx.has_global::() { return; - }; + } - let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else { - return; - }; - - output_handle.play_raw(source).log_err(); + cx.update_global::(|this, _| { + this._output_stream.take(); + this.output_handle.take(); + }); } } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 33ba7a2ab9..3ac29bfc85 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -5,6 +5,7 @@ pub mod room; use std::sync::Arc; use anyhow::{anyhow, Result}; +use audio::Audio; use call_settings::CallSettings; use client::{ proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, @@ -309,6 +310,7 @@ impl ActiveCall { pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { cx.notify(); self.report_call_event("hang up", cx); + Audio::end_call(cx); if let Some((room, _)) = self.room.take() { room.update(cx, |room, cx| room.leave(cx)) } else { From 1c4be24fb73609f3583d1d3772399f86304a2f77 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 10:19:20 +0300 Subject: [PATCH 238/326] Move gpui derives tests into gpui crate to avoid dependency cycles `cargo run` on Zed project leads to rust-analyzer evantually emitting `[ERROR project_model::workspace] cyclic deps: gpui_macros(Idx::(269)) -> gpui(Idx::(264)), alternative path: gpui(Idx::(264)) -> gpui_macros(Idx::(269))` error after loading the project. The PR fixes this by moving away the test to the "root" project. --- Cargo.lock | 1 - crates/{gpui_macros => gpui}/tests/test.rs | 0 crates/gpui_macros/Cargo.toml | 3 --- 3 files changed, 4 deletions(-) rename crates/{gpui_macros => gpui}/tests/test.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 1ff9981a6a..a0be9756bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,7 +3172,6 @@ dependencies = [ name = "gpui_macros" version = "0.1.0" dependencies = [ - "gpui", "proc-macro2", "quote", "syn 1.0.109", diff --git a/crates/gpui_macros/tests/test.rs b/crates/gpui/tests/test.rs similarity index 100% rename from crates/gpui_macros/tests/test.rs rename to crates/gpui/tests/test.rs diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index 9ff340299b..9d1d232c95 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -13,6 +13,3 @@ doctest = false syn = "1.0" quote = "1.0" proc-macro2 = "1.0" - -[dev-dependencies] -gpui = { path = "../gpui" } From 11ecb7b6048f6bc97f49870fb56a19c0d29021b9 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 11:35:09 +0100 Subject: [PATCH 239/326] reorganize search bar, enable filters and disable select all during invalid states Co-authored-by: Piotr --- crates/search/src/buffer_search.rs | 25 +-- crates/search/src/project_search.rs | 235 ++++++++++++---------------- crates/theme/src/theme.rs | 2 +- styles/src/style_tree/search.ts | 71 ++++++--- 4 files changed, 162 insertions(+), 171 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index cbb68ba99f..32e6c93f4a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -222,17 +222,11 @@ impl View for BufferSearchBar { let icon_style = theme.search.editor_icon.clone(); let side_column_min_width = 165.; - let button_height = 32.; let nav_column = Flex::row() - .with_child( - Flex::row() - .with_children(match_count) - .constrained() - .with_min_width(100.), - ) + .with_child(self.render_action_button("Select All", cx)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(self.render_action_button("Select All", cx)) + .with_child(Flex::row().with_children(match_count)) .constrained() .with_height(theme.search.search_bar_row_height); @@ -493,11 +487,20 @@ impl BufferSearchBar { let tooltip = "Select All Matches"; let tooltip_style = theme::current(cx).tooltip.clone(); let action_type_id = 0_usize; - + let has_matches = self.active_match_index.is_some(); + let cursor_style = if has_matches { + CursorStyle::PointingHand + } else { + CursorStyle::default() + }; enum ActionButton {} MouseEventHandler::::new(action_type_id, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.action_button.style_for(state); + let style = theme + .search + .action_button + .in_state(has_matches) + .style_for(state); Label::new(icon, style.text.clone()) .aligned() .contained() @@ -506,7 +509,7 @@ impl BufferSearchBar { .on_click(MouseButton::Left, move |_, this, cx| { this.select_all_matches(&SelectAllMatches, cx) }) - .with_cursor_style(CursorStyle::PointingHand) + .with_cursor_style(cursor_style) .with_tooltip::( action_type_id, tooltip.to_string(), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 338e52eedd..0b2c06f4b8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1392,27 +1392,7 @@ impl View for ProjectSearchBar { } else { theme.search.editor.input.container }; - let include_container_style = - if search.panels_with_errors.contains(&InputPanel::Include) { - theme.search.invalid_include_exclude_editor - } else { - theme.search.include_exclude_editor.input.container - }; - let exclude_container_style = - if search.panels_with_errors.contains(&InputPanel::Exclude) { - theme.search.invalid_include_exclude_editor - } else { - theme.search.include_exclude_editor.input.container - }; - let included_files_view = ChildView::new(&search.included_files_editor, cx) - .aligned() - .left() - .flex(1.0, true); - let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) - .aligned() - .right() - .flex(1.0, true); let row_spacing = theme.workspace.toolbar.container.padding.bottom; let search = _search.read(cx); let filter_button = render_option_button_icon( @@ -1455,19 +1435,15 @@ impl View for ProjectSearchBar { let search = _search.read(cx); let icon_style = theme.search.editor_icon.clone(); + + // Editor Functionality let query = Flex::row() .with_child( Svg::for_style(icon_style.icon) .contained() - .with_style(icon_style.container) - .constrained(), - ) - .with_child( - ChildView::new(&search.query_editor, cx) - .constrained() - .flex(1., true) - .into_any(), + .with_style(icon_style.container), ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) .with_child( Flex::row() .with_child(filter_button) @@ -1477,10 +1453,70 @@ impl View for ProjectSearchBar { .contained(), ) .align_children_center() - .aligned() - .left() .flex(1., true); + let search = _search.read(cx); + + let include_container_style = + if search.panels_with_errors.contains(&InputPanel::Include) { + theme.search.invalid_include_exclude_editor + } else { + theme.search.include_exclude_editor.input.container + }; + + let exclude_container_style = + if search.panels_with_errors.contains(&InputPanel::Exclude) { + theme.search.invalid_include_exclude_editor + } else { + theme.search.include_exclude_editor.input.container + }; + + let included_files_view = ChildView::new(&search.included_files_editor, cx) + .contained() + .flex(1.0, true); + let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) + .contained() + .flex(1.0, true); + let filters = search.filters_enabled.then(|| { + Flex::row() + .with_child( + included_files_view + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .with_min_width(theme.search.include_exclude_editor.min_width) + .with_max_width(theme.search.include_exclude_editor.max_width), + ) + .with_child( + excluded_files_view + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .with_min_width(theme.search.include_exclude_editor.min_width) + .with_max_width(theme.search.include_exclude_editor.max_width), + ) + .contained() + .with_padding_top(3.) + }); + + let editor_column = Flex::column() + .with_child( + query + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), + ) + .with_children(filters) + .contained() + .with_background_color(gpui::color::Color::blue()) + .flex(1., false); + let matches = search.active_match_index.map(|match_ix| { Label::new( format!( @@ -1492,33 +1528,9 @@ impl View for ProjectSearchBar { ) .contained() .with_style(theme.search.match_index.container) + .aligned() }); - let filters = search.filters_enabled.then(|| { - Flex::row() - .with_child( - Flex::row() - .with_child(included_files_view) - .contained() - .with_style(include_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width) - .flex(1., false), - ) - .with_child( - Flex::row() - .with_child(excluded_files_view) - .contained() - .with_style(exclude_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width) - .flex(1., false), - ) - }); let search_button_for_mode = |mode, cx: &mut ViewContext| { let is_active = if let Some(search) = self.active_project_search.as_ref() { let search = search.read(cx); @@ -1551,95 +1563,52 @@ impl View for ProjectSearchBar { cx, ) }; - let nav_column = Flex::column() + + let nav_column = Flex::row() + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .with_child(Flex::row().with_children(matches)) + .constrained() + .with_height(theme.search.search_bar_row_height) + .contained() + .with_background_color(gpui::color::Color::red()); + + let side_column_min_width = 200.; + let mode_column = Flex::row() .with_child( Flex::row() - .align_children_center() - .with_child( - Flex::row().align_children_center() - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .aligned(), - ) - .with_children(matches) - .aligned() - ) - .flex(1., true); - let editor_column = Flex::column() - .align_children_center() - .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child(query) - .contained() - .with_style(query_container_style) - .aligned() - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_max_height(theme.search.search_bar_row_height) - .flex(1., false), - ) + .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_children(semantic_index) + .with_child(search_button_for_mode(SearchMode::Regex, cx)) .contained() - .with_margin_bottom(row_spacing), + .with_style(theme.search.modes_container), ) - .with_children(filters) - .contained() + .with_child(super::search_bar::render_close_button( + "Dismiss Project Search", + &theme.search, + cx, + |_, this, cx| { + if let Some(search) = this.active_project_search.as_mut() { + search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) + } + }, + None, + )) + .constrained() + .with_height(theme.search.search_bar_row_height) .aligned() + .right() .top() - .flex(1., false); - let mode_column = Flex::column() - .with_child( - Flex::row() - .align_children_center() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .aligned() - .left() - .contained() - .with_style(theme.search.modes_container), - ) - .with_child( - super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - ) - .aligned() - .right(), - ) - .constrained() - .with_height(theme.search.search_bar_row_height) - .aligned() - .right() - .top() - .flex(1., true), - ) - .with_children( - _search - .read(cx) - .filters_enabled - .then(|| Flex::row().flex(1., true)), - ) - .contained() - .flex(1., true); + .constrained() + .with_min_width(side_column_min_width) + .flex_float(); + Flex::row() - .with_child(nav_column) .with_child(editor_column) + .with_child(nav_column) .with_child(mode_column) .contained() .with_style(theme.search.container) - .flex_float() .into_any_named("project search") } else { Empty::new().into_any() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 91d7eeb5dc..5ee11a4d94 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -380,7 +380,7 @@ pub struct Search { pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, pub option_button: Toggleable>, - pub action_button: Interactive, + pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, pub major_results_status: TextStyle, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 24d0930863..05392ce48d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -89,31 +89,52 @@ export default function search(): any { }, }, }), - action_button: interactive({ - base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - corner_radius: 6, - border: border(theme.highest, "on"), - padding: { - // bottom: 2, - left: 10, - right: 10, - // top: 2, + action_button: toggleable({ + base: interactive({ + base: { + ...text(theme.highest, "mono", "disabled"), + background: background(theme.highest, "disabled"), + corner_radius: 6, + border: border(theme.highest, "disabled"), + padding: { + // bottom: 2, + left: 10, + right: 10, + // top: 2, + }, }, - }, + state: { + hovered: {} + }, + }), state: { - hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), - }, - clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), - }, - }, + active: interactive({ + base: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 6, + border: border(theme.highest, "on"), + padding: { + // bottom: 2, + left: 10, + right: 10, + // top: 2, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }) + } }), editor, invalid_editor: { @@ -128,7 +149,7 @@ export default function search(): any { match_index: { ...text(theme.highest, "mono", "variant"), padding: { - left: 6, + left: 9, }, }, option_button_group: { @@ -208,10 +229,8 @@ export default function search(): any { }, padding: { - // bottom: 4, left: 10, right: 10, - // top: 5, }, corner_radius: 6, }, From 9bf227b884a35098670e26b988b52b8a282dfa1e Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 12:17:59 +0100 Subject: [PATCH 240/326] remove regex keymap, and made spacing consistent between search objects Co-authored-by: Piotr --- crates/search/src/project_search.rs | 10 +++------- styles/src/style_tree/search.ts | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0b2c06f4b8..fb0e71b1ed 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -64,7 +64,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::cycle_mode); cx.add_action(ProjectSearchBar::next_history_query); cx.add_action(ProjectSearchBar::previous_history_query); - cx.add_action(ProjectSearchBar::activate_regex_mode); + // cx.add_action(ProjectSearchBar::activate_regex_mode); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); @@ -1498,7 +1498,7 @@ impl View for ProjectSearchBar { .with_max_width(theme.search.include_exclude_editor.max_width), ) .contained() - .with_padding_top(3.) + .with_padding_top(theme.workspace.toolbar.container.padding.bottom) }); let editor_column = Flex::column() @@ -1513,8 +1513,6 @@ impl View for ProjectSearchBar { .flex(1., false), ) .with_children(filters) - .contained() - .with_background_color(gpui::color::Color::blue()) .flex(1., false); let matches = search.active_match_index.map(|match_ix| { @@ -1569,9 +1567,7 @@ impl View for ProjectSearchBar { .with_child(nav_button_for_direction(">", Direction::Next, cx)) .with_child(Flex::row().with_children(matches)) .constrained() - .with_height(theme.search.search_bar_row_height) - .contained() - .with_background_color(gpui::color::Color::red()); + .with_height(theme.search.search_bar_row_height); let side_column_min_width = 200.; let mode_column = Flex::row() diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 05392ce48d..abb27f39e8 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -102,6 +102,9 @@ export default function search(): any { right: 10, // top: 2, }, + margin: { + right: 9, + } }, state: { hovered: {} @@ -112,14 +115,7 @@ export default function search(): any { base: { ...text(theme.highest, "mono", "on"), background: background(theme.highest, "on"), - corner_radius: 6, border: border(theme.highest, "on"), - padding: { - // bottom: 2, - left: 10, - right: 10, - // top: 2, - }, }, state: { hovered: { From a59535efa126572330aa73b628dfc12c9e7c193a Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 12:55:32 +0100 Subject: [PATCH 241/326] remove redundant and unneeded styling Co-authored-by: Piotr --- crates/search/src/buffer_search.rs | 3 --- crates/search/src/project_search.rs | 11 +++-------- styles/src/style_tree/search.ts | 2 -- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index daf92151fc..6fe8712ee0 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -221,7 +221,6 @@ impl View for BufferSearchBar { }; let icon_style = theme.search.editor_icon.clone(); - let side_column_min_width = 165.; let nav_column = Flex::row() .with_child(self.render_action_button("Select All", cx)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) @@ -289,8 +288,6 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height) .aligned() .right() - .constrained() - .with_min_width(side_column_min_width) .flex_float(); Flex::row() .with_child(editor_column) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d0a1aa95c1..884f56bda5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -327,10 +327,8 @@ impl View for ProjectSearchView { let semantic_status = if let Some(semantic) = &self.semantic_state { if semantic.outstanding_file_count > 0 { - let dots_count = semantic.outstanding_file_count % 3 + 1; - let dots: String = std::iter::repeat('.').take(dots_count).collect(); format!( - "Indexing: {} of {}{dots}", + "Indexing: {} of {}...", semantic.file_count - semantic.outstanding_file_count, semantic.file_count ) @@ -1473,10 +1471,10 @@ impl View for ProjectSearchBar { let included_files_view = ChildView::new(&search.included_files_editor, cx) .contained() - .flex(1.0, true); + .flex(1., true); let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) .contained() - .flex(1.0, true); + .flex(1., true); let filters = search.filters_enabled.then(|| { Flex::row() .with_child( @@ -1569,7 +1567,6 @@ impl View for ProjectSearchBar { .constrained() .with_height(theme.search.search_bar_row_height); - let side_column_min_width = 200.; let mode_column = Flex::row() .with_child( Flex::row() @@ -1595,8 +1592,6 @@ impl View for ProjectSearchBar { .aligned() .right() .top() - .constrained() - .with_min_width(side_column_min_width) .flex_float(); Flex::row() diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index abb27f39e8..4dfa9624b4 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -152,8 +152,6 @@ export default function search(): any { padding: { left: 12, right: 12, - // top: 3, - // bottom: 3, }, }, include_exclude_inputs: { From 80c779b95e610ade8258cf5dcf28fa4f8ac7cd2c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 15:06:42 +0300 Subject: [PATCH 242/326] Focus terminal view on mouse click in terminal Before, terminal view focused the parent (pane) instead and, if terminal's search bar was open and focused, pane transferred the focus back --- crates/terminal_view/src/terminal_element.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 232d3c5535..0ac189db0b 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -400,7 +400,8 @@ impl TerminalElement { region = region // Start selections .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { - cx.focus_parent(); + let terminal_view = cx.handle(); + cx.focus(&terminal_view); v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); if let Some(conn_handle) = connection.upgrade(cx) { conn_handle.update(cx, |terminal, cx| { From 6f78a1633dd0ceefc75f17bb1b7f4882431b8e33 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 13:51:41 +0100 Subject: [PATCH 243/326] fix editor height in buffer search, but the dancing is back Co-authored-by: Piotr --- crates/search/src/buffer_search.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 6fe8712ee0..0637191f97 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -263,6 +263,7 @@ impl View for BufferSearchBar { .constrained() .with_min_width(theme.search.editor.min_width) .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) .flex(1., false), ) .contained() @@ -295,6 +296,7 @@ impl View for BufferSearchBar { .with_child(mode_column) .contained() .with_style(theme.search.container) + .aligned() .into_any_named("search bar") } } From 2a7df106e124f614b5de68052d6b5014c47df8af Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 14:01:06 +0100 Subject: [PATCH 244/326] adjusted icon sizes downwards Co-authored-by: Piotr --- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 3 ++- crates/search/src/search_bar.rs | 6 +++++- crates/theme/src/theme.rs | 2 +- styles/src/style_tree/search.ts | 14 +++++++++----- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 0637191f97..3e13f49181 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -250,7 +250,7 @@ impl View for BufferSearchBar { SearchOptions::WHOLE_WORD, cx, )) - .flex(1., true) + .flex_float() .contained(), ) .align_children_center() diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 884f56bda5..323c33bc56 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1447,7 +1447,8 @@ impl View for ProjectSearchBar { .with_child(filter_button) .with_children(case_sensitive) .with_children(whole_word) - .flex(1., true) + .flex(1., false) + .constrained() .contained(), ) .align_children_center() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 88d4675102..f227ced0d4 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -185,11 +185,15 @@ pub(crate) fn render_option_button_icon( .in_state(is_active) .style_for(state); Svg::new(icon) - .with_color(style.text.color.clone()) + .with_color(style.color.clone()) + .constrained() + .with_width(style.icon_width) .contained() .with_style(style.container) + // .aligned() .constrained() .with_height(theme.search.option_button_height) + .with_width(style.button_width) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5ee11a4d94..df6dbe9f55 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -379,7 +379,7 @@ pub struct Search { pub include_exclude_editor: FindEditor, pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, - pub option_button: Toggleable>, + pub option_button: Toggleable>, pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4dfa9624b4..b5259886b9 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,6 +2,7 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +import { toggleable_icon_button } from "../component/icon_button" export default function search(): any { const theme = useTheme() @@ -43,7 +44,9 @@ export default function search(): any { option_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "variant"), + icon_width: 14, + button_width: 32, + color: foreground(theme.highest, "variant"), background: background(theme.highest, "on"), corner_radius: 2, margin: { right: 2 }, @@ -53,6 +56,8 @@ export default function search(): any { padding: { left: 4, right: 4, + top: 4, + bottom: 4, }, }, state: { @@ -75,6 +80,9 @@ export default function search(): any { state: { active: { default: { + icon_width: 14, + button_width: 32, + color: foreground(theme.highest, "variant"), background: background(theme.highest, "accent"), border: border(theme.highest, "accent"), }, @@ -272,10 +280,8 @@ export default function search(): any { }, padding: { - bottom: 0, left: 10, right: 10, - top: 0, }, }, state: { @@ -294,10 +300,8 @@ export default function search(): any { }, padding: { - bottom: 0, left: 10, right: 10, - top: 0, }, }, state: { From 6e3e61ec95607c8e83866f9e2594a931e7ff31ea Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 14:01:53 +0100 Subject: [PATCH 245/326] removed commented out code --- crates/search/src/search_bar.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index f227ced0d4..7d3c5261ea 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -190,7 +190,6 @@ pub(crate) fn render_option_button_icon( .with_width(style.icon_width) .contained() .with_style(style.container) - // .aligned() .constrained() .with_height(theme.search.option_button_height) .with_width(style.button_width) From aeda5d984246ef63c3f855247de9f36d7f933c35 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 15:50:54 +0100 Subject: [PATCH 246/326] fix semantic search panic which is created via incompatible build_search_query path Co-authored-by: Piotr --- crates/project/src/search.rs | 73 +++++++++++++---------- crates/search/src/project_search.rs | 59 +++++++++--------- crates/terminal_view/src/terminal_view.rs | 6 +- styles/src/style_tree/search.ts | 1 - 4 files changed, 75 insertions(+), 64 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 71a0b70b81..08ff803598 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -13,24 +13,39 @@ use std::{ sync::Arc, }; +#[derive(Clone, Debug)] +pub struct SearchInputs { + query: Arc, + files_to_include: Vec, + files_to_exclude: Vec, +} + +impl SearchInputs { + pub fn as_str(&self) -> &str { + self.query.as_ref() + } + pub fn files_to_include(&self) -> &[PathMatcher] { + &self.files_to_include + } + pub fn files_to_exclude(&self) -> &[PathMatcher] { + &self.files_to_exclude + } +} #[derive(Clone, Debug)] pub enum SearchQuery { Text { search: Arc>, - query: Arc, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + inner: SearchInputs, }, Regex { regex: Regex, - query: Arc, + multiline: bool, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + inner: SearchInputs, }, } @@ -72,13 +87,16 @@ impl SearchQuery { .auto_configure(&[&query]) .ascii_case_insensitive(!case_sensitive) .build(&[&query]); + let inner = SearchInputs { + query: query.into(), + files_to_exclude, + files_to_include, + }; Self::Text { search: Arc::new(search), - query: Arc::from(query), whole_word, case_sensitive, - files_to_include, - files_to_exclude, + inner, } } @@ -104,14 +122,17 @@ impl SearchQuery { .case_insensitive(!case_sensitive) .multi_line(multiline) .build()?; + let inner = SearchInputs { + query: initial_query, + files_to_exclude, + files_to_include, + }; Ok(Self::Regex { regex, - query: initial_query, multiline, whole_word, case_sensitive, - files_to_include, - files_to_exclude, + inner, }) } @@ -267,10 +288,7 @@ impl SearchQuery { } pub fn as_str(&self) -> &str { - match self { - Self::Text { query, .. } => query.as_ref(), - Self::Regex { query, .. } => query.as_ref(), - } + self.as_inner().as_str() } pub fn whole_word(&self) -> bool { @@ -292,25 +310,11 @@ impl SearchQuery { } pub fn files_to_include(&self) -> &[PathMatcher] { - match self { - Self::Text { - files_to_include, .. - } => files_to_include, - Self::Regex { - files_to_include, .. - } => files_to_include, - } + self.as_inner().files_to_include() } pub fn files_to_exclude(&self) -> &[PathMatcher] { - match self { - Self::Text { - files_to_exclude, .. - } => files_to_exclude, - Self::Regex { - files_to_exclude, .. - } => files_to_exclude, - } + self.as_inner().files_to_exclude() } pub fn file_matches(&self, file_path: Option<&Path>) -> bool { @@ -329,6 +333,11 @@ impl SearchQuery { None => self.files_to_include().is_empty(), } } + pub fn as_inner(&self) -> &SearchInputs { + match self { + Self::Regex { inner, .. } | Self::Text { inner, .. } => inner, + } + } } fn deserialize_path_matches(glob_set: &str) -> anyhow::Result> { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 323c33bc56..6535c257e2 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -24,7 +24,7 @@ use gpui::{ use menu::Confirm; use postage::stream::Stream; use project::{ - search::{PathMatcher, SearchQuery}, + search::{PathMatcher, SearchInputs, SearchQuery}, Entry, Project, }; use semantic_index::SemanticIndex; @@ -177,10 +177,12 @@ impl ProjectSearch { } fn kill_search(&mut self) { + dbg!("Killing search"); self.active_query = None; self.match_ranges.clear(); self.pending_search = None; self.no_results = None; + dbg!("Killed search"); } fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { @@ -226,22 +228,22 @@ impl ProjectSearch { cx.notify(); } - fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { let search = SemanticIndex::global(cx).map(|index| { index.update(cx, |semantic_index, cx| { semantic_index.search_project( self.project.clone(), - query.as_str().to_owned(), + inputs.as_str().to_owned(), 10, - query.files_to_include().to_vec(), - query.files_to_exclude().to_vec(), + inputs.files_to_include().to_vec(), + inputs.files_to_exclude().to_vec(), cx, ) }) }); self.search_id += 1; self.match_ranges.clear(); - self.search_history.add(query.as_str().to_string()); + self.search_history.add(inputs.as_str().to_string()); self.no_results = Some(true); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; @@ -682,6 +684,7 @@ impl ProjectSearchView { fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { let previous_mode = self.current_mode; + log::error!("Going from {previous_mode:?} to {:?}", mode); if previous_mode == mode { return; } @@ -690,6 +693,7 @@ impl ProjectSearchView { match mode { SearchMode::Semantic => { + dbg!("Matched on Semantic"); let has_permission = self.semantic_permissioned(cx); self.active_match_index = None; cx.spawn(|this, mut cx| async move { @@ -947,7 +951,7 @@ impl ProjectSearchView { if let Some(query) = self.build_search_query(cx) { self.model - .update(cx, |model, cx| model.semantic_search(query, cx)); + .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx)); } } } @@ -986,33 +990,34 @@ impl ProjectSearchView { return None; } }; - if self.current_mode == SearchMode::Regex { - match SearchQuery::regex( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - included_files, - excluded_files, - ) { - Ok(query) => { - self.panels_with_errors.remove(&InputPanel::Query); - Some(query) - } - Err(_e) => { - self.panels_with_errors.insert(InputPanel::Query); - cx.notify(); - None + let current_mode = self.current_mode; + match current_mode { + SearchMode::Regex => { + match SearchQuery::regex( + text, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + included_files, + excluded_files, + ) { + Ok(query) => { + self.panels_with_errors.remove(&InputPanel::Query); + Some(query) + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Query); + cx.notify(); + None + } } } - } else { - debug_assert_ne!(self.current_mode, SearchMode::Semantic); - Some(SearchQuery::text( + _ => Some(SearchQuery::text( text, self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::CASE_SENSITIVE), included_files, excluded_files, - )) + )), } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index b48597d901..4f891e678e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -483,10 +483,8 @@ fn possible_open_targets( } pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { - let searcher = match query { - project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query), - project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query), - }; + let query = query.as_str(); + let searcher = RegexSearch::new(&query); searcher.ok() } diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index b5259886b9..4c0df69804 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 { toggleable_icon_button } from "../component/icon_button" export default function search(): any { const theme = useTheme() From c99b530968a81856508c8a442af976d2fbfe9c25 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 16 Aug 2023 16:13:21 +0100 Subject: [PATCH 247/326] remove kill search, and restart search immediately upon cycle mode Co-authored-by: Piotr --- crates/search/src/project_search.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6535c257e2..065b8d0880 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -176,15 +176,6 @@ impl ProjectSearch { }) } - fn kill_search(&mut self) { - dbg!("Killing search"); - self.active_query = None; - self.match_ranges.clear(); - self.pending_search = None; - self.no_results = None; - dbg!("Killed search"); - } - fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { let search = self .project @@ -688,7 +679,7 @@ impl ProjectSearchView { if previous_mode == mode { return; } - self.model.update(cx, |model, _| model.kill_search()); + self.current_mode = mode; match mode { @@ -746,6 +737,10 @@ impl ProjectSearchView { self.active_match_index = None; } } + + if let Some(query) = self.build_search_query(cx) { + self.search(cx); + }; cx.notify(); } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { From 6c15636ccccdad1cd3db0da1efeacd4ef6538011 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 12:38:44 -0400 Subject: [PATCH 248/326] Style cleanup for channels panel --- styles/src/component/button.ts | 122 ++++++++++++++++++++++++-- styles/src/component/icon_button.ts | 36 +++++--- styles/src/component/label_button.ts | 78 ++++++++++++++++ styles/src/component/text_button.ts | 8 +- styles/src/element/index.ts | 4 +- styles/src/element/toggle.ts | 2 +- styles/src/style_tree/collab_panel.ts | 36 ++++---- 7 files changed, 241 insertions(+), 45 deletions(-) create mode 100644 styles/src/component/label_button.ts diff --git a/styles/src/component/button.ts b/styles/src/component/button.ts index ba72851768..3b554ae37a 100644 --- a/styles/src/component/button.ts +++ b/styles/src/component/button.ts @@ -1,6 +1,118 @@ -export const ButtonVariant = { - Default: 'default', - Ghost: 'ghost' -} as const +import { font_sizes, useTheme } from "../common" +import { Layer, Theme } from "../theme" +import { TextStyle, background } from "../style_tree/components" -export type Variant = typeof ButtonVariant[keyof typeof ButtonVariant] +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Button { + export type Options = { + layer: Layer, + background: keyof Theme["lowest"] + color: keyof Theme["lowest"] + variant: Button.Variant + size: Button.Size + shape: Button.Shape + margin: { + top?: number + bottom?: number + left?: number + right?: number + }, + states: { + enabled?: boolean, + hovered?: boolean, + pressed?: boolean, + focused?: boolean, + disabled?: boolean, + } + } + + export type ToggleableOptions = Options & { + active_background: keyof Theme["lowest"] + active_color: keyof Theme["lowest"] + } + + /** Padding added to each side of a Shape.Rectangle button */ + export const RECTANGLE_PADDING = 2 + export const FONT_SIZE = font_sizes.sm + export const ICON_SIZE = 14 + export const CORNER_RADIUS = 6 + + export const variant = { + Default: 'filled', + Outline: 'outline', + Ghost: 'ghost' + } as const + + export type Variant = typeof variant[keyof typeof variant] + + export const shape = { + Rectangle: 'rectangle', + Square: 'square' + } as const + + export type Shape = typeof shape[keyof typeof shape] + + export const size = { + Small: "sm", + Medium: "md" + } as const + + export type Size = typeof size[keyof typeof size] + + export type BaseStyle = { + corder_radius: number + background: string | null + padding: { + top: number + bottom: number + left: number + right: number + }, + margin: Button.Options['margin'] + button_height: number + } + + export type LabelButtonStyle = BaseStyle & TextStyle + // export type IconButtonStyle = ButtonStyle + + export const button_base = ( + options: Partial = { + variant: Button.variant.Default, + shape: Button.shape.Rectangle, + states: { + hovered: true, + pressed: true + } + } + ): BaseStyle => { + const theme = useTheme() + + const layer = options.layer ?? theme.middle + const color = options.color ?? "base" + const background_color = options.variant === Button.variant.Ghost ? null : background(layer, options.background ?? color) + + const m = { + top: options.margin?.top ?? 0, + bottom: options.margin?.bottom ?? 0, + left: options.margin?.left ?? 0, + right: options.margin?.right ?? 0, + } + const size = options.size || Button.size.Medium + const padding = 2 + + const base: BaseStyle = { + background: background_color, + corder_radius: Button.CORNER_RADIUS, + padding: { + top: padding, + bottom: padding, + left: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding, + right: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding + }, + margin: m, + button_height: 16, + } + + return base + } +} diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index ae3fa763e7..1a2d0bcec4 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -1,7 +1,7 @@ import { interactive, toggleable } from "../element" import { background, foreground } from "../style_tree/components" -import { useTheme, Theme } from "../theme" -import { ButtonVariant, Variant } from "./button" +import { useTheme, Theme, Layer } from "../theme" +import { Button } from "./button" export type Margin = { top: number @@ -17,19 +17,24 @@ interface IconButtonOptions { | Theme["highest"] color?: keyof Theme["lowest"] margin?: Partial - variant?: Variant + variant?: Button.Variant + size?: Button.Size } type ToggleableIconButtonOptions = IconButtonOptions & { active_color?: keyof Theme["lowest"] + active_layer?: Layer } -export function icon_button({ color, margin, layer, variant = ButtonVariant.Default }: IconButtonOptions) { +export function icon_button({ color, margin, layer, variant, size }: IconButtonOptions = { + variant: Button.variant.Default, + size: Button.size.Medium, +}) { const theme = useTheme() if (!color) color = "base" - const background_color = variant === ButtonVariant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) const m = { top: margin?.top ?? 0, @@ -38,15 +43,17 @@ export function icon_button({ color, margin, layer, variant = ButtonVariant.Defa right: margin?.right ?? 0, } + const padding = { + top: size === Button.size.Small ? 0 : 2, + bottom: size === Button.size.Small ? 0 : 2, + left: size === Button.size.Small ? 0 : 4, + right: size === Button.size.Small ? 0 : 4, + } + return interactive({ base: { corner_radius: 6, - padding: { - top: 2, - bottom: 2, - left: 4, - right: 4, - }, + padding: padding, margin: m, icon_width: 14, icon_height: 14, @@ -72,17 +79,18 @@ export function icon_button({ color, margin, layer, variant = ButtonVariant.Defa export function toggleable_icon_button( theme: Theme, - { color, active_color, margin, variant }: ToggleableIconButtonOptions + { color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions ) { if (!color) color = "base" return toggleable({ state: { - inactive: icon_button({ color, margin, variant }), + inactive: icon_button({ color, margin, variant, size }), active: icon_button({ color: active_color ? active_color : color, margin, - layer: theme.middle, + layer: active_layer, + size }), }, }) diff --git a/styles/src/component/label_button.ts b/styles/src/component/label_button.ts new file mode 100644 index 0000000000..3f1c54a7f6 --- /dev/null +++ b/styles/src/component/label_button.ts @@ -0,0 +1,78 @@ +import { Interactive, interactive, toggleable, Toggleable } from "../element" +import { TextStyle, background, text } from "../style_tree/components" +import { useTheme } from "../theme" +import { Button } from "./button" + +type LabelButtonStyle = { + corder_radius: number + background: string | null + padding: { + top: number + bottom: number + left: number + right: number + }, + margin: Button.Options['margin'] + button_height: number +} & TextStyle + +/** Styles an Interactive<ContainedText> */ +export function label_button_style( + options: Partial = { + variant: Button.variant.Default, + shape: Button.shape.Rectangle, + states: { + hovered: true, + pressed: true + } + } +): Interactive { + const theme = useTheme() + + const base = Button.button_base(options) + const layer = options.layer ?? theme.middle + const color = options.color ?? "base" + + const default_state = { + ...base, + ...text(layer ?? theme.lowest, "sans", color), + font_size: Button.FONT_SIZE, + } + + return interactive({ + base: default_state, + state: { + hovered: { + background: background(layer, options.background ?? color, "hovered") + }, + clicked: { + background: background(layer, options.background ?? color, "pressed") + } + } + }) +} + +/** Styles an Toggleable<Interactive<ContainedText>> */ +export function toggle_label_button_style( + options: Partial = { + variant: Button.variant.Default, + shape: Button.shape.Rectangle, + states: { + hovered: true, + pressed: true + } + } +): Toggleable> { + const activeOptions = { + ...options, + color: options.active_color || options.color, + background: options.active_background || options.background + } + + return toggleable({ + state: { + inactive: label_button_style(options), + active: label_button_style(activeOptions), + }, + }) +} diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 2be2dd19cb..b911cd5b77 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -6,7 +6,7 @@ import { text, } from "../style_tree/components" import { useTheme, Theme } from "../theme" -import { ButtonVariant, Variant } from "./button" +import { Button } from "./button" import { Margin } from "./icon_button" interface TextButtonOptions { @@ -14,7 +14,7 @@ interface TextButtonOptions { | Theme["lowest"] | Theme["middle"] | Theme["highest"] - variant?: Variant + variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial text_properties?: TextProperties @@ -25,7 +25,7 @@ type ToggleableTextButtonOptions = TextButtonOptions & { } export function text_button({ - variant = ButtonVariant.Default, + variant = Button.variant.Default, color, layer, margin, @@ -34,7 +34,7 @@ export function text_button({ const theme = useTheme() if (!color) color = "base" - const background_color = variant === ButtonVariant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) const text_options: TextProperties = { size: "xs", diff --git a/styles/src/element/index.ts b/styles/src/element/index.ts index 81c911c7bd..d41b4e2cc3 100644 --- a/styles/src/element/index.ts +++ b/styles/src/element/index.ts @@ -1,4 +1,4 @@ import { interactive, Interactive } from "./interactive" -import { toggleable } from "./toggle" +import { toggleable, Toggleable } from "./toggle" -export { interactive, Interactive, toggleable } +export { interactive, Interactive, toggleable, Toggleable } diff --git a/styles/src/element/toggle.ts b/styles/src/element/toggle.ts index c3cde46d65..25217444da 100644 --- a/styles/src/element/toggle.ts +++ b/styles/src/element/toggle.ts @@ -3,7 +3,7 @@ import { DeepPartial } from "utility-types" type ToggleState = "inactive" | "active" -type Toggleable = Record +export type Toggleable = Record export const NO_INACTIVE_OR_BASE_ERROR = "A toggleable object must have an inactive state, or a base property." diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index c65887e17c..61c96ad75a 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -9,7 +9,7 @@ import { interactive, toggleable } from "../element" import { useTheme } from "../theme" import collab_modals from "./collab_modals" import { text_button } from "../component/text_button" -import { toggleable_icon_button } from "../component/icon_button" +import { icon_button, toggleable_icon_button } from "../component/icon_button" import { indicator } from "../component/indicator" export default function contacts_panel(): any { @@ -27,7 +27,7 @@ export default function contacts_panel(): any { color: foreground(layer, "on"), icon_width: 14, button_width: 16, - corner_radius: 8, + corner_radius: 8 } const project_row = { @@ -62,8 +62,9 @@ export default function contacts_panel(): any { } const header_icon_button = toggleable_icon_button(theme, { - layer: theme.middle, variant: "ghost", + size: "sm", + active_layer: theme.lowest, }) const subheader_row = toggleable({ @@ -87,8 +88,8 @@ export default function contacts_panel(): any { state: { active: { default: { - ...text(layer, "ui_sans", "active", { size: "sm" }), - background: background(layer, "active"), + ...text(theme.lowest, "ui_sans", { size: "sm" }), + background: background(theme.lowest), }, clicked: { background: background(layer, "pressed"), @@ -140,8 +141,8 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(layer, "ui_sans", "active", { size: "sm" }), - background: background(layer, "active"), + ...text(theme.lowest, "ui_sans", { size: "sm" }), + background: background(theme.lowest), }, clicked: { background: background(layer, "pressed"), @@ -221,8 +222,8 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(layer, "ui_sans", "active", { size: "sm" }), - background: background(layer, "active"), + ...text(theme.lowest, "ui_sans", { size: "sm" }), + background: background(theme.lowest), }, clicked: { background: background(layer, "pressed"), @@ -271,8 +272,8 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(layer, "ui_sans", "active", { size: "sm" }), - background: background(layer, "active"), + ...text(theme.lowest, "ui_sans", { size: "sm" }), + background: background(theme.lowest), }, clicked: { background: background(layer, "pressed"), @@ -306,13 +307,10 @@ export default function contacts_panel(): any { }, }, contact_button_spacing: NAME_MARGIN, - contact_button: interactive({ - base: { ...contact_button }, - state: { - hovered: { - background: background(layer, "hovered"), - }, - }, + contact_button: icon_button({ + variant: "ghost", + color: "variant", + size: "sm", }), disabled_button: { ...contact_button, @@ -364,7 +362,7 @@ export default function contacts_panel(): any { }), state: { active: { - default: { background: background(layer, "active") }, + default: { background: background(theme.lowest) }, }, }, }), From 7f06191c9fc781096415da80af8c5121a588cbfe Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 16 Aug 2023 10:44:59 -0600 Subject: [PATCH 249/326] Disable autoindent in visual block insert mode --- crates/editor/src/editor.rs | 17 ++++++++++++++--- crates/editor/src/multi_buffer.rs | 1 + crates/vim/src/state.rs | 4 ++++ crates/vim/src/test/neovim_connection.rs | 3 +-- crates/vim/src/vim.rs | 2 ++ crates/vim/src/visual.rs | 4 ++-- 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 256ef2284c..59143e8a39 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -575,6 +575,7 @@ pub struct Editor { searchable: bool, cursor_shape: CursorShape, collapse_matches: bool, + autoindent_mode: Option, workspace: Option<(WeakViewHandle, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, @@ -1409,6 +1410,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, workspace: None, keymap_context_layers: Default::default(), @@ -1587,6 +1589,14 @@ impl Editor { self.input_enabled = input_enabled; } + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } + pub fn set_read_only(&mut self, read_only: bool) { self.read_only = read_only; } @@ -1719,7 +1729,7 @@ impl Editor { } self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx) + buffer.edit(edits, self.autoindent_mode.clone(), cx) }); } @@ -2194,7 +2204,7 @@ impl Editor { drop(snapshot); self.transact(cx, |this, cx| { this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + buffer.edit(edits, this.autoindent_mode.clone(), cx); }); let new_anchor_selections = new_selections.iter().map(|e| &e.0); @@ -2504,6 +2514,7 @@ impl Editor { } pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { + dbg!("insert!"); self.insert_with_autoindent_mode( text, Some(AutoindentMode::Block { @@ -3003,7 +3014,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.edit( ranges.iter().map(|range| (range.clone(), text)), - Some(AutoindentMode::EachLine), + this.autoindent_mode.clone(), cx, ); }); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8417c411f2..df807c8f28 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -364,6 +364,7 @@ impl MultiBuffer { S: ToOffset, T: Into>, { + dbg!("edit", &autoindent_mode); if self.buffers.borrow().is_empty() { return; } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 66aaec02b9..5f146aa690 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -90,6 +90,10 @@ impl VimState { ) } + pub fn should_autoindent(&self) -> bool { + !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock) + } + pub fn clip_at_line_ends(&self) -> bool { match self.mode { Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => false, diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index ddeb26164b..2c7a33909e 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -520,6 +520,5 @@ fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { byte_range }) .collect::>(); - let ret = util::test::generate_marked_text(text, &byte_ranges[..], true); - ret + util::test::generate_marked_text(text, &byte_ranges[..], true); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index df35e951d2..e3f7c7dd10 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -339,6 +339,7 @@ impl Vim { editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); + editor.set_autoindent(state.should_autoindent()); editor.selections.line_mode = matches!(state.mode, Mode::VisualLine); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer, cx); @@ -355,6 +356,7 @@ impl Vim { editor.set_cursor_shape(CursorShape::Bar, cx); editor.set_clip_at_line_ends(false, cx); editor.set_input_enabled(true); + editor.set_autoindent(true); editor.selections.line_mode = false; // we set the VimEnabled context on all editors so that we diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index cb4d865dc9..6dff69a6e4 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -192,7 +192,7 @@ pub fn visual_block_motion( 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 mut selection = Selection { + let selection = Selection { id: s.new_selection_id(), start: start.to_point(map), end: end.to_point(map), @@ -392,7 +392,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext linewise = all_selections_were_entire_line; } - let selection = selection.clone(); + let mut selection = selection.clone(); if !selection.reversed { let adjusted = selection.end; // If the selection is empty, move both the start and end forward one From 43127384c6ffddf0338dbfc7d93e825ac6132ad3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 13:48:12 -0400 Subject: [PATCH 250/326] Update modal icon styles Co-Authored-By: Max Brunsfeld --- assets/icons/ellipsis.svg | 5 ++++ .../src/collab_panel/channel_modal.rs | 21 +++++++++---- crates/theme/src/theme.rs | 4 +-- styles/src/style_tree/collab_modals.ts | 30 +++++-------------- 4 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 assets/icons/ellipsis.svg diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 0000000000..1858c65520 --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 3e4f274f23..75ab40be85 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -423,30 +423,39 @@ impl PickerDelegate for ChannelModalDelegate { .with_children({ let svg = match self.mode { Mode::ManageMembers => Some( - Svg::new("icons/ellipsis_14.svg") + Svg::new("icons/ellipsis.svg") .with_color(theme.member_icon.color) .constrained() - .with_width(theme.member_icon.width) + .with_width(theme.member_icon.icon_width) .aligned() + .constrained() + .with_width(theme.member_icon.button_width) + .with_height(theme.member_icon.button_width) .contained() .with_style(theme.member_icon.container), ), Mode::InviteMembers => match request_status { Some(proto::channel_member::Kind::Member) => Some( - Svg::new("icons/check_8.svg") + Svg::new("icons/check.svg") .with_color(theme.member_icon.color) .constrained() - .with_width(theme.member_icon.width) + .with_width(theme.member_icon.icon_width) .aligned() + .constrained() + .with_width(theme.member_icon.button_width) + .with_height(theme.member_icon.button_width) .contained() .with_style(theme.member_icon.container), ), Some(proto::channel_member::Kind::Invitee) => Some( - Svg::new("icons/check_8.svg") + Svg::new("icons/check.svg") .with_color(theme.invitee_icon.color) .constrained() - .with_width(theme.invitee_icon.width) + .with_width(theme.invitee_icon.icon_width) .aligned() + .constrained() + .with_width(theme.invitee_icon.button_width) + .with_height(theme.invitee_icon.button_width) .contained() .with_style(theme.invitee_icon.container), ), diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 912ca0e8b8..69fa7a09b3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -276,8 +276,8 @@ pub struct ChannelModal { pub contact_username: ContainerStyle, pub remove_member_button: ContainedText, pub cancel_invite_button: ContainedText, - pub member_icon: Icon, - pub invitee_icon: Icon, + pub member_icon: IconButton, + pub invitee_icon: IconButton, pub member_tag: ContainedText, } diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index c0bf358e71..4bdeb45f9c 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -4,6 +4,7 @@ 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" export default function channel_modal(): any { const theme = useTheme() @@ -26,6 +27,11 @@ export default function channel_modal(): any { const picker_input = input() + const member_icon_style = icon_button({ + variant: "ghost", + size: "sm", + }).default + return { contact_finder: contact_finder(), tabbed_modal: { @@ -93,29 +99,9 @@ export default function channel_modal(): any { }, channel_modal: { // This is used for the icons that are rendered to the right of channel Members in both UIs - member_icon: { - background: background(theme.middle), - padding: { - bottom: 4, - left: 4, - right: 4, - top: 4, - }, - width: 5, - color: foreground(theme.middle, "accent"), - }, + member_icon: member_icon_style, // This is used for the icons that are rendered to the right of channel invites in both UIs - invitee_icon: { - background: background(theme.middle), - padding: { - bottom: 4, - left: 4, - right: 4, - top: 4, - }, - width: 5, - color: foreground(theme.middle, "accent"), - }, + invitee_icon: member_icon_style, remove_member_button: { ...text(theme.middle, "sans", { size: "xs" }), background: background(theme.middle), From 925e09e0129617ff05c3bbda7c350ba90e9b6c7b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 13:56:11 -0400 Subject: [PATCH 251/326] Update collab panel empty state to match project panel Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_panel.rs | 2 ++ styles/src/style_tree/collab_panel.ts | 32 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f113f12f9d..4f0a61bf6a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2299,6 +2299,8 @@ impl View for CollabPanel { MouseEventHandler::new::(0, cx, |state, _| { let button = theme.log_in_button.style_for(state); Label::new("Sign in to collaborate", button.text.clone()) + .aligned() + .left() .contained() .with_style(button.container) }) diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 61c96ad75a..2d8c050838 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -153,7 +153,37 @@ export default function contacts_panel(): any { return { ...collab_modals(), - log_in_button: text_button(), + log_in_button: interactive({ + base: { + background: background(theme.middle), + border: border(theme.middle, "active"), + corner_radius: 4, + margin: { + top: 4, + left: 16, + right: 16, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(theme.middle, "sans", "default", { size: "sm" }), + }, + state: { + hovered: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "hovered"), + border: border(theme.middle, "active"), + }, + clicked: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "pressed"), + border: border(theme.middle, "active"), + }, + }, + }), background: background(layer), padding: { top: SPACING, From 442ec606d0add77138bfefe277464c08ea8258a4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 16 Aug 2023 11:05:08 -0700 Subject: [PATCH 252/326] collab 0.17.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 eb210eb797..0ccdf4c783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1479,7 +1479,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index c61fdeebfb..fc8c1644cd 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.16.0" +version = "0.17.0" publish = false [[bin]] From 07675e3c559b32182256de16958bc418667f437f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 16 Aug 2023 14:22:54 -0400 Subject: [PATCH 253/326] v0.101.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 0ccdf4c783..1b54bdda02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9863,7 +9863,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.100.0" +version = "0.101.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 1a2575dd5f..d0aebb15e7 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.100.0" +version = "0.101.0" publish = false [lib] From 5c3d563f0fb5093154d589851ef9d45bc7f154de Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 9 Aug 2023 20:24:06 +0300 Subject: [PATCH 254/326] Draft quick actions bar --- crates/editor/src/editor.rs | 33 +++++- crates/editor/src/inlay_hint_cache.rs | 2 +- crates/search/src/buffer_search.rs | 2 +- crates/theme/src/theme.rs | 1 + crates/zed/src/quick_action_bar.rs | 143 ++++++++++++++++++++++++++ crates/zed/src/zed.rs | 7 ++ styles/src/style_tree/workspace.ts | 4 + 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 crates/zed/src/quick_action_bar.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 256ef2284c..3c6103cd90 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -302,10 +302,11 @@ actions!( Hover, Format, ToggleSoftWrap, + ToggleInlays, RevealInFinder, CopyPath, CopyRelativePath, - CopyHighlightJson + CopyHighlightJson, ] ); @@ -446,6 +447,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::open_excerpts); cx.add_action(Editor::toggle_soft_wrap); + cx.add_action(Editor::toggle_inlays); cx.add_action(Editor::reveal_in_finder); cx.add_action(Editor::copy_path); cx.add_action(Editor::copy_relative_path); @@ -1238,6 +1240,7 @@ enum GotoDefinitionKind { #[derive(Debug, Clone)] enum InlayRefreshReason { + Toggled(bool), SettingsChange(InlayHintSettings), NewLinesShown, BufferEdited(HashSet>), @@ -2669,12 +2672,40 @@ impl Editor { } } + pub fn toggle_inlays(&mut self, _: &ToggleInlays, cx: &mut ViewContext) { + self.inlay_hint_cache.enabled = !self.inlay_hint_cache.enabled; + self.refresh_inlays( + InlayRefreshReason::Toggled(self.inlay_hint_cache.enabled), + cx, + ) + } + + pub fn inlays_enabled(&self) -> bool { + self.inlay_hint_cache.enabled + } + fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext) { if self.project.is_none() || self.mode != EditorMode::Full { return; } let (invalidate_cache, required_languages) = match reason { + InlayRefreshReason::Toggled(enabled) => { + if enabled { + (InvalidationStrategy::RefreshRequested, None) + } else { + self.inlay_hint_cache.clear(); + self.splice_inlay_hints( + self.visible_inlay_hints(cx) + .iter() + .map(|inlay| inlay.id) + .collect(), + Vec::new(), + cx, + ); + return; + } + } InlayRefreshReason::SettingsChange(new_settings) => { match self.inlay_hint_cache.update_settings( &self.buffer, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 8be72aec46..4327ff0d73 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -380,7 +380,7 @@ impl InlayHintCache { } } - fn clear(&mut self) { + pub fn clear(&mut self) { self.version += 1; self.update_tasks.clear(); self.hints.clear(); diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 36c9d3becd..fc3c78afe2 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -553,7 +553,7 @@ impl BufferSearchBar { .into_any() } - fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { + pub fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { let mut propagate_action = true; if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 69fa7a09b3..30a2e8caec 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -399,6 +399,7 @@ pub struct Toolbar { pub height: f32, pub item_spacing: f32, pub nav_button: Interactive, + pub toggleable_tool: Toggleable>, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/zed/src/quick_action_bar.rs b/crates/zed/src/quick_action_bar.rs new file mode 100644 index 0000000000..28dba9399f --- /dev/null +++ b/crates/zed/src/quick_action_bar.rs @@ -0,0 +1,143 @@ +use editor::Editor; +use gpui::{ + elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg}, + platform::{CursorStyle, MouseButton}, + Action, AnyElement, Element, Entity, EventContext, View, ViewContext, ViewHandle, +}; + +use search::{buffer_search, BufferSearchBar}; +use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace}; + +pub struct QuickActionBar { + workspace: ViewHandle, + active_item: Option>, +} + +impl QuickActionBar { + pub fn new(workspace: ViewHandle) -> Self { + Self { + workspace, + active_item: None, + } + } + + fn active_editor(&self) -> Option> { + self.active_item + .as_ref() + .and_then(|item| item.downcast::()) + } +} + +impl Entity for QuickActionBar { + type Event = (); +} + +impl View for QuickActionBar { + fn ui_name() -> &'static str { + "QuickActionsBar" + } + + fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { + let Some(editor) = self.active_editor() else { return Empty::new().into_any(); }; + + let inlays_enabled = editor.read(cx).inlays_enabled(); + let mut bar = Flex::row().with_child(render_quick_action_bar_button( + 0, + "icons/hamburger_15.svg", + inlays_enabled, + ( + "Toggle inlays".to_string(), + Some(Box::new(editor::ToggleInlays)), + ), + cx, + |this, cx| { + if let Some(editor) = this.active_editor() { + editor.update(cx, |editor, cx| { + editor.toggle_inlays(&editor::ToggleInlays, cx); + }); + } + }, + )); + + if editor.read(cx).buffer().read(cx).is_singleton() { + let search_action = buffer_search::Deploy { focus: true }; + + // TODO kb: this opens the search bar in a differently focused pane (should be the same) + should be toggleable + let pane = self.workspace.read(cx).active_pane().clone(); + bar = bar.with_child(render_quick_action_bar_button( + 1, + "icons/magnifying_glass_12.svg", + false, + ( + "Search in buffer".to_string(), + Some(Box::new(search_action.clone())), + ), + cx, + move |_, cx| { + pane.update(cx, |pane, cx| { + BufferSearchBar::deploy(pane, &search_action, cx); + }); + }, + )); + } + + bar.into_any() + } +} + +fn render_quick_action_bar_button< + F: 'static + Fn(&mut QuickActionBar, &mut EventContext), +>( + index: usize, + icon: &'static str, + toggled: bool, + tooltip: (String, Option>), + cx: &mut ViewContext, + on_click: F, +) -> AnyElement { + enum QuickActionBarButton {} + + let theme = theme::current(cx); + let (tooltip_text, action) = tooltip; + + MouseEventHandler::::new(index, cx, |mouse_state, _| { + let style = theme + .workspace + .toolbar + .toggleable_tool + .in_state(toggled) + .style_for(mouse_state); + Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) + .with_tooltip::(index, tooltip_text, action, theme.tooltip.clone(), cx) + .into_any_named("quick action bar button") +} + +impl ToolbarItemView for QuickActionBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut ViewContext, + ) -> ToolbarItemLocation { + match active_pane_item { + Some(active_item) => { + dbg!("@@@@@@@@@@ TODO kb", active_item.id()); + self.active_item = Some(active_item.boxed_clone()); + ToolbarItemLocation::PrimaryRight { flex: None } + } + None => { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1c57174fe2..5ff453484c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5,6 +5,8 @@ pub mod only_instance; #[cfg(any(test, feature = "test-support"))] pub mod test; +mod quick_action_bar; + use ai::AssistantPanel; use anyhow::Context; use assets::Assets; @@ -30,6 +32,7 @@ use gpui::{ pub use lsp; pub use project; use project_panel::ProjectPanel; +use quick_action_bar::QuickActionBar; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; @@ -255,12 +258,16 @@ pub fn initialize_workspace( workspace_handle.update(&mut cx, |workspace, cx| { let workspace_handle = cx.handle(); cx.subscribe(&workspace_handle, { + let workspace_handle = workspace_handle.clone(); move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); toolbar.add_item(breadcrumbs, cx); + let quick_action_bar = + cx.add_view(|_| QuickActionBar::new(workspace_handle.clone())); + toolbar.add_item(quick_action_bar, cx); let buffer_search_bar = cx.add_view(BufferSearchBar::new); toolbar.add_item(buffer_search_bar, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 5aee3c987d..4d44166eb8 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -12,6 +12,7 @@ import tabBar from "./tab_bar" import { interactive } from "../element" import { titlebar } from "./titlebar" import { useTheme } from "../theme" +import { toggleable_icon_button } from "../component/icon_button" export default function workspace(): any { const theme = useTheme() @@ -149,6 +150,9 @@ export default function workspace(): any { }, }, }), + toggleable_tool: toggleable_icon_button(theme, { + active_color: "accent", + }), padding: { left: 8, right: 8, top: 4, bottom: 4 }, }, breadcrumb_height: 24, From 6a326c1bd86a32ea9d4d0f4c9baa05773c60b1c0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 00:15:37 +0300 Subject: [PATCH 255/326] Toggle buffer search via quick actions --- crates/zed/src/quick_action_bar.rs | 42 +++++++++++++++++++++--------- crates/zed/src/zed.rs | 7 +++-- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/crates/zed/src/quick_action_bar.rs b/crates/zed/src/quick_action_bar.rs index 28dba9399f..c133f5ece4 100644 --- a/crates/zed/src/quick_action_bar.rs +++ b/crates/zed/src/quick_action_bar.rs @@ -6,17 +6,17 @@ use gpui::{ }; use search::{buffer_search, BufferSearchBar}; -use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace}; +use workspace::{item::ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView}; pub struct QuickActionBar { - workspace: ViewHandle, + pane: ViewHandle, active_item: Option>, } impl QuickActionBar { - pub fn new(workspace: ViewHandle) -> Self { + pub fn new(pane: ViewHandle) -> Self { Self { - workspace, + pane, active_item: None, } } @@ -60,23 +60,40 @@ impl View for QuickActionBar { )); if editor.read(cx).buffer().read(cx).is_singleton() { + let buffer_search_bar = self + .pane + .read(cx) + .toolbar() + .read(cx) + .item_of_type::(); + let search_bar_shown = buffer_search_bar + .as_ref() + .map(|bar| !bar.read(cx).is_dismissed()) + .unwrap_or(false); + let search_action = buffer_search::Deploy { focus: true }; - // TODO kb: this opens the search bar in a differently focused pane (should be the same) + should be toggleable - let pane = self.workspace.read(cx).active_pane().clone(); bar = bar.with_child(render_quick_action_bar_button( 1, "icons/magnifying_glass_12.svg", - false, + search_bar_shown, ( - "Search in buffer".to_string(), + "Toggle buffer search".to_string(), Some(Box::new(search_action.clone())), ), cx, - move |_, cx| { - pane.update(cx, |pane, cx| { - BufferSearchBar::deploy(pane, &search_action, cx); - }); + move |this, cx| { + if search_bar_shown { + if let Some(buffer_search_bar) = buffer_search_bar.as_ref() { + buffer_search_bar.update(cx, |buffer_search_bar, cx| { + buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); + }); + } + } else { + this.pane.update(cx, |pane, cx| { + BufferSearchBar::deploy(pane, &search_action, cx); + }); + } }, )); } @@ -130,7 +147,6 @@ impl ToolbarItemView for QuickActionBar { ) -> ToolbarItemLocation { match active_pane_item { Some(active_item) => { - dbg!("@@@@@@@@@@ TODO kb", active_item.id()); self.active_item = Some(active_item.boxed_clone()); ToolbarItemLocation::PrimaryRight { flex: None } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5ff453484c..47dff41729 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -258,15 +258,14 @@ pub fn initialize_workspace( workspace_handle.update(&mut cx, |workspace, cx| { let workspace_handle = cx.handle(); cx.subscribe(&workspace_handle, { - let workspace_handle = workspace_handle.clone(); move |workspace, _, event, cx| { - if let workspace::Event::PaneAdded(pane) = event { - pane.update(cx, |pane, cx| { + if let workspace::Event::PaneAdded(pane_handle) = event { + pane_handle.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); toolbar.add_item(breadcrumbs, cx); let quick_action_bar = - cx.add_view(|_| QuickActionBar::new(workspace_handle.clone())); + cx.add_view(|_| QuickActionBar::new(pane_handle.clone())); toolbar.add_item(quick_action_bar, cx); let buffer_search_bar = cx.add_view(BufferSearchBar::new); toolbar.add_item(buffer_search_bar, cx); From 0b93e490a550693f4b65faf61d30c8ae14943cb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 12:28:17 +0300 Subject: [PATCH 256/326] Improve toggle UI, fix inlays update speed --- crates/editor/src/editor.rs | 11 ++++++----- crates/zed/src/quick_action_bar.rs | 10 +++++----- styles/src/style_tree/workspace.ts | 1 + 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3c6103cd90..ef02cee3d0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1240,7 +1240,7 @@ enum GotoDefinitionKind { #[derive(Debug, Clone)] enum InlayRefreshReason { - Toggled(bool), + Toggle(bool), SettingsChange(InlayHintSettings), NewLinesShown, BufferEdited(HashSet>), @@ -2673,11 +2673,10 @@ impl Editor { } pub fn toggle_inlays(&mut self, _: &ToggleInlays, cx: &mut ViewContext) { - self.inlay_hint_cache.enabled = !self.inlay_hint_cache.enabled; self.refresh_inlays( - InlayRefreshReason::Toggled(self.inlay_hint_cache.enabled), + InlayRefreshReason::Toggle(!self.inlay_hint_cache.enabled), cx, - ) + ); } pub fn inlays_enabled(&self) -> bool { @@ -2690,7 +2689,8 @@ impl Editor { } let (invalidate_cache, required_languages) = match reason { - InlayRefreshReason::Toggled(enabled) => { + InlayRefreshReason::Toggle(enabled) => { + self.inlay_hint_cache.enabled = enabled; if enabled { (InvalidationStrategy::RefreshRequested, None) } else { @@ -2805,6 +2805,7 @@ impl Editor { self.display_map.update(cx, |display_map, cx| { display_map.splice_inlays(to_remove, to_insert, cx); }); + cx.notify(); } fn trigger_on_type_formatting( diff --git a/crates/zed/src/quick_action_bar.rs b/crates/zed/src/quick_action_bar.rs index c133f5ece4..6157ca9c47 100644 --- a/crates/zed/src/quick_action_bar.rs +++ b/crates/zed/src/quick_action_bar.rs @@ -45,10 +45,7 @@ impl View for QuickActionBar { 0, "icons/hamburger_15.svg", inlays_enabled, - ( - "Toggle inlays".to_string(), - Some(Box::new(editor::ToggleInlays)), - ), + ("Inlays".to_string(), Some(Box::new(editor::ToggleInlays))), cx, |this, cx| { if let Some(editor) = this.active_editor() { @@ -78,7 +75,8 @@ impl View for QuickActionBar { "icons/magnifying_glass_12.svg", search_bar_shown, ( - "Toggle buffer search".to_string(), + "Buffer search".to_string(), + // TODO kb no keybinding is shown for search + toggle inlays does not update icon color Some(Box::new(search_action.clone())), ), cx, @@ -132,6 +130,8 @@ fn render_quick_action_bar_button< .constrained() .with_width(style.button_width) .with_height(style.button_width) + .contained() + .with_style(style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 4d44166eb8..578dd23c6e 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -151,6 +151,7 @@ export default function workspace(): any { }, }), toggleable_tool: toggleable_icon_button(theme, { + margin: { left: 8 }, active_color: "accent", }), padding: { left: 8, right: 8, top: 4, bottom: 4 }, From 0f650acc23a89239bb788801ffe40d8e6fdf3c5b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 13:30:12 +0300 Subject: [PATCH 257/326] Repaint inlays icon on inlays cache disabling/enabling Co-Authored-By: Mikayla Maki --- crates/editor/src/inlay_hint_cache.rs | 2 +- crates/gpui/src/keymap_matcher/binding.rs | 8 ++++++-- crates/zed/src/quick_action_bar.rs | 21 ++++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 4327ff0d73..3385546971 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -24,7 +24,7 @@ pub struct InlayHintCache { hints: HashMap>>, allowed_hint_kinds: HashSet>, version: usize, - enabled: bool, + pub(super) enabled: bool, update_tasks: HashMap, } diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index 527052c85d..f7296d50fb 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -88,8 +88,12 @@ impl Binding { action: &dyn Action, contexts: &[KeymapContext], ) -> Option> { - if self.action.eq(action) && self.match_context(contexts) { - Some(self.keystrokes.clone()) + if self.action.eq(action) { + if self.match_context(contexts) { + Some(self.keystrokes.clone()) + } else { + None + } } else { None } diff --git a/crates/zed/src/quick_action_bar.rs b/crates/zed/src/quick_action_bar.rs index 6157ca9c47..245983dc49 100644 --- a/crates/zed/src/quick_action_bar.rs +++ b/crates/zed/src/quick_action_bar.rs @@ -2,7 +2,7 @@ use editor::Editor; use gpui::{ elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg}, platform::{CursorStyle, MouseButton}, - Action, AnyElement, Element, Entity, EventContext, View, ViewContext, ViewHandle, + Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle, }; use search::{buffer_search, BufferSearchBar}; @@ -11,6 +11,7 @@ use workspace::{item::ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView}; pub struct QuickActionBar { pane: ViewHandle, active_item: Option>, + _inlays_enabled_subscription: Option, } impl QuickActionBar { @@ -18,6 +19,7 @@ impl QuickActionBar { Self { pane, active_item: None, + _inlays_enabled_subscription: None, } } @@ -76,7 +78,6 @@ impl View for QuickActionBar { search_bar_shown, ( "Buffer search".to_string(), - // TODO kb no keybinding is shown for search + toggle inlays does not update icon color Some(Box::new(search_action.clone())), ), cx, @@ -143,11 +144,25 @@ impl ToolbarItemView for QuickActionBar { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, - _: &mut ViewContext, + cx: &mut ViewContext, ) -> ToolbarItemLocation { match active_pane_item { Some(active_item) => { self.active_item = Some(active_item.boxed_clone()); + self._inlays_enabled_subscription.take(); + + if let Some(editor) = active_item.downcast::() { + let mut inlays_enabled = editor.read(cx).inlays_enabled(); + self._inlays_enabled_subscription = + Some(cx.observe(&editor, move |_, editor, cx| { + let new_inlays_enabled = editor.read(cx).inlays_enabled(); + if inlays_enabled != new_inlays_enabled { + inlays_enabled = new_inlays_enabled; + cx.notify(); + } + })); + } + ToolbarItemLocation::PrimaryRight { flex: None } } None => { From 8926c23bdbed957369e517af897264f8dae930e6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 13:23:27 +0300 Subject: [PATCH 258/326] Extract quick_action_bar into its own crate --- Cargo.lock | 12 ++++++++++ crates/quick_action_bar/Cargo.toml | 22 +++++++++++++++++++ .../src/quick_action_bar.rs | 0 crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 2 -- 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 crates/quick_action_bar/Cargo.toml rename crates/{zed => quick_action_bar}/src/quick_action_bar.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 1b54bdda02..51ca31bd9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5714,6 +5714,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick_action_bar" +version = "0.1.0" +dependencies = [ + "editor", + "gpui", + "search", + "theme", + "workspace", +] + [[package]] name = "quote" version = "1.0.32" @@ -9922,6 +9933,7 @@ dependencies = [ "project", "project_panel", "project_symbols", + "quick_action_bar", "rand 0.8.5", "recent_projects", "regex", diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml new file mode 100644 index 0000000000..6953ac0e02 --- /dev/null +++ b/crates/quick_action_bar/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "quick_action_bar" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/quick_action_bar.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +gpui = { path = "../gpui" } +search = { path = "../search" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +theme = { path = "../theme", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/zed/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs similarity index 100% rename from crates/zed/src/quick_action_bar.rs rename to crates/quick_action_bar/src/quick_action_bar.rs diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d0aebb15e7..988648d4b1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -54,6 +54,7 @@ plugin_runtime = { path = "../plugin_runtime",optional = true } project = { path = "../project" } project_panel = { path = "../project_panel" } project_symbols = { path = "../project_symbols" } +quick_action_bar = { path = "../quick_action_bar" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 47dff41729..8d851909b3 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5,8 +5,6 @@ pub mod only_instance; #[cfg(any(test, feature = "test-support"))] pub mod test; -mod quick_action_bar; - use ai::AssistantPanel; use anyhow::Context; use assets::Assets; From 9c6135f47ad4b37a8d8bab305edc229f0453e8da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 13:51:14 +0300 Subject: [PATCH 259/326] Test hints toggle --- crates/editor/src/inlay_hint_cache.rs | 127 ++++++++++++++++++++++ crates/gpui/src/keymap_matcher/binding.rs | 8 +- 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 3385546971..4c998c3afa 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2683,6 +2683,127 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); } + #[gpui::test] + async fn test_toggle_inlays(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + + editor.update(cx, |editor, cx| { + editor.toggle_inlays(&crate::ToggleInlays, cx) + }); + cx.foreground().start_waiting(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should display inlays after toggle despite them disabled in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "First toggle should be cache's first update" + ); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlays(&crate::ToggleInlays, cx) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after 2nd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 2nd time after enabling hints in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 3); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlays(&crate::ToggleInlays, cx) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after enabling in settings and a 3rd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 4); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlays(&crate::ToggleInlays, cx) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 5); + }); + } + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { cx.foreground().forbid_parking(); @@ -2759,6 +2880,12 @@ all hints should be invalidated and requeried for all of its visible excerpts" .downcast::() .unwrap(); + editor.update(cx, |editor, cx| { + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 0); + }); + ("/a/main.rs", editor, fake_server) } diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index f7296d50fb..527052c85d 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -88,12 +88,8 @@ impl Binding { action: &dyn Action, contexts: &[KeymapContext], ) -> Option> { - if self.action.eq(action) { - if self.match_context(contexts) { - Some(self.keystrokes.clone()) - } else { - None - } + if self.action.eq(action) && self.match_context(contexts) { + Some(self.keystrokes.clone()) } else { None } From 1938fd85e8029830af0760a588a742ea0e1457b2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 14:42:56 +0300 Subject: [PATCH 260/326] Do not leak pane handles --- .../quick_action_bar/src/quick_action_bar.rs | 36 ++++++------------- crates/search/src/buffer_search.rs | 25 ++++++++----- crates/zed/src/zed.rs | 12 +++---- 3 files changed, 33 insertions(+), 40 deletions(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 245983dc49..b506f8dc17 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -6,18 +6,18 @@ use gpui::{ }; use search::{buffer_search, BufferSearchBar}; -use workspace::{item::ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView}; +use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; pub struct QuickActionBar { - pane: ViewHandle, + buffer_search_bar: ViewHandle, active_item: Option>, _inlays_enabled_subscription: Option, } impl QuickActionBar { - pub fn new(pane: ViewHandle) -> Self { + pub fn new(buffer_search_bar: ViewHandle) -> Self { Self { - pane, + buffer_search_bar, active_item: None, _inlays_enabled_subscription: None, } @@ -59,17 +59,7 @@ impl View for QuickActionBar { )); if editor.read(cx).buffer().read(cx).is_singleton() { - let buffer_search_bar = self - .pane - .read(cx) - .toolbar() - .read(cx) - .item_of_type::(); - let search_bar_shown = buffer_search_bar - .as_ref() - .map(|bar| !bar.read(cx).is_dismissed()) - .unwrap_or(false); - + let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); let search_action = buffer_search::Deploy { focus: true }; bar = bar.with_child(render_quick_action_bar_button( @@ -82,17 +72,13 @@ impl View for QuickActionBar { ), cx, move |this, cx| { - if search_bar_shown { - if let Some(buffer_search_bar) = buffer_search_bar.as_ref() { - buffer_search_bar.update(cx, |buffer_search_bar, cx| { - buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); - }); + this.buffer_search_bar.update(cx, |buffer_search_bar, cx| { + if search_bar_shown { + buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); + } else { + buffer_search_bar.deploy(&search_action, cx); } - } else { - this.pane.update(cx, |pane, cx| { - BufferSearchBar::deploy(pane, &search_action, cx); - }); - } + }); }, )); } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index fc3c78afe2..d85d311b8f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -36,7 +36,7 @@ pub enum Event { } pub fn init(cx: &mut AppContext) { - cx.add_action(BufferSearchBar::deploy); + cx.add_action(BufferSearchBar::deploy_bar); cx.add_action(BufferSearchBar::dismiss); cx.add_action(BufferSearchBar::focus_editor); cx.add_action(BufferSearchBar::select_next_match); @@ -327,6 +327,19 @@ impl BufferSearchBar { cx.notify(); } + pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext) -> bool { + if self.show(cx) { + self.search_suggested(cx); + if deploy.focus { + self.select_query(cx); + cx.focus_self(); + } + return true; + } + + false + } + pub fn show(&mut self, cx: &mut ViewContext) -> bool { if self.active_searchable_item.is_none() { return false; @@ -553,21 +566,15 @@ impl BufferSearchBar { .into_any() } - pub fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { + fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { let mut propagate_action = true; if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { - if search_bar.show(cx) { - search_bar.search_suggested(cx); - if action.focus { - search_bar.select_query(cx); - cx.focus_self(); - } + if search_bar.deploy(action, cx) { propagate_action = false; } }); } - if propagate_action { cx.propagate_action(); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8d851909b3..de05c259c8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -257,16 +257,16 @@ pub fn initialize_workspace( let workspace_handle = cx.handle(); cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { - if let workspace::Event::PaneAdded(pane_handle) = event { - pane_handle.update(cx, |pane, cx| { + if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); toolbar.add_item(breadcrumbs, cx); - let quick_action_bar = - cx.add_view(|_| QuickActionBar::new(pane_handle.clone())); - toolbar.add_item(quick_action_bar, cx); let buffer_search_bar = cx.add_view(BufferSearchBar::new); - toolbar.add_item(buffer_search_bar, cx); + toolbar.add_item(buffer_search_bar.clone(), cx); + let quick_action_bar = + cx.add_view(|_| QuickActionBar::new(buffer_search_bar)); + toolbar.add_item(quick_action_bar, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, cx); let submit_feedback_button = From f9131f657efa31b764eb87d18b7fa3004226548f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 17:51:37 +0300 Subject: [PATCH 261/326] Use InlayHint instead of Inlay where appropriate --- crates/collab/src/tests/integration_tests.rs | 4 +- crates/editor/src/editor.rs | 40 ++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 10 ++--- crates/editor/src/scroll.rs | 6 +-- crates/project/src/project.rs | 8 ++-- .../quick_action_bar/src/quick_action_bar.rs | 27 +++++++------ 6 files changed, 50 insertions(+), 45 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 2224ecb838..a03e2ff16f 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7867,7 +7867,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .insert_tree( "/a", json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", "other.rs": "// Test file", }), ) @@ -8177,7 +8177,7 @@ async fn test_inlay_hint_refresh_is_forwarded( .insert_tree( "/a", json!({ - "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out", "other.rs": "// Test file", }), ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ef02cee3d0..904e77c9f0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -302,7 +302,7 @@ actions!( Hover, Format, ToggleSoftWrap, - ToggleInlays, + ToggleInlayHints, RevealInFinder, CopyPath, CopyRelativePath, @@ -447,7 +447,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::toggle_code_actions); cx.add_action(Editor::open_excerpts); cx.add_action(Editor::toggle_soft_wrap); - cx.add_action(Editor::toggle_inlays); + cx.add_action(Editor::toggle_inlay_hints); cx.add_action(Editor::reveal_in_finder); cx.add_action(Editor::copy_path); cx.add_action(Editor::copy_relative_path); @@ -1239,7 +1239,7 @@ enum GotoDefinitionKind { } #[derive(Debug, Clone)] -enum InlayRefreshReason { +enum InlayHintRefreshReason { Toggle(bool), SettingsChange(InlayHintSettings), NewLinesShown, @@ -1357,8 +1357,8 @@ impl Editor { })); } project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { - if let project::Event::RefreshInlays = event { - editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx); + if let project::Event::RefreshInlayHints = event { + editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); }; })); } @@ -2672,24 +2672,24 @@ impl Editor { } } - pub fn toggle_inlays(&mut self, _: &ToggleInlays, cx: &mut ViewContext) { - self.refresh_inlays( - InlayRefreshReason::Toggle(!self.inlay_hint_cache.enabled), + pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext) { + self.refresh_inlay_hints( + InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled), cx, ); } - pub fn inlays_enabled(&self) -> bool { + pub fn inlay_hints_enabled(&self) -> bool { self.inlay_hint_cache.enabled } - fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext) { + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext) { if self.project.is_none() || self.mode != EditorMode::Full { return; } let (invalidate_cache, required_languages) = match reason { - InlayRefreshReason::Toggle(enabled) => { + InlayHintRefreshReason::Toggle(enabled) => { self.inlay_hint_cache.enabled = enabled; if enabled { (InvalidationStrategy::RefreshRequested, None) @@ -2706,7 +2706,7 @@ impl Editor { return; } } - InlayRefreshReason::SettingsChange(new_settings) => { + InlayHintRefreshReason::SettingsChange(new_settings) => { match self.inlay_hint_cache.update_settings( &self.buffer, new_settings, @@ -2724,11 +2724,13 @@ impl Editor { ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), } } - InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), - InlayRefreshReason::BufferEdited(buffer_languages) => { + InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), + InlayHintRefreshReason::BufferEdited(buffer_languages) => { (InvalidationStrategy::BufferEdited, Some(buffer_languages)) } - InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None), + InlayHintRefreshReason::RefreshRequested => { + (InvalidationStrategy::RefreshRequested, None) + } }; if let Some(InlaySplice { @@ -7728,8 +7730,8 @@ impl Editor { .cloned() .collect::>(); if !languages_affected.is_empty() { - self.refresh_inlays( - InlayRefreshReason::BufferEdited(languages_affected), + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), cx, ); } @@ -7767,8 +7769,8 @@ impl Editor { fn settings_changed(&mut self, cx: &mut ViewContext) { self.refresh_copilot_suggestions(true, cx); - self.refresh_inlays( - InlayRefreshReason::SettingsChange(inlay_hint_settings( + self.refresh_inlay_hints( + InlayHintRefreshReason::SettingsChange(inlay_hint_settings( self.selections.newest_anchor().head(), &self.buffer.read(cx).snapshot(cx), cx, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 4c998c3afa..b5ccdb4f2d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2684,7 +2684,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" } #[gpui::test] - async fn test_toggle_inlays(cx: &mut gpui::TestAppContext) { + async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: false, @@ -2697,7 +2697,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; editor.update(cx, |editor, cx| { - editor.toggle_inlays(&crate::ToggleInlays, cx) + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); cx.foreground().start_waiting(); let lsp_request_count = Arc::new(AtomicU32::new(0)); @@ -2743,7 +2743,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); editor.update(cx, |editor, cx| { - editor.toggle_inlays(&crate::ToggleInlays, cx) + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { @@ -2776,7 +2776,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); editor.update(cx, |editor, cx| { - editor.toggle_inlays(&crate::ToggleInlays, cx) + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { @@ -2789,7 +2789,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); editor.update(cx, |editor, cx| { - editor.toggle_inlays(&crate::ToggleInlays, cx) + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 1f3adaf477..f5edb00d58 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -19,7 +19,7 @@ use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, persistence::DB, - Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot, + Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, ToPoint, }; @@ -301,7 +301,7 @@ impl Editor { cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { - editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx) + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx) }) .ok() }) @@ -333,7 +333,7 @@ impl Editor { cx, ); - self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1aa2a2dd40..933f259700 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -282,7 +282,7 @@ pub enum Event { new_peer_id: proto::PeerId, }, CollaboratorLeft(proto::PeerId), - RefreshInlays, + RefreshInlayHints, } pub enum LanguageServerState { @@ -2872,7 +2872,7 @@ impl Project { .upgrade(&cx) .ok_or_else(|| anyhow!("project dropped"))?; this.update(&mut cx, |project, cx| { - cx.emit(Event::RefreshInlays); + cx.emit(Event::RefreshInlayHints); project.remote_id().map(|project_id| { project.client.send(proto::RefreshInlayHints { project_id }) }) @@ -3436,7 +3436,7 @@ impl Project { cx: &mut ModelContext, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - cx.emit(Event::RefreshInlays); + cx.emit(Event::RefreshInlayHints); status.pending_work.remove(&token); cx.notify(); } @@ -6810,7 +6810,7 @@ impl Project { mut cx: AsyncAppContext, ) -> Result { this.update(&mut cx, |_, cx| { - cx.emit(Event::RefreshInlays); + cx.emit(Event::RefreshInlayHints); }); Ok(proto::Ack {}) } diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index b506f8dc17..a2bddff313 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -11,7 +11,7 @@ use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; pub struct QuickActionBar { buffer_search_bar: ViewHandle, active_item: Option>, - _inlays_enabled_subscription: Option, + _inlay_hints_enabled_subscription: Option, } impl QuickActionBar { @@ -19,7 +19,7 @@ impl QuickActionBar { Self { buffer_search_bar, active_item: None, - _inlays_enabled_subscription: None, + _inlay_hints_enabled_subscription: None, } } @@ -42,17 +42,20 @@ impl View for QuickActionBar { fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let Some(editor) = self.active_editor() else { return Empty::new().into_any(); }; - let inlays_enabled = editor.read(cx).inlays_enabled(); + let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut bar = Flex::row().with_child(render_quick_action_bar_button( 0, "icons/hamburger_15.svg", - inlays_enabled, - ("Inlays".to_string(), Some(Box::new(editor::ToggleInlays))), + inlay_hints_enabled, + ( + "Inlay hints".to_string(), + Some(Box::new(editor::ToggleInlayHints)), + ), cx, |this, cx| { if let Some(editor) = this.active_editor() { editor.update(cx, |editor, cx| { - editor.toggle_inlays(&editor::ToggleInlays, cx); + editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); }); } }, @@ -135,15 +138,15 @@ impl ToolbarItemView for QuickActionBar { match active_pane_item { Some(active_item) => { self.active_item = Some(active_item.boxed_clone()); - self._inlays_enabled_subscription.take(); + self._inlay_hints_enabled_subscription.take(); if let Some(editor) = active_item.downcast::() { - let mut inlays_enabled = editor.read(cx).inlays_enabled(); - self._inlays_enabled_subscription = + let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + self._inlay_hints_enabled_subscription = Some(cx.observe(&editor, move |_, editor, cx| { - let new_inlays_enabled = editor.read(cx).inlays_enabled(); - if inlays_enabled != new_inlays_enabled { - inlays_enabled = new_inlays_enabled; + let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + if inlay_hints_enabled != new_inlay_hints_enabled { + inlay_hints_enabled = new_inlay_hints_enabled; cx.notify(); } })); From 3ed50708ac09274b5074a86a110e0ce7ac0162b7 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 11:25:11 -0400 Subject: [PATCH 262/326] Add inlay_hint icon, update search icon, update tooltips --- assets/icons/inlay_hint.svg | 6 ++++++ crates/quick_action_bar/src/quick_action_bar.rs | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 assets/icons/inlay_hint.svg diff --git a/assets/icons/inlay_hint.svg b/assets/icons/inlay_hint.svg new file mode 100644 index 0000000000..571e2e4784 --- /dev/null +++ b/assets/icons/inlay_hint.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index a2bddff313..ab3ccacdb7 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -45,10 +45,10 @@ impl View for QuickActionBar { let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut bar = Flex::row().with_child(render_quick_action_bar_button( 0, - "icons/hamburger_15.svg", + "icons/inlay_hint.svg", inlay_hints_enabled, ( - "Inlay hints".to_string(), + "Toggle Inlay Hints".to_string(), Some(Box::new(editor::ToggleInlayHints)), ), cx, @@ -67,10 +67,10 @@ impl View for QuickActionBar { bar = bar.with_child(render_quick_action_bar_button( 1, - "icons/magnifying_glass_12.svg", + "icons/magnifying_glass.svg", search_bar_shown, ( - "Buffer search".to_string(), + "Buffer Search".to_string(), Some(Box::new(search_action.clone())), ), cx, From 7fcf9022b45960e1407cd81bc80cb9acd2aaebda Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 19:25:51 +0300 Subject: [PATCH 263/326] Fix rebase issues --- crates/quick_action_bar/src/quick_action_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index ab3ccacdb7..3055399c13 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -105,7 +105,7 @@ fn render_quick_action_bar_button< let theme = theme::current(cx); let (tooltip_text, action) = tooltip; - MouseEventHandler::::new(index, cx, |mouse_state, _| { + MouseEventHandler::new::(index, cx, |mouse_state, _| { let style = theme .workspace .toolbar From 5bb6a14d420071c25d1eaa05d79f460cfea02e27 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 14:20:41 -0400 Subject: [PATCH 264/326] Update inlay_hint icon --- assets/icons/inlay_hint.svg | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/icons/inlay_hint.svg b/assets/icons/inlay_hint.svg index 571e2e4784..c8e6bb2d36 100644 --- a/assets/icons/inlay_hint.svg +++ b/assets/icons/inlay_hint.svg @@ -1,6 +1,5 @@ - - - - + + + From ef86c08174116d6b7a10b79ce15dcbb2715741b8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 14:24:36 -0400 Subject: [PATCH 265/326] Use the `ghost` variant for a flat button style in the toolbar --- styles/src/style_tree/workspace.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 578dd23c6e..d4eaeb99da 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -152,6 +152,7 @@ export default function workspace(): any { }), toggleable_tool: toggleable_icon_button(theme, { margin: { left: 8 }, + variant: "ghost", active_color: "accent", }), padding: { left: 8, right: 8, top: 4, bottom: 4 }, From 7334bdccbff6065666c71dde8e7dee53497e939c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 23:37:55 +0300 Subject: [PATCH 266/326] Better multibuffer tests --- crates/editor/src/inlay_hint_cache.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b5ccdb4f2d..70cccf21da 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2001,7 +2001,7 @@ mod tests { }); } - #[gpui::test] + #[gpui::test(iterations = 10)] async fn test_multiple_excerpts_large_multibuffer( deterministic: Arc, cx: &mut gpui::TestAppContext, @@ -2335,10 +2335,12 @@ mod tests { all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!( - editor.inlay_hint_cache().version, - last_scroll_update_version + expected_layers.len(), - "Due to every excerpt having one hint, cache should update per new excerpt received" + + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_layers.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" ); }); } From a5a212e1dadb6d84e0b29154d9bcca53ad3053af Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 16 Aug 2023 14:25:40 -0700 Subject: [PATCH 267/326] Use our fork of alacritty to avoid winit dependency --- Cargo.lock | 248 +------------------------------------ crates/terminal/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 244 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b54bdda02..474dc7fd8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,18 +126,17 @@ dependencies = [ [[package]] name = "alacritty_config" version = "0.1.2-dev" -source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5" +source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5" dependencies = [ "log", "serde", "toml 0.7.6", - "winit", ] [[package]] name = "alacritty_config_derive" version = "0.2.2-dev" -source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5" +source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5" dependencies = [ "proc-macro2", "quote", @@ -147,7 +146,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.20.0-dev" -source = "git+https://github.com/alacritty/alacritty?rev=7b9f32300ee0a249c0872302c97635b460e45ba5#7b9f32300ee0a249c0872302c97635b460e45ba5" +source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5" dependencies = [ "alacritty_config", "alacritty_config_derive", @@ -213,30 +212,6 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049" -[[package]] -name = "android-activity" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" -dependencies = [ - "android-properties", - "bitflags 1.3.2", - "cc", - "jni-sys", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum 0.6.1", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -926,25 +901,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-sys" -version = "0.1.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" -dependencies = [ - "objc-sys", -] - -[[package]] -name = "block2" -version = "0.2.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" -dependencies = [ - "block-sys", - "objc2-encode", -] - [[package]] name = "blocking" version = "1.3.1" @@ -1126,20 +1082,6 @@ dependencies = [ "util", ] -[[package]] -name = "calloop" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" -dependencies = [ - "bitflags 1.3.2", - "log", - "nix 0.25.1", - "slotmap", - "thiserror", - "vec_map", -] - [[package]] name = "cap-fs-ext" version = "0.24.4" @@ -1248,12 +1190,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "chrono" version = "0.4.26" @@ -2073,15 +2009,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "cursor-icon" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740bb192a8e2d1350119916954f4409ee7f62f149b536911eeb78ba5a20526bf" -dependencies = [ - "serde", -] - [[package]] name = "dashmap" version = "5.5.0" @@ -2288,12 +2215,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dlib" version = "0.5.2" @@ -4533,7 +4454,7 @@ dependencies = [ "bitflags 1.3.2", "jni-sys", "ndk-sys", - "num_enum 0.5.11", + "num_enum", "raw-window-handle", "thiserror", ] @@ -4575,19 +4496,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if 1.0.0", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.26.2" @@ -4753,16 +4661,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" -dependencies = [ - "num_enum_derive 0.6.1", + "num_enum_derive", ] [[package]] @@ -4777,18 +4676,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "num_enum_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 2.0.28", -] - [[package]] name = "nvim-rs" version = "0.5.0" @@ -4814,32 +4701,6 @@ dependencies = [ "objc_exception", ] -[[package]] -name = "objc-sys" -version = "0.2.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" - -[[package]] -name = "objc2" -version = "0.3.0-beta.3.patch-leaks.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" -dependencies = [ - "block2", - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-encode" -version = "2.0.0-pre.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" -dependencies = [ - "objc-sys", -] - [[package]] name = "objc_exception" version = "0.1.2" @@ -4958,15 +4819,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "orbclient" -version = "0.3.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "221d488cd70617f1bd599ed8ceb659df2147d9393717954d82a0f5e8032a6ab1" -dependencies = [ - "redox_syscall 0.3.5", -] - [[package]] name = "ordered-float" version = "2.10.0" @@ -7095,15 +6947,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - [[package]] name = "sluice" version = "0.5.5" @@ -7148,15 +6991,6 @@ dependencies = [ "pin-project-lite 0.1.12", ] -[[package]] -name = "smol_str" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" -dependencies = [ - "serde", -] - [[package]] name = "snippet" version = "0.1.0" @@ -8843,12 +8677,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" @@ -9313,17 +9141,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19353897b48e2c4d849a2d73cb0aeb16dc2be4e00c565abfc11eb65a806e47de" -dependencies = [ - "js-sys", - "once_cell", - "wasm-bindgen", -] - [[package]] name = "webpki" version = "0.21.4" @@ -9639,42 +9456,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" -[[package]] -name = "winit" -version = "0.29.0-beta.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1afaf8490cc3f1309520ebb53a4cd3fc3642c7df8064a4b074bb9867998d44" -dependencies = [ - "android-activity", - "atomic-waker", - "bitflags 2.3.3", - "calloop", - "cfg_aliases", - "core-foundation", - "core-graphics", - "cursor-icon", - "dispatch", - "js-sys", - "libc", - "log", - "ndk", - "ndk-sys", - "objc2", - "once_cell", - "orbclient", - "raw-window-handle", - "redox_syscall 0.3.5", - "serde", - "smol_str", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "windows-sys", - "xkbcommon-dl", -] - [[package]] name = "winnow" version = "0.5.2" @@ -9792,25 +9573,6 @@ dependencies = [ "libc", ] -[[package]] -name = "xkbcommon-dl" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699" -dependencies = [ - "bitflags 2.3.3", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" - [[package]] name = "xmlparser" version = "0.13.5" diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index fbcf0ec4b9..18c0f8be3c 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -16,7 +16,7 @@ db = { path = "../db" } theme = { path = "../theme" } util = { path = "../util" } -alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "7b9f32300ee0a249c0872302c97635b460e45ba5" } +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "f6d001ba8080ebfab6822106a436c64b677a44d5" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } smallvec.workspace = true smol.workspace = true From 3074455386126085ecd8f68d9ecc8dddfece51b0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 16 Aug 2023 16:56:00 -0700 Subject: [PATCH 268/326] WIP --- crates/collab/src/tests/channel_tests.rs | 13 +++++++++++++ styles/src/style_tree/collab_modals.ts | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index d778b6a472..a250f59a21 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -770,6 +770,19 @@ async fn test_call_from_channel( }); } +#[gpui::test] +async fn test_lost_channel_creation( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + // Invite a member + // Create a new sub channel + // Member accepts invite + // Make sure that member can see new channel + todo!(); +} + #[derive(Debug, PartialEq)] struct ExpectedChannel { depth: usize, diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index 4bdeb45f9c..0f50e01a39 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -76,7 +76,8 @@ export default function channel_modal(): any { }, }, - max_height: 400, + // FIXME: due to a bug in the picker's size calculation, this must be 600 + max_height: 600, max_width: 540, title: { ...text(theme.middle, "sans", "on", { size: "lg" }), From 2f1614705569567b3e18331c4de18f8fe68df4af Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 16 Aug 2023 19:47:54 -0700 Subject: [PATCH 269/326] Fix dock resizing --- crates/ai/src/assistant.rs | 6 +- crates/collab_ui/src/collab_panel.rs | 4 +- crates/gpui/src/app.rs | 8 ++ crates/gpui/src/elements.rs | 17 ++- crates/gpui/src/elements/component.rs | 5 +- crates/gpui/src/elements/resizable.rs | 147 +++++++++++++++++---- crates/project_panel/src/project_panel.rs | 4 +- crates/terminal_view/src/terminal_panel.rs | 6 +- crates/workspace/src/dock.rs | 18 +-- crates/workspace/src/workspace.rs | 7 +- 10 files changed, 170 insertions(+), 52 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e0fe41aebe..70473cbc7f 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -726,10 +726,10 @@ impl Panel for AssistantPanel { } } - fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { match self.position(cx) { - DockPosition::Left | DockPosition::Right => self.width = Some(size), - DockPosition::Bottom => self.height = Some(size), + DockPosition::Left | DockPosition::Right => self.width = size, + DockPosition::Bottom => self.height = size, } cx.notify(); } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 4f0a61bf6a..b31ea0fbf2 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2391,8 +2391,8 @@ impl Panel for CollabPanel { .unwrap_or_else(|| settings::get::(cx).default_width) } - fn set_size(&mut self, size: f32, cx: &mut ViewContext) { - self.width = Some(size); + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; self.serialize(cx); cx.notify(); } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 8e6d43a45d..b08d9501f6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -577,6 +577,14 @@ impl AppContext { } } + pub fn optional_global(&self) -> Option<&T> { + if let Some(global) = self.globals.get(&TypeId::of::()) { + Some(global.downcast_ref().unwrap()) + } else { + None + } + } + pub fn upgrade(&self) -> App { App(self.weak_self.as_ref().unwrap().upgrade().unwrap()) } diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index b8af978658..f1be9b34ae 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -186,16 +186,27 @@ pub trait Element: 'static { Tooltip::new::(id, text, action, style, self.into_any(), cx) } - fn resizable( + /// Uses the the given element to calculate resizes for the given tag + fn provide_resize_bounds(self) -> BoundsProvider + where + Self: 'static + Sized, + { + BoundsProvider::<_, Tag>::new(self.into_any()) + } + + /// Calls the given closure with the new size of the element whenever the + /// handle is dragged. This will be calculated in relation to the bounds + /// provided by the given tag + fn resizable( self, side: HandleSide, size: f32, - on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), + on_resize: impl 'static + FnMut(&mut V, Option, &mut ViewContext), ) -> Resizable where Self: 'static + Sized, { - Resizable::new(self.into_any(), side, size, on_resize) + Resizable::new::(self.into_any(), side, size, on_resize) } fn mouse(self, region_id: usize) -> MouseEventHandler diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 1c4359e2c3..2f9cc6cce6 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -82,6 +82,9 @@ impl + 'static> Element for ComponentAdapter { view: &V, cx: &ViewContext, ) -> serde_json::Value { - element.debug(view, cx) + serde_json::json!({ + "type": "ComponentAdapter", + "child": element.debug(view, cx), + }) } } diff --git a/crates/gpui/src/elements/resizable.rs b/crates/gpui/src/elements/resizable.rs index 0b1d94f8f8..37e40d6584 100644 --- a/crates/gpui/src/elements/resizable.rs +++ b/crates/gpui/src/elements/resizable.rs @@ -1,14 +1,14 @@ use std::{cell::RefCell, rc::Rc}; +use collections::HashMap; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use crate::{ geometry::rect::RectF, platform::{CursorStyle, MouseButton}, - scene::MouseDrag, - AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, - SizeConstraint, View, ViewContext, + AnyElement, AppContext, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, + SizeConstraint, TypeTag, View, ViewContext, }; #[derive(Copy, Clone, Debug)] @@ -27,15 +27,6 @@ impl HandleSide { } } - /// 'before' is in reference to the standard english document ordering of left-to-right - /// then top-to-bottom - fn before_content(self) -> bool { - match self { - HandleSide::Left | HandleSide::Top => true, - HandleSide::Right | HandleSide::Bottom => false, - } - } - fn relevant_component(&self, vector: Vector2F) -> f32 { match self.axis() { Axis::Horizontal => vector.x(), @@ -43,14 +34,6 @@ impl HandleSide { } } - fn compute_delta(&self, e: MouseDrag) -> f32 { - if self.before_content() { - self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position) - } else { - self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position) - } - } - fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF { match self { HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)), @@ -69,21 +52,29 @@ impl HandleSide { } } +fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)> +where +{ + cx.optional_global::() + .and_then(|map| map.0.get(&tag)) +} + pub struct Resizable { child: AnyElement, + tag: TypeTag, handle_side: HandleSide, handle_size: f32, - on_resize: Rc)>>, + on_resize: Rc, &mut ViewContext)>>, } const DEFAULT_HANDLE_SIZE: f32 = 4.0; impl Resizable { - pub fn new( + pub fn new( child: AnyElement, handle_side: HandleSide, size: f32, - on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), + on_resize: impl 'static + FnMut(&mut V, Option, &mut ViewContext), ) -> Self { let child = match handle_side.axis() { Axis::Horizontal => child.constrained().with_max_width(size), @@ -94,6 +85,7 @@ impl Resizable { Self { child, handle_side, + tag: TypeTag::new::(), handle_size: DEFAULT_HANDLE_SIZE, on_resize: Rc::new(RefCell::new(on_resize)), } @@ -139,6 +131,14 @@ impl Element for Resizable { handle_region, ) .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere + .on_click(MouseButton::Left, { + let on_resize = self.on_resize.clone(); + move |click, v, cx| { + if click.click_count == 2 { + on_resize.borrow_mut()(v, None, cx); + } + } + }) .on_drag(MouseButton::Left, { let bounds = bounds.clone(); let side = self.handle_side; @@ -146,16 +146,30 @@ impl Element for Resizable { let min_size = side.relevant_component(constraint.min); let max_size = side.relevant_component(constraint.max); let on_resize = self.on_resize.clone(); + let tag = self.tag; move |event, view: &mut V, cx| { if event.end { return; } - let new_size = min_size - .max(prev_size + side.compute_delta(event)) - .min(max_size) - .round(); + + let Some((bounds, _)) = get_bounds(tag, cx) else { + return; + }; + + let new_size_raw = match side { + // Handle on top side of element => Element is on bottom + HandleSide::Top => bounds.height() + bounds.origin_y() - event.position.y(), + // Handle on right side of element => Element is on left + HandleSide::Right => event.position.x() - bounds.lower_left().x(), + // Handle on left side of element => Element is on the right + HandleSide::Left => bounds.width() + bounds.origin_x() - event.position.x(), + // Handle on bottom side of element => Element is on the top + HandleSide::Bottom => event.position.y() - bounds.lower_left().y(), + }; + + let new_size = min_size.max(new_size_raw).min(max_size).round(); if new_size != prev_size { - on_resize.borrow_mut()(view, new_size, cx); + on_resize.borrow_mut()(view, Some(new_size), cx); } } }), @@ -201,3 +215,80 @@ impl Element for Resizable { }) } } + +#[derive(Debug, Default)] +struct ProviderMap(HashMap); + +pub struct BoundsProvider { + child: AnyElement, + phantom: std::marker::PhantomData

, +} + +impl BoundsProvider { + pub fn new(child: AnyElement) -> Self { + Self { + child, + phantom: std::marker::PhantomData, + } + } +} + +impl Element for BoundsProvider { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: crate::SizeConstraint, + view: &mut V, + cx: &mut crate::LayoutContext, + ) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) { + (self.child.layout(constraint, view, cx), ()) + } + + fn paint( + &mut self, + scene: &mut crate::SceneBuilder, + bounds: pathfinder_geometry::rect::RectF, + visible_bounds: pathfinder_geometry::rect::RectF, + _: &mut Self::LayoutState, + view: &mut V, + cx: &mut crate::PaintContext, + ) -> Self::PaintState { + cx.update_default_global::(|map, _| { + map.0.insert(TypeTag::new::

(), (bounds, visible_bounds)); + }); + + self.child + .paint(scene, bounds.origin(), visible_bounds, view, cx) + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: pathfinder_geometry::rect::RectF, + _: pathfinder_geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &crate::ViewContext, + ) -> Option { + self.child.rect_for_text_range(range_utf16, view, cx) + } + + fn debug( + &self, + _: pathfinder_geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &crate::ViewContext, + ) -> serde_json::Value { + serde_json::json!({ + "type": "Provider", + "providing": format!("{:?}", TypeTag::new::

()), + "child": self.child.debug(view, cx), + }) + } +} diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7a3e405e58..9fbbd3408f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1651,8 +1651,8 @@ impl workspace::dock::Panel for ProjectPanel { .unwrap_or_else(|| settings::get::(cx).default_width) } - fn set_size(&mut self, size: f32, cx: &mut ViewContext) { - self.width = Some(size); + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; self.serialize(cx); cx.notify(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 7141cda172..472e748359 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -362,10 +362,10 @@ impl Panel for TerminalPanel { } } - fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { match self.position(cx) { - DockPosition::Left | DockPosition::Right => self.width = Some(size), - DockPosition::Bottom => self.height = Some(size), + DockPosition::Left | DockPosition::Right => self.width = size, + DockPosition::Bottom => self.height = size, } self.serialize(cx); cx.notify(); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 55233c0836..3d40c8c420 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -1,4 +1,4 @@ -use crate::{StatusItemView, Workspace}; +use crate::{StatusItemView, Workspace, WorkspaceBounds}; use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext, @@ -13,7 +13,7 @@ pub trait Panel: View { fn position_is_valid(&self, position: DockPosition) -> bool; fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); fn size(&self, cx: &WindowContext) -> f32; - fn set_size(&mut self, size: f32, cx: &mut ViewContext); + fn set_size(&mut self, size: Option, cx: &mut ViewContext); fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; fn icon_tooltip(&self) -> (String, Option>); fn icon_label(&self, _: &WindowContext) -> Option { @@ -50,7 +50,7 @@ pub trait PanelHandle { fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); fn set_active(&self, active: bool, cx: &mut WindowContext); fn size(&self, cx: &WindowContext) -> f32; - fn set_size(&self, size: f32, cx: &mut WindowContext); + fn set_size(&self, size: Option, cx: &mut WindowContext); fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); fn icon_label(&self, cx: &WindowContext) -> Option; @@ -82,7 +82,7 @@ where self.read(cx).size(cx) } - fn set_size(&self, size: f32, cx: &mut WindowContext) { + fn set_size(&self, size: Option, cx: &mut WindowContext) { self.update(cx, |this, cx| this.set_size(size, cx)) } @@ -373,7 +373,7 @@ impl Dock { } } - pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext) { + pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { entry.panel.set_size(size, cx); cx.notify(); @@ -386,7 +386,7 @@ impl Dock { .into_any() .contained() .with_style(self.style(cx)) - .resizable( + .resizable::( self.position.to_resize_handle_side(), active_entry.panel.size(cx), |_, _, _| {}, @@ -423,7 +423,7 @@ impl View for Dock { ChildView::new(active_entry.panel.as_any(), cx) .contained() .with_style(style) - .resizable( + .resizable::( self.position.to_resize_handle_side(), active_entry.panel.size(cx), |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), @@ -700,8 +700,8 @@ pub mod test { self.size } - fn set_size(&mut self, size: f32, _: &mut ViewContext) { - self.size = size; + fn set_size(&mut self, size: Option, _: &mut ViewContext) { + self.size = size.unwrap_or(300.); } fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 79f38f8e30..79b701e015 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -553,6 +553,8 @@ struct FollowerState { items_by_leader_view_id: HashMap>, } +enum WorkspaceBounds {} + impl Workspace { pub fn new( workspace_id: WorkspaceId, @@ -3776,6 +3778,7 @@ impl View for Workspace { })) .with_children(self.render_notifications(&theme.workspace, cx)), )) + .provide_resize_bounds::() .flex(1.0, true), ) .with_child(ChildView::new(&self.status_bar, cx)) @@ -4859,7 +4862,9 @@ mod tests { panel_1.size(cx) ); - left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx)); + left_dock.update(cx, |left_dock, cx| { + left_dock.resize_active_panel(Some(1337.), cx) + }); assert_eq!( workspace .right_dock() From 8b1322745d6d21b17bcb971411d85019f85e2331 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 16 Aug 2023 22:50:02 -0400 Subject: [PATCH 270/326] Fix collab indicators --- styles/src/component/indicator.ts | 4 ++-- styles/src/style_tree/collab_panel.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/styles/src/component/indicator.ts b/styles/src/component/indicator.ts index 3a078fb53f..81a3b40da7 100644 --- a/styles/src/component/indicator.ts +++ b/styles/src/component/indicator.ts @@ -1,9 +1,9 @@ -import { background } from "../style_tree/components" +import { foreground } from "../style_tree/components" import { Layer, StyleSets } from "../theme" export const indicator = ({ layer, color }: { layer: Layer, color: StyleSets }) => ({ corner_radius: 4, padding: 4, margin: { top: 12, left: 12 }, - background: background(layer, color), + background: foreground(layer, color), }) diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 2d8c050838..7f0fd5f423 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -8,7 +8,6 @@ import { import { interactive, toggleable } from "../element" import { useTheme } from "../theme" import collab_modals from "./collab_modals" -import { text_button } from "../component/text_button" import { icon_button, toggleable_icon_button } from "../component/icon_button" import { indicator } from "../component/indicator" From 05becc75d1919588e45b9ee5e3a1810415681c25 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 16 Aug 2023 19:51:41 -0700 Subject: [PATCH 271/326] Collapse offline section by default --- crates/collab_ui/src/collab_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b31ea0fbf2..0e7bd5f929 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -397,7 +397,7 @@ impl CollabPanel { project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), - collapsed_sections: Vec::default(), + collapsed_sections: vec![Section::Offline], workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, From 5bc481112eccfe187103f1b504ff28b401cdb44f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 16 Aug 2023 20:05:21 -0700 Subject: [PATCH 272/326] Add test for lost channel update --- crates/collab/src/tests/channel_tests.rs | 97 +++++++++++++++++++++++- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index a250f59a21..06cf3607c0 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -776,11 +776,100 @@ async fn test_lost_channel_creation( cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + server + .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await; + // Invite a member - // Create a new sub channel - // Member accepts invite - // Make sure that member can see new channel - todo!(); + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // Sanity check + assert_channel_invitations( + client_b.channel_store(), + cx_b, + &[ExpectedChannel { + depth: 0, + id: channel_id, + name: "x".to_string(), + user_is_admin: false, + }], + ); + + let subchannel_id = client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.create_channel("subchannel", Some(channel_id), cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // Make sure A sees their new channel + assert_channels( + client_a.channel_store(), + cx_a, + &[ + ExpectedChannel { + depth: 0, + id: channel_id, + name: "x".to_string(), + user_is_admin: true, + }, + ExpectedChannel { + depth: 1, + id: subchannel_id, + name: "subchannel".to_string(), + user_is_admin: true, + }, + ], + ); + + // Accept the invite + client_b + .channel_store() + .update(cx_b, |channel_store, _| { + channel_store.respond_to_channel_invite(channel_id, true) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + // B should now see the channel + assert_channels( + client_b.channel_store(), + cx_b, + &[ + ExpectedChannel { + depth: 0, + id: channel_id, + name: "x".to_string(), + user_is_admin: false, + }, + ExpectedChannel { + depth: 1, + id: subchannel_id, + name: "subchannel".to_string(), + user_is_admin: false, + }, + ], + ); } #[derive(Debug, PartialEq)] From 7598030102772fe75e281a515f46a94a27e0f87c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 16 Aug 2023 12:03:29 -0600 Subject: [PATCH 273/326] Tidy-up --- crates/editor/src/editor.rs | 1 - crates/editor/src/movement.rs | 16 +-- crates/editor/src/multi_buffer.rs | 1 - crates/vim/src/test/neovim_connection.rs | 2 +- crates/vim/src/visual.rs | 118 ++++++++---------- .../vim/test_data/test_visual_block_mode.json | 1 + 6 files changed, 63 insertions(+), 76 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 59143e8a39..5875b558c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2514,7 +2514,6 @@ impl Editor { } pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { - dbg!("insert!"); self.insert_with_autoindent_mode( text, Some(AutoindentMode::Block { diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index f70436abeb..4eec92c8eb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -61,10 +61,10 @@ pub fn up_by_rows( goal: SelectionGoal, preserve_column_at_start: bool, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = if let SelectionGoal::Column(column) = goal { - column - } else { - map.column_to_chars(start.row(), start.column()) + let mut goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => map.column_to_chars(start.row(), start.column()), }; let prev_row = start.row().saturating_sub(row_count); @@ -95,10 +95,10 @@ pub fn down_by_rows( goal: SelectionGoal, preserve_column_at_end: bool, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_column = if let SelectionGoal::Column(column) = goal { - column - } else { - map.column_to_chars(start.row(), start.column()) + let mut goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => map.column_to_chars(start.row(), start.column()), }; let new_row = start.row() + row_count; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index df807c8f28..8417c411f2 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -364,7 +364,6 @@ impl MultiBuffer { S: ToOffset, T: Into>, { - dbg!("edit", &autoindent_mode); if self.buffers.borrow().is_empty() { return; } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 2c7a33909e..fc677f032c 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -520,5 +520,5 @@ fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { byte_range }) .collect::>(); - util::test::generate_marked_text(text, &byte_ranges[..], true); + util::test::generate_marked_text(text, &byte_ranges[..], true) } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 6dff69a6e4..18eddad5ca 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -129,62 +129,37 @@ pub fn visual_block_motion( let was_reversed = tail.column() > head.column(); - if !was_reversed && !(head.column() == 0 && head == map.max_point()) { + if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } - let Some((new_head, new_goal)) = move_selection(&map, head, goal) else { + let Some((new_head, _)) = move_selection(&map, head, goal) else { return }; head = new_head; - if goal == SelectionGoal::None { - goal = new_goal; - } - let mut is_reversed = tail.column() > head.column(); + let is_reversed = tail.column() > head.column(); if was_reversed && !is_reversed { tail = movement::left(map, tail) } else if !was_reversed && is_reversed { tail = movement::right(map, tail) } - if !is_reversed { + if !is_reversed && !preserve_goal { head = movement::saturating_right(map, head) } - if !preserve_goal - || !matches!( - goal, - SelectionGoal::ColumnRange { .. } | SelectionGoal::Column(_) - ) - { - goal = SelectionGoal::ColumnRange { - start: tail.column(), - end: head.column(), - } - } - - let mut columns = if let SelectionGoal::ColumnRange { start, end } = goal { - if start > end { - is_reversed = true; - end..start - } else { - is_reversed = false; - start..end - } - } else if let SelectionGoal::Column(column) = goal { - is_reversed = false; - column..(column + 1) - } else { - unreachable!() + let (start, end) = match goal { + SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), + SelectionGoal::Column(start) if preserve_goal => (start, start + 1), + _ => (tail.column(), head.column()), }; + goal = SelectionGoal::ColumnRange { start, end }; - if columns.start >= map.line_len(head.row()) { - columns.start = map.line_len(head.row()).saturating_sub(1); - } - if columns.start >= map.line_len(tail.row()) { - columns.start = map.line_len(tail.row()).saturating_sub(1); - } - + let columns = if is_reversed { + head.column()..tail.column() + } else { + tail.column()..head.column() + }; let mut selections = Vec::new(); let mut row = tail.row(); @@ -291,37 +266,39 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext = Default::default(); let line_mode = editor.selections.line_mode; - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let mut position = selection.head(); - if !selection.reversed { - position = movement::left(map, position); + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if line_mode { + let mut position = selection.head(); + if !selection.reversed { + position = movement::left(map, position); + } + original_columns.insert(selection.id, position.to_point(map).column); } - original_columns.insert(selection.id, position.to_point(map).column); - } - selection.goal = SelectionGoal::None; + selection.goal = SelectionGoal::None; + }); }); - }); - copy_selections_content(editor, line_mode, cx); - editor.insert("", cx); + copy_selections_content(editor, line_mode, cx); + editor.insert("", cx); - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head().to_point(map); + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head().to_point(map); - if let Some(column) = original_columns.get(&selection.id) { - cursor.column = *column + if let Some(column) = original_columns.get(&selection.id) { + cursor.column = *column + } + let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); + selection.collapse_to(cursor, selection.goal) + }); + if vim.state.mode == Mode::VisualBlock { + s.select_anchors(vec![s.first_anchor()]) } - let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); - selection.collapse_to(cursor, selection.goal) }); - if vim.state.mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); + }) }); vim.switch_mode(Mode::Normal, true, cx); }); @@ -948,8 +925,19 @@ mod test { " }) .await; - cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "down"]) + cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"]) .await; + cx.assert_shared_state(indoc! { + "The«ˇ q»uick + bro«ˇwn» + foxˇ + jumps over the + + lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["down"]).await; cx.assert_shared_state(indoc! { "The «qˇ»uick brow«nˇ» diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json index 743f7fa76c..ac306de4ab 100644 --- a/crates/vim/test_data/test_visual_block_mode.json +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -21,6 +21,7 @@ {"Key":"ctrl-v"} {"Key":"down"} {"Key":"down"} +{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njumps over the\n\nlazy dog\n","mode":"VisualBlock"}} {"Key":"down"} {"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}} {"Key":"left"} From 75679291a9a7353d7ddc444e460163c881806430 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 17 Aug 2023 00:55:11 -0700 Subject: [PATCH 274/326] Add fix for lost channel update bug --- crates/client/src/channel_store.rs | 2 ++ crates/collab/src/db.rs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index e2c18a63a9..03d334a9de 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -441,10 +441,12 @@ impl ChannelStore { for channel in payload.channels { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) { + // FIXME: We may be missing a path for this existing channel in certain cases let existing_channel = Arc::make_mut(existing_channel); existing_channel.name = channel.name; continue; } + self.channels_by_id.insert( channel.id, Arc::new(Channel { diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 64349123af..b457c4c116 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3650,7 +3650,11 @@ impl Database { let ancestor_ids = self.get_channel_ancestors(id, tx).await?; let user_ids = channel_member::Entity::find() .distinct() - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .filter( + channel_member::Column::ChannelId + .is_in(ancestor_ids.iter().copied()) + .and(channel_member::Column::Accepted.eq(true)), + ) .select_only() .column(channel_member::Column::UserId) .into_values::<_, QueryUserIds>() From b7dd12e53e3a9774dfb9da378c1a9e9683c89a85 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 17 Aug 2023 11:11:09 +0100 Subject: [PATCH 275/326] ensured search results are cleared appropriately while cycling modes --- crates/search/src/project_search.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 065b8d0880..e15bd0e27a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -673,9 +673,22 @@ impl ProjectSearchView { } } + fn clear_search(&mut self, cx: &mut ViewContext) { + self.model.update(cx, |model, cx| { + model.pending_search = None; + model.no_results = None; + model.match_ranges.clear(); + + model.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + }); + }); + } + fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + self.clear_search(cx); + let previous_mode = self.current_mode; - log::error!("Going from {previous_mode:?} to {:?}", mode); if previous_mode == mode { return; } @@ -684,7 +697,6 @@ impl ProjectSearchView { match mode { SearchMode::Semantic => { - dbg!("Matched on Semantic"); let has_permission = self.semantic_permissioned(cx); self.active_match_index = None; cx.spawn(|this, mut cx| async move { @@ -738,9 +750,6 @@ impl ProjectSearchView { } } - if let Some(query) = self.build_search_query(cx) { - self.search(cx); - }; cx.notify(); } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { From 1bd7d7077a68202b6851b4e507ebf99efb295d4f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:45:11 +0200 Subject: [PATCH 276/326] Move nav buttons to the left hand side of a tab bar. Co-authored-by: Nate --- crates/workspace/src/pane.rs | 115 +++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index dee6701564..3b295af802 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -287,8 +287,7 @@ impl Pane { menu.set_position_mode(OverlayPositionMode::Local) }); let theme = theme::current(cx).workspace.tab_bar.clone(); - let mut border_for_nav_buttons = theme.tab_style(false, false).container.border.clone(); - border_for_nav_buttons.left = false; + let nav_button_height = theme.height; let button_style = theme.nav_button; @@ -323,56 +322,6 @@ impl Pane { render_tab_bar_buttons: Rc::new(move |pane, cx| { let tooltip_style = theme::current(cx).tooltip.clone(); Flex::row() - .with_child(nav_button( - "icons/arrow_left_16.svg", - button_style.clone(), - nav_button_height, - tooltip_style.clone(), - pane.can_navigate_backward(), - { - move |pane, cx| { - if let Some(workspace) = pane.workspace.upgrade(cx) { - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, cx).detach_and_log_err(cx) - }) - }) - } - } - }, - super::GoBack, - "Go Back", - cx, - )) - .with_child( - nav_button( - "icons/arrow_right_16.svg", - button_style.clone(), - nav_button_height, - tooltip_style, - pane.can_navigate_forward(), - { - move |pane, cx| { - if let Some(workspace) = pane.workspace.upgrade(cx) { - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane, cx) - .detach_and_log_err(cx) - }) - }) - } - } - }, - super::GoForward, - "Go Forward", - cx, - ) - .contained() - .with_border(border_for_nav_buttons), - ) // New menu .with_child(Self::render_tab_bar_button( 0, @@ -1677,8 +1626,70 @@ impl View for Pane { }, ), ); + let tooltip_style = theme.tooltip.clone(); + let tab_bar_theme = theme.workspace.tab_bar.clone(); + + let nav_button_height = tab_bar_theme.height; + let button_style = tab_bar_theme.nav_button; + let border_for_nav_buttons = tab_bar_theme + .tab_style(false, false) + .container + .border + .clone(); let mut tab_row = Flex::row() + .with_child(nav_button( + "icons/arrow_left_16.svg", + button_style.clone(), + nav_button_height, + tooltip_style.clone(), + self.can_navigate_backward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_back(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoBack, + "Go Back", + cx, + )) + .with_child( + nav_button( + "icons/arrow_right_16.svg", + button_style.clone(), + nav_button_height, + tooltip_style, + self.can_navigate_forward(), + { + move |pane, cx| { + if let Some(workspace) = pane.workspace.upgrade(cx) { + let pane = cx.weak_handle(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace + .go_forward(pane, cx) + .detach_and_log_err(cx) + }) + }) + } + } + }, + super::GoForward, + "Go Forward", + cx, + ) + .contained() + .with_border(border_for_nav_buttons), + ) .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); if self.has_focus { From f451e3423d64c2fb3bd97da13653ddd888f78b69 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 17 Aug 2023 11:00:22 -0400 Subject: [PATCH 277/326] Fix missing border on tab bar navigation arrows --- styles/src/style_tree/tab_bar.ts | 7 ++++++- styles/src/theme/create_theme.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 81e1f7adf3..129bd17869 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -90,6 +90,10 @@ export default function tab_bar(): any { icon_width: 12, button_width: active_pane_active_tab.height, + border: border(theme.lowest, "on", { + bottom: true, + overlay: true, + }) }, state: { hovered: { @@ -97,10 +101,11 @@ export default function tab_bar(): any { background: background(theme.highest, "on", "hovered"), }, disabled: { - color: foreground(theme.highest, "on", "disabled"), + color: foreground(theme.highest, "on", "disabled") }, }, }) + const dragged_tab = { ...active_pane_active_tab, background: with_opacity(tab.background, 0.9), diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index e0da345bc5..ab3c96f280 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -1,4 +1,4 @@ -import chroma, { Scale, Color } from "chroma-js" +import { Scale, Color } from "chroma-js" import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" export { Syntax, ThemeSyntax, SyntaxHighlightStyle } import { From d308c910205b300ec408dfc8629109de48ed3b0f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 11:21:58 -0600 Subject: [PATCH 278/326] Add I and A in visual block mode --- assets/keymaps/vim.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index fc54934f2b..2864e5c1d2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -378,6 +378,11 @@ "s": "vim::Substitute", "c": "vim::Substitute", "~": "vim::ChangeCase", + "shift-i": [ + "vim::SwitchMode", + "Insert" + ], + "shift-a": "vim::InsertAfter", "r": [ "vim::PushOperator", "Replace" From 3514816ecedd0bedcaa81f6f52296034d01d0f02 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 13:35:32 -0600 Subject: [PATCH 279/326] Store some vim state per-editor This fixes a bug where opening and closing command would reset your selection incorrectly. --- crates/vim/src/editor_events.rs | 6 +- crates/vim/src/mode_indicator.rs | 4 +- crates/vim/src/motion.rs | 4 +- crates/vim/src/normal.rs | 4 +- crates/vim/src/normal/case.rs | 2 +- crates/vim/src/normal/search.rs | 11 ++-- crates/vim/src/normal/substitute.rs | 2 +- crates/vim/src/object.rs | 2 +- crates/vim/src/state.rs | 12 ++-- crates/vim/src/test/vim_test_context.rs | 4 +- crates/vim/src/vim.rs | 82 ++++++++++++++++++------- crates/vim/src/visual.rs | 21 +++++-- 12 files changed, 106 insertions(+), 48 deletions(-) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 893f5e8a85..f1b01f460d 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,4 +1,4 @@ -use crate::Vim; +use crate::{Vim, VimEvent}; use editor::{EditorBlurred, EditorFocused, EditorReleased}; use gpui::AppContext; @@ -22,6 +22,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { vim.set_active_editor(editor.clone(), cx); + cx.emit_global(VimEvent::ModeChanged { + mode: vim.state().mode, + }); }); }); } @@ -48,6 +51,7 @@ fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) { vim.active_editor = None; } } + vim.editor_states.remove(&editor.id()) }); }); } diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 79a3bbd051..4b1ade7a22 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -34,7 +34,7 @@ impl ModeIndicator { if settings::get::(cx).0 { mode_indicator.mode = cx .has_global::() - .then(|| cx.global::().state.mode); + .then(|| cx.global::().state().mode); } else { mode_indicator.mode.take(); } @@ -46,7 +46,7 @@ impl ModeIndicator { .has_global::() .then(|| { let vim = cx.global::(); - vim.enabled.then(|| vim.state.mode) + vim.enabled.then(|| vim.state().mode) }) .flatten(); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e04457d65c..29a1ba7df8 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -147,7 +147,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx)); let operator = Vim::read(cx).active_operator(); - match Vim::read(cx).state.mode { + match Vim::read(cx).state().mode { Mode::Normal => normal_motion(motion, operator, times, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx), Mode::Insert => { @@ -158,7 +158,7 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { } fn repeat_motion(backwards: bool, cx: &mut WindowContext) { - let find = match Vim::read(cx).state.last_find.clone() { + let find = match Vim::read(cx).workspace_state.last_find.clone() { Some(Motion::FindForward { before, text }) => { if backwards { Motion::FindBackward { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 5ac3e86165..ca26a7a217 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -116,8 +116,8 @@ pub fn normal_motion( pub fn normal_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - match vim.state.operator_stack.pop() { - Some(Operator::Object { around }) => match vim.state.operator_stack.pop() { + match vim.maybe_pop_operator() { + Some(Operator::Object { around }) => match vim.maybe_pop_operator() { Some(Operator::Change) => change_object(vim, object, around, cx), Some(Operator::Delete) => delete_object(vim, object, around, cx), Some(Operator::Yank) => yank_object(vim, object, around, cx), diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 0ec0eeba84..90967949bb 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -13,7 +13,7 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext(cx) { - match vim.state.mode { + match vim.state().mode { Mode::VisualLine => { let start = Point::new(selection.start.row, 0); let end = diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 9375c4e78d..44b304392f 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -70,10 +70,10 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext find. fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext) { - Vim::update(cx, |vim, _| vim.state.search = Default::default()); + Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default()); cx.propagate_action(); } @@ -93,8 +93,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { - let state = &mut vim.state.search; + let state = &mut vim.workspace_state.search; let mut count = state.count; + let direction = state.direction; // in the case that the query has changed, the search bar // will have selected the next match already. @@ -103,8 +104,8 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte { count = count.saturating_sub(1) } - search_bar.select_match(state.direction, count, cx); state.count = 1; + search_bar.select_match(direction, count, cx); search_bar.focus_editor(&Default::default(), cx); }); } diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index cf4e5a63d6..bfd2af0481 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -4,7 +4,7 @@ use language::Point; use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { - let line_mode = vim.state.mode == Mode::VisualLine; + let line_mode = vim.state().mode == Mode::VisualLine; vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 37476caed5..14166d2dff 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -62,7 +62,7 @@ pub fn init(cx: &mut AppContext) { } fn object(object: Object, cx: &mut WindowContext) { - match Vim::read(cx).state.mode { + match Vim::read(cx).state().mode { Mode::Normal => normal_object(object, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_object(object, cx), Mode::Insert => { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 5f146aa690..aacd3d26e0 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -41,16 +41,20 @@ pub enum Operator { FindBackward { after: bool }, } -#[derive(Default)] -pub struct VimState { +#[derive(Default, Clone)] +pub struct EditorState { pub mode: Mode, pub last_mode: Mode, pub operator_stack: Vec, - pub search: SearchState, +} +#[derive(Default, Clone)] +pub struct WorkspaceState { + pub search: SearchState, pub last_find: Option, } +#[derive(Clone)] pub struct SearchState { pub direction: Direction, pub count: usize, @@ -67,7 +71,7 @@ impl Default for SearchState { } } -impl VimState { +impl EditorState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { Mode::Normal => { diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ab5d7382c7..f5136be036 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -76,12 +76,12 @@ impl<'a> VimTestContext<'a> { } pub fn mode(&mut self) -> Mode { - self.cx.read(|cx| cx.global::().state.mode) + self.cx.read(|cx| cx.global::().state().mode) } pub fn active_operator(&mut self) -> Option { self.cx - .read(|cx| cx.global::().state.operator_stack.last().copied()) + .read(|cx| cx.global::().state().operator_stack.last().copied()) } pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e3f7c7dd10..ca22d25012 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,7 +12,7 @@ mod utils; mod visual; use anyhow::Result; -use collections::CommandPaletteFilter; +use collections::{CommandPaletteFilter, HashMap}; use editor::{movement, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, @@ -24,7 +24,7 @@ use motion::Motion; use normal::normal_replace; use serde::Deserialize; use settings::{Setting, SettingsStore}; -use state::{Mode, Operator, VimState}; +use state::{EditorState, Mode, Operator, WorkspaceState}; use std::sync::Arc; use visual::{visual_block_motion, visual_replace}; use workspace::{self, Workspace}; @@ -127,7 +127,9 @@ pub struct Vim { active_editor: Option>, editor_subscription: Option, enabled: bool, - state: VimState, + editor_states: HashMap, + workspace_state: WorkspaceState, + default_state: EditorState, } impl Vim { @@ -143,7 +145,7 @@ impl Vim { } fn set_active_editor(&mut self, editor: ViewHandle, cx: &mut WindowContext) { - self.active_editor = Some(editor.downgrade()); + self.active_editor = Some(editor.clone().downgrade()); self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { Event::SelectionsChanged { local: true } => { let editor = editor.read(cx); @@ -163,7 +165,10 @@ impl Vim { let editor_mode = editor.mode(); let newest_selection_empty = editor.selections.newest::(cx).is_empty(); - if editor_mode == EditorMode::Full && !newest_selection_empty { + if editor_mode == EditorMode::Full + && !newest_selection_empty + && self.state().mode == Mode::Normal + { self.switch_mode(Mode::Visual, true, cx); } } @@ -181,11 +186,14 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { - let last_mode = self.state.mode; - let prior_mode = self.state.last_mode; - self.state.last_mode = last_mode; - self.state.mode = mode; - self.state.operator_stack.clear(); + let state = self.state(); + let last_mode = state.mode; + let prior_mode = state.last_mode; + self.update_state(|state| { + state.last_mode = last_mode; + state.mode = mode; + state.operator_stack.clear(); + }); cx.emit_global(VimEvent::ModeChanged { mode }); @@ -207,7 +215,9 @@ impl Vim { // we cheat with visual block mode and use multiple cursors. // the cost of this cheat is we need to convert back to a single // cursor whenever vim would. - if last_mode == Mode::VisualBlock && mode != Mode::VisualBlock { + if last_mode == Mode::VisualBlock + && (mode != Mode::VisualBlock && mode != Mode::Insert) + { let tail = s.oldest_anchor().tail(); let head = s.newest_anchor().head(); s.select_anchor_ranges(vec![tail..head]); @@ -237,7 +247,7 @@ impl Vim { } fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) { - self.state.operator_stack.push(operator); + self.update_state(|state| state.operator_stack.push(operator)); self.sync_vim_settings(cx); } @@ -250,9 +260,13 @@ impl Vim { } } + fn maybe_pop_operator(&mut self) -> Option { + self.update_state(|state| state.operator_stack.pop()) + } + fn pop_operator(&mut self, cx: &mut WindowContext) -> Operator { - let popped_operator = self.state.operator_stack.pop() - .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); + let popped_operator = self.update_state( |state| state.operator_stack.pop() + ) .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); self.sync_vim_settings(cx); popped_operator } @@ -266,12 +280,12 @@ impl Vim { } fn clear_operator(&mut self, cx: &mut WindowContext) { - self.state.operator_stack.clear(); + self.update_state(|state| state.operator_stack.clear()); self.sync_vim_settings(cx); } fn active_operator(&self) -> Option { - self.state.operator_stack.last().copied() + self.state().operator_stack.last().copied() } fn active_editor_input_ignored(text: Arc, cx: &mut WindowContext) { @@ -282,15 +296,19 @@ impl Vim { match Vim::read(cx).active_operator() { Some(Operator::FindForward { before }) => { let find = Motion::FindForward { before, text }; - Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone())); + Vim::update(cx, |vim, _| { + vim.workspace_state.last_find = Some(find.clone()) + }); motion::motion(find, cx) } Some(Operator::FindBackward { after }) => { let find = Motion::FindBackward { after, text }; - Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone())); + Vim::update(cx, |vim, _| { + vim.workspace_state.last_find = Some(find.clone()) + }); motion::motion(find, cx) } - Some(Operator::Replace) => match Vim::read(cx).state.mode { + Some(Operator::Replace) => match Vim::read(cx).state().mode { Mode::Normal => normal_replace(text, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_replace(text, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), @@ -302,7 +320,6 @@ impl Vim { fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) { if self.enabled != enabled { self.enabled = enabled; - self.state = Default::default(); cx.update_default_global::(|filter, _| { if self.enabled { @@ -329,8 +346,29 @@ impl Vim { } } + pub fn state(&self) -> &EditorState { + if let Some(active_editor) = self.active_editor.as_ref() { + if let Some(state) = self.editor_states.get(&active_editor.id()) { + return state; + } + } + + &self.default_state + } + + pub fn update_state(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T { + let mut state = self.state().clone(); + let ret = func(&mut state); + + if let Some(active_editor) = self.active_editor.as_ref() { + self.editor_states.insert(active_editor.id(), state); + } + + ret + } + fn sync_vim_settings(&self, cx: &mut WindowContext) { - let state = &self.state; + let state = self.state(); let cursor_shape = state.cursor_shape(); self.update_active_editor(cx, |editor, cx| { @@ -391,7 +429,7 @@ impl Setting for VimModeSetting { fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty { + if vim.enabled && vim.state().mode == Mode::Normal && !newest_empty { vim.switch_mode(Mode::Visual, false, cx) } }) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 18eddad5ca..866086d538 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -53,7 +53,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| { - if vim.state.mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { + if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { 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) @@ -85,7 +85,7 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex // ensure the current character is included in the selection. if !selection.reversed { - let next_point = if vim.state.mode == Mode::VisualBlock { + let next_point = if vim.state().mode == Mode::VisualBlock { movement::saturating_right(map, selection.end) } else { movement::right(map, selection.end) @@ -240,7 +240,7 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { fn toggle_mode(mode: Mode, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - if vim.state.mode == mode { + if vim.state().mode == mode { vim.switch_mode(Mode::Normal, false, cx); } else { vim.switch_mode(mode, false, cx); @@ -294,7 +294,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) s.move_with(|_, selection| { selection.collapse_to(selection.start, SelectionGoal::None) }); - if vim.state.mode == Mode::VisualBlock { + if vim.state().mode == Mode::VisualBlock { s.select_anchors(vec![s.first_anchor()]) } }); @@ -971,4 +971,15 @@ mod test { }) .await; } + + #[gpui::test] + async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aˇbc", Mode::Normal); + cx.simulate_keystrokes(["ctrl-v"]); + assert_eq!(cx.mode(), Mode::VisualBlock); + cx.simulate_keystrokes(["cmd-shift-p", "escape"]); + assert_eq!(cx.mode(), Mode::VisualBlock); + } } From 59d1a5632f26b5cc6ee33b10855c867a32f5b9cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 15:15:00 -0600 Subject: [PATCH 280/326] Fix edge-cases in visual block insert --- crates/vim/src/normal/substitute.rs | 3 +- crates/vim/src/vim.rs | 2 +- crates/vim/src/visual.rs | 58 +++++++++++++++++++ .../test_data/test_visual_block_insert.json | 18 ++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 crates/vim/test_data/test_visual_block_insert.json diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index bfd2af0481..1d53c6831c 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -5,8 +5,8 @@ use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { let line_mode = vim.state().mode == Mode::VisualLine; - vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -32,6 +32,7 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { editor.edit(edits, cx); }); }); + vim.switch_mode(Mode::Insert, true, cx); } #[cfg(test)] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index ca22d25012..e41fab5495 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -232,7 +232,7 @@ impl Vim { s.move_with(|map, selection| { if last_mode.is_visual() && !mode.is_visual() { let mut point = selection.head(); - if !selection.reversed { + if !selection.reversed && !selection.is_empty() { point = movement::left(map, selection.head()); } selection.collapse_to(point, selection.goal) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 866086d538..4065657e59 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -157,9 +157,12 @@ pub fn visual_block_motion( let columns = if is_reversed { head.column()..tail.column() + } else if head.column() == tail.column() { + head.column()..(head.column() + 1) } else { tail.column()..head.column() }; + let mut selections = Vec::new(); let mut row = tail.row(); @@ -972,6 +975,61 @@ mod test { .await; } + #[gpui::test] + async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "ˇThe quick brown + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await; + cx.assert_shared_state(indoc! { + "«Tˇ»he quick brown + «fˇ»ox jumps over + «tˇ»he lazy dog + ˇ" + }) + .await; + + cx.simulate_shared_keystrokes(["shift-i", "k", "escape"]) + .await; + cx.assert_shared_state(indoc! { + "ˇkThe quick brown + kfox jumps over + kthe lazy dog + k" + }) + .await; + + cx.set_shared_state(indoc! { + "ˇThe quick brown + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await; + cx.assert_shared_state(indoc! { + "«Tˇ»he quick brown + «fˇ»ox jumps over + «tˇ»he lazy dog + ˇ" + }) + .await; + cx.simulate_shared_keystrokes(["c", "k", "escape"]).await; + cx.assert_shared_state(indoc! { + "ˇkhe quick brown + kox jumps over + khe lazy dog + k" + }) + .await; + } + #[gpui::test] async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/test_data/test_visual_block_insert.json b/crates/vim/test_data/test_visual_block_insert.json new file mode 100644 index 0000000000..d3d2689bd3 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_insert.json @@ -0,0 +1,18 @@ +{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"9"} +{"Key":"down"} +{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} +{"Key":"shift-i"} +{"Key":"k"} +{"Key":"escape"} +{"Get":{"state":"ˇkThe quick brown\nkfox jumps over\nkthe lazy dog\nk","mode":"Normal"}} +{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} +{"Key":"ctrl-v"} +{"Key":"9"} +{"Key":"down"} +{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} +{"Key":"c"} +{"Key":"k"} +{"Key":"escape"} +{"Get":{"state":"ˇkhe quick brown\nkox jumps over\nkhe lazy dog\nk","mode":"Normal"}} From eb0b2e60bb6c0ad727545663e6a048a537897044 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 15:40:27 -0600 Subject: [PATCH 281/326] Preserve line mode selection on undo This diverges from vim's behaviour (which collapses the cursor to a single point on undo). --- crates/vim/src/vim.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e41fab5495..da1c634682 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -18,7 +18,7 @@ use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::CursorShape; +use language::{CursorShape, Selection, SelectionGoal}; pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::normal_replace; @@ -150,8 +150,8 @@ impl Vim { Event::SelectionsChanged { local: true } => { let editor = editor.read(cx); if editor.leader_replica_id().is_none() { - let newest_empty = editor.selections.newest::(cx).is_empty(); - local_selections_changed(newest_empty, cx); + let newest = editor.selections.newest::(cx); + local_selections_changed(newest, cx); } } Event::InputIgnored { text } => { @@ -427,10 +427,14 @@ impl Setting for VimModeSetting { } } -fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) { +fn local_selections_changed(newest: Selection, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - if vim.enabled && vim.state().mode == Mode::Normal && !newest_empty { - vim.switch_mode(Mode::Visual, false, cx) + if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() { + if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) { + vim.switch_mode(Mode::VisualBlock, false, cx); + } else { + vim.switch_mode(Mode::Visual, false, cx) + } } }) } From 3c483d85f7a353689442893969ff33a50d2a6a50 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 15:58:10 -0600 Subject: [PATCH 282/326] Scrolling should work in visual mode --- assets/keymaps/vim.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2864e5c1d2..5281ec4213 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -107,6 +107,14 @@ "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion + "ctrl-f": "vim::PageDown", + "pagedown": "vim::PageDown", + "ctrl-b": "vim::PageUp", + "pageup": "vim::PageUp", + "ctrl-d": "vim::ScrollDown", + "ctrl-u": "vim::ScrollUp", + "ctrl-e": "vim::LineDown", + "ctrl-y": "vim::LineUp", // "g" commands "g g": "vim::StartOfDocument", "g h": "editor::Hover", @@ -294,14 +302,6 @@ "backwards": true } ], - "ctrl-f": "vim::PageDown", - "pagedown": "vim::PageDown", - "ctrl-b": "vim::PageUp", - "pageup": "vim::PageUp", - "ctrl-d": "vim::ScrollDown", - "ctrl-u": "vim::ScrollUp", - "ctrl-e": "vim::LineDown", - "ctrl-y": "vim::LineUp", "r": [ "vim::PushOperator", "Replace" From 243d1664e524d679771a76be030a82ce413dd6d0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 16:01:19 -0600 Subject: [PATCH 283/326] shift-enter should also give a newline (reported as vim feedback, but really true of the editor too) --- assets/keymaps/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 38ec8ffb40..7c18ec7012 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -172,6 +172,7 @@ "context": "Editor && mode == full", "bindings": { "enter": "editor::Newline", + "shift-enter": "editor::Newline", "cmd-shift-enter": "editor::NewlineAbove", "cmd-enter": "editor::NewlineBelow", "alt-z": "editor::ToggleSoftWrap", From d4276acab82fe2f2fdb2cfa0ccb4cb8c9d9248ca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 16:04:55 -0600 Subject: [PATCH 284/326] Give up on monospace indicator Changing mode almost always introduces the (1 selected) text in the status bar, so we may as well also keep the --'s for block and line mode. --- crates/vim/src/mode_indicator.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 4b1ade7a22..b110c39dc4 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -80,15 +80,12 @@ impl View for ModeIndicator { let theme = &theme::current(cx).workspace.status_bar; - // we always choose text to be 12 monospace characters - // so that as the mode indicator changes, the rest of the - // UI stays still. let text = match mode { Mode::Normal => "-- NORMAL --", Mode::Insert => "-- INSERT --", Mode::Visual => "-- VISUAL --", - Mode::VisualLine => "VISUAL LINE", - Mode::VisualBlock => "VISUAL BLOCK", + Mode::VisualLine => "-- VISUAL LINE --", + Mode::VisualBlock => "-- VISUAL BLOCK --", }; Label::new(text, theme.vim_mode_indicator.text.clone()) .contained() From d9ef987b049a97e649e40284dc44b37cdb25f27a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Aug 2023 15:23:28 -0700 Subject: [PATCH 285/326] Fix AppKit screen coordinate conversion leading to wrong window bounds --- crates/collab_ui/src/collab_ui.rs | 38 ++++++++++++- .../src/incoming_call_notification.rs | 31 +++-------- .../src/project_shared_notification.rs | 27 +++------- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/geometry.rs | 15 ++---- crates/gpui/src/platform/mac/screen.rs | 53 +++++++++++++++---- crates/gpui/src/platform/mac/window.rs | 43 ++++----------- crates/gpui/src/platform/test.rs | 4 ++ 8 files changed, 114 insertions(+), 98 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index f2ba35967f..0fed2e0ef9 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -9,8 +9,16 @@ mod sharing_status_indicator; use call::{ActiveCall, Room}; pub use collab_titlebar_item::CollabTitlebarItem; -use gpui::{actions, AppContext, Task}; -use std::sync::Arc; +use gpui::{ + actions, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + platform::{Screen, WindowBounds, WindowKind, WindowOptions}, + AppContext, Task, +}; +use std::{rc::Rc, sync::Arc}; use util::ResultExt; use workspace::AppState; @@ -88,3 +96,29 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { .log_err(); } } + +fn notification_window_options( + screen: Rc, + window_size: Vector2F, +) -> WindowOptions<'static> { + const NOTIFICATION_PADDING: f32 = 16.; + + let screen_bounds = screen.content_bounds(); + WindowOptions { + bounds: WindowBounds::Fixed(RectF::new( + screen_bounds.upper_right() + + vec2f( + -NOTIFICATION_PADDING - window_size.x(), + NOTIFICATION_PADDING, + ), + window_size, + )), + titlebar: None, + center: false, + focus: false, + show: true, + kind: WindowKind::PopUp, + is_movable: false, + screen: Some(screen), + } +} diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 410adbf862..c614a814ca 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -1,14 +1,14 @@ -use std::sync::{Arc, Weak}; - +use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; use client::proto; use futures::StreamExt; use gpui::{ elements::*, - geometry::{rect::RectF, vector::vec2f}, - platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, + geometry::vector::vec2f, + platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Entity, View, ViewContext, WindowHandle, }; +use std::sync::{Arc, Weak}; use util::ResultExt; use workspace::AppState; @@ -23,31 +23,16 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } if let Some(incoming_call) = incoming_call { - const PADDING: f32 = 16.; let window_size = cx.read(|cx| { let theme = &theme::current(cx).incoming_call_notification; vec2f(theme.window_width, theme.window_height) }); for screen in cx.platform().screens() { - let screen_bounds = screen.bounds(); - let window = cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::new( - screen_bounds.upper_right() - - vec2f(PADDING + window_size.x(), PADDING), - window_size, - )), - titlebar: None, - center: false, - focus: false, - show: true, - kind: WindowKind::PopUp, - is_movable: false, - screen: Some(screen), - }, - |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()), - ); + let window = cx + .add_window(notification_window_options(screen, window_size), |_| { + IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) + }); notification_windows.push(window); } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 500599db59..21fa7d4ee6 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -1,10 +1,11 @@ +use crate::notification_window_options; use call::{room, ActiveCall}; use client::User; use collections::HashMap; use gpui::{ elements::*, - geometry::{rect::RectF, vector::vec2f}, - platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, + geometry::vector::vec2f, + platform::{CursorStyle, MouseButton}, AppContext, Entity, View, ViewContext, }; use std::sync::{Arc, Weak}; @@ -20,35 +21,19 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { project_id, worktree_root_names, } => { - const PADDING: f32 = 16.; let theme = &theme::current(cx).project_shared_notification; let window_size = vec2f(theme.window_width, theme.window_height); for screen in cx.platform().screens() { - let screen_bounds = screen.bounds(); - let window = cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::new( - screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING), - window_size, - )), - titlebar: None, - center: false, - focus: false, - show: true, - kind: WindowKind::PopUp, - is_movable: false, - screen: Some(screen), - }, - |_| { + let window = + cx.add_window(notification_window_options(screen, window_size), |_| { ProjectSharedNotification::new( owner.clone(), *project_id, worktree_root_names.clone(), app_state.clone(), ) - }, - ); + }); notification_windows .entry(*project_id) .or_insert(Vec::new()) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 9f6e303cb7..c994b65573 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -135,6 +135,7 @@ pub trait InputHandler { pub trait Screen: Debug { fn as_any(&self) -> &dyn Any; fn bounds(&self) -> RectF; + fn content_bounds(&self) -> RectF; fn display_uuid(&self) -> Option; } diff --git a/crates/gpui/src/platform/mac/geometry.rs b/crates/gpui/src/platform/mac/geometry.rs index 3ff6c1d8cb..d4ca665549 100644 --- a/crates/gpui/src/platform/mac/geometry.rs +++ b/crates/gpui/src/platform/mac/geometry.rs @@ -3,10 +3,7 @@ use cocoa::{ foundation::{NSPoint, NSRect}, }; use objc::{msg_send, sel, sel_impl}; -use pathfinder_geometry::{ - rect::RectF, - vector::{vec2f, Vector2F}, -}; +use pathfinder_geometry::vector::{vec2f, Vector2F}; ///! Macos screen have a y axis that goings up from the bottom of the screen and ///! an origin at the bottom left of the main display. @@ -15,6 +12,7 @@ pub trait Vector2FExt { /// Converts self to an NSPoint with y axis pointing up. fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint; } + impl Vector2FExt for Vector2F { fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint { unsafe { @@ -25,16 +23,13 @@ impl Vector2FExt for Vector2F { } pub trait NSRectExt { - fn to_rectf(&self) -> RectF; + fn size_vec(&self) -> Vector2F; fn intersects(&self, other: Self) -> bool; } impl NSRectExt for NSRect { - fn to_rectf(&self) -> RectF { - RectF::new( - vec2f(self.origin.x as f32, self.origin.y as f32), - vec2f(self.size.width as f32, self.size.height as f32), - ) + fn size_vec(&self) -> Vector2F { + vec2f(self.size.width as f32, self.size.height as f32) } fn intersects(&self, other: Self) -> bool { diff --git a/crates/gpui/src/platform/mac/screen.rs b/crates/gpui/src/platform/mac/screen.rs index 3766a13de3..e44ea35480 100644 --- a/crates/gpui/src/platform/mac/screen.rs +++ b/crates/gpui/src/platform/mac/screen.rs @@ -1,21 +1,19 @@ -use std::{any::Any, ffi::c_void}; - +use super::ns_string; use crate::platform; use cocoa::{ appkit::NSScreen, base::{id, nil}, - foundation::{NSArray, NSDictionary}, + foundation::{NSArray, NSDictionary, NSPoint, NSRect, NSSize}, }; use core_foundation::{ number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef}, uuid::{CFUUIDGetUUIDBytes, CFUUIDRef}, }; use core_graphics::display::CGDirectDisplayID; -use pathfinder_geometry::rect::RectF; +use pathfinder_geometry::{rect::RectF, vector::vec2f}; +use std::{any::Any, ffi::c_void}; use uuid::Uuid; -use super::{geometry::NSRectExt, ns_string}; - #[link(name = "ApplicationServices", kind = "framework")] extern "C" { pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; @@ -51,6 +49,40 @@ impl Screen { } screens } + + /// Convert the given rectangle in screen coordinates from GPUI's + /// coordinate system to the AppKit coordinate system. + /// + /// In GPUI's coordinates, the origin is at the top left of the main screen, with + /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the + /// bottom left of the main screen, with the Y axis pointing upward. + pub(crate) fn screen_rect_to_native(rect: RectF) -> NSRect { + let main_screen_height = unsafe { NSScreen::mainScreen(nil).frame().size.height }; + NSRect::new( + NSPoint::new( + rect.origin_x() as f64, + main_screen_height - rect.origin_y() as f64 - rect.height() as f64, + ), + NSSize::new(rect.width() as f64, rect.height() as f64), + ) + } + + /// Convert the given rectangle in screen coordinates from the AppKit + /// coordinate system to GPUI's coordinate system. + /// + /// In GPUI's coordinates, the origin is at the top left of the main screen, with + /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the + /// bottom left of the main screen, with the Y axis pointing upward. + pub(crate) fn screen_rect_from_native(rect: NSRect) -> RectF { + let main_screen_height = unsafe { NSScreen::mainScreen(nil).frame().size.height }; + RectF::new( + vec2f( + rect.origin.x as f32, + (main_screen_height - rect.origin.y - rect.size.height) as f32, + ), + vec2f(rect.size.width as f32, rect.size.height as f32), + ) + } } impl platform::Screen for Screen { @@ -108,9 +140,10 @@ impl platform::Screen for Screen { } fn bounds(&self) -> RectF { - unsafe { - let frame = self.native_screen.frame(); - frame.to_rectf() - } + unsafe { Self::screen_rect_from_native(self.native_screen.frame()) } + } + + fn content_bounds(&self) -> RectF { + unsafe { Self::screen_rect_from_native(self.native_screen.visibleFrame()) } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index f62c33e9f5..3be425d312 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -368,32 +368,20 @@ impl WindowState { return WindowBounds::Fullscreen; } - let window_frame = self.frame(); - let screen_frame = self.native_window.screen().visibleFrame().to_rectf(); - if window_frame.size() == screen_frame.size() { + let frame = self.frame(); + let screen_size = self.native_window.screen().visibleFrame().size_vec(); + if frame.size() == screen_size { WindowBounds::Maximized } else { - WindowBounds::Fixed(window_frame) + WindowBounds::Fixed(frame) } } } - // Returns the window bounds in window coordinates fn frame(&self) -> RectF { unsafe { - let screen_frame = self.native_window.screen().visibleFrame(); - let window_frame = NSWindow::frame(self.native_window); - RectF::new( - vec2f( - window_frame.origin.x as f32, - (screen_frame.size.height - window_frame.origin.y - window_frame.size.height) - as f32, - ), - vec2f( - window_frame.size.width as f32, - window_frame.size.height as f32, - ), - ) + let frame = NSWindow::frame(self.native_window); + Screen::screen_rect_from_native(frame) } } @@ -480,21 +468,12 @@ impl MacWindow { native_window.setFrame_display_(screen.visibleFrame(), YES); } WindowBounds::Fixed(rect) => { - let screen_frame = screen.visibleFrame(); - let ns_rect = NSRect::new( - NSPoint::new( - rect.origin_x() as f64, - screen_frame.size.height - - rect.origin_y() as f64 - - rect.height() as f64, - ), - NSSize::new(rect.width() as f64, rect.height() as f64), - ); - - if ns_rect.intersects(screen_frame) { - native_window.setFrame_display_(ns_rect, YES); + let bounds = Screen::screen_rect_to_native(rect); + let screen_bounds = screen.visibleFrame(); + if bounds.intersects(screen_bounds) { + native_window.setFrame_display_(bounds, YES); } else { - native_window.setFrame_display_(screen_frame, YES); + native_window.setFrame_display_(screen_bounds, YES); } } } diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 6c11817b5c..ee3a26c6fd 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -250,6 +250,10 @@ impl super::Screen for Screen { RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.)) } + fn content_bounds(&self) -> RectF { + RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.)) + } + fn display_uuid(&self) -> Option { Some(uuid::Uuid::new_v4()) } From 8630557ece7e3ad0a29060f0c91ba5df68a2910d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 17 Aug 2023 15:30:40 -0700 Subject: [PATCH 286/326] Add action button component for rendering the search options --- crates/gpui/src/app.rs | 29 +++++++++++++- crates/gpui/src/app/action.rs | 7 ++++ crates/gpui/src/elements.rs | 18 ++++++++- crates/gpui/src/elements/component.rs | 34 ++++++++++++++++ crates/gpui/src/elements/tooltip.rs | 22 ++++++++--- crates/search/src/buffer_search.rs | 31 ++++++--------- crates/search/src/search.rs | 26 +++++++++++- crates/theme/src/theme.rs | 29 ++++++++++++-- crates/theme/src/ui.rs | 4 +- crates/workspace/src/pane.rs | 1 - styles/src/style_tree/search.ts | 57 +++++++++++++++++++++++++++ 11 files changed, 223 insertions(+), 35 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b08d9501f6..ca5c2fb8b5 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3313,11 +3313,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { &mut self, element_id: usize, initial: T, + ) -> ElementStateHandle { + self.element_state_dynamic(TypeTag::new::(), element_id, initial) + } + + pub fn element_state_dynamic( + &mut self, + tag: TypeTag, + element_id: usize, + initial: T, ) -> ElementStateHandle { let id = ElementStateId { view_id: self.view_id(), element_id, - tag: TypeId::of::(), + tag, }; self.element_states .entry(id) @@ -3331,11 +3340,20 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { ) -> ElementStateHandle { self.element_state::(element_id, T::default()) } + + pub fn default_element_state_dynamic( + &mut self, + tag: TypeTag, + element_id: usize, + ) -> ElementStateHandle { + self.element_state_dynamic::(tag, element_id, T::default()) + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct TypeTag { tag: TypeId, + composed: Option, #[cfg(debug_assertions)] tag_type_name: &'static str, } @@ -3344,6 +3362,7 @@ impl TypeTag { pub fn new() -> Self { Self { tag: TypeId::of::(), + composed: None, #[cfg(debug_assertions)] tag_type_name: std::any::type_name::(), } @@ -3352,11 +3371,17 @@ impl TypeTag { pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self { Self { tag, + composed: None, #[cfg(debug_assertions)] tag_type_name: type_name, } } + pub fn compose(mut self, other: TypeTag) -> Self { + self.composed = Some(other.tag); + self + } + #[cfg(debug_assertions)] pub(crate) fn type_name(&self) -> &'static str { self.tag_type_name @@ -4751,7 +4776,7 @@ impl Hash for AnyWeakViewHandle { pub struct ElementStateId { view_id: usize, element_id: usize, - tag: TypeId, + tag: TypeTag, } pub struct ElementStateHandle { diff --git a/crates/gpui/src/app/action.rs b/crates/gpui/src/app/action.rs index c6b43e489b..23eb4da730 100644 --- a/crates/gpui/src/app/action.rs +++ b/crates/gpui/src/app/action.rs @@ -1,10 +1,13 @@ use std::any::{Any, TypeId}; +use crate::TypeTag; + pub trait Action: 'static { fn id(&self) -> TypeId; fn namespace(&self) -> &'static str; fn name(&self) -> &'static str; fn as_any(&self) -> &dyn Any; + fn type_tag(&self) -> TypeTag; fn boxed_clone(&self) -> Box; fn eq(&self, other: &dyn Action) -> bool; @@ -107,6 +110,10 @@ macro_rules! __impl_action { } } + fn type_tag(&self) -> $crate::TypeTag { + $crate::TypeTag::new::() + } + $from_json_fn } }; diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index f1be9b34ae..03caae8dd9 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -34,8 +34,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext, - WeakViewHandle, WindowContext, + json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, TypeTag, View, + ViewContext, WeakViewHandle, WindowContext, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -172,6 +172,20 @@ pub trait Element: 'static { FlexItem::new(self.into_any()).float() } + fn with_dynamic_tooltip( + self, + tag: TypeTag, + id: usize, + text: impl Into>, + action: Option>, + style: TooltipStyle, + cx: &mut ViewContext, + ) -> Tooltip + where + Self: 'static + Sized, + { + Tooltip::new_dynamic(tag, id, text, action, style, self.into_any(), cx) + } fn with_tooltip( self, id: usize, diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 2f9cc6cce6..018dc644c6 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -7,6 +7,34 @@ use crate::{ ViewContext, }; +use super::Empty; + +pub trait GeneralComponent { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; +} + +pub trait StyleableComponent { + type Style: Clone; + type Output: GeneralComponent; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +impl GeneralComponent for () { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl StyleableComponent for () { + type Style = (); + type Output = (); + + fn with_style(self, _: Self::Style) -> Self::Output { + () + } +} + pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; @@ -18,6 +46,12 @@ pub trait Component { } } +impl Component for C { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(v, cx) + } +} + pub struct ComponentAdapter { component: Option, phantom: PhantomData, diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 0ba0110303..0ce34fcc14 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -7,7 +7,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::json, Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, - Task, View, ViewContext, + Task, TypeTag, View, ViewContext, }; use schemars::JsonSchema; use serde::Deserialize; @@ -61,11 +61,23 @@ impl Tooltip { child: AnyElement, cx: &mut ViewContext, ) -> Self { - struct ElementState(Tag); - struct MouseEventHandlerState(Tag); + Self::new_dynamic(TypeTag::new::(), id, text, action, style, child, cx) + } + + pub fn new_dynamic( + mut tag: TypeTag, + id: usize, + text: impl Into>, + action: Option>, + style: TooltipStyle, + child: AnyElement, + cx: &mut ViewContext, + ) -> Self { + tag = tag.compose(TypeTag::new::()); + let focused_view_id = cx.focused_view_id(); - let state_handle = cx.default_element_state::, Rc>(id); + let state_handle = cx.default_element_state_dynamic::>(tag, id); let state = state_handle.read(cx).clone(); let text = text.into(); @@ -95,7 +107,7 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::new::, _>(id, cx, |_, _| child) + let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child) .on_hover(move |e, _, cx| { let position = e.position; if e.started { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c31236023b..0c5b5717d6 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -19,6 +19,7 @@ use gpui::{ use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; + use util::ResultExt; use workspace::{ item::ItemHandle, @@ -167,23 +168,17 @@ impl View for BufferSearchBar { cx, ) }; - let render_search_option = - |options: bool, icon, option, cx: &mut ViewContext| { - options.then(|| { - let is_active = self.search_options.contains(option); - crate::search_bar::render_option_button_icon::( - is_active, - icon, - option.bits as usize, - format!("Toggle {}", option.label()), - option.to_toggle_action(), - move |_, this, cx| { - this.toggle_search_option(option, cx); - }, - cx, - ) - }) - }; + let render_search_option = |options: bool, icon, option| { + options.then(|| { + let is_active = self.search_options.contains(option); + option.as_button( + is_active, + icon, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + ) + }) + }; let match_count = self .active_searchable_item .as_ref() @@ -242,13 +237,11 @@ impl View for BufferSearchBar { supported_options.case, "icons/case_insensitive_12.svg", SearchOptions::CASE_SENSITIVE, - cx, )) .with_children(render_search_option( supported_options.word, "icons/word_search_12.svg", SearchOptions::WHOLE_WORD, - cx, )) .flex_float() .contained(), diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 079a8965eb..ec6f97b04d 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,9 +1,14 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; -use gpui::{actions, Action, AppContext}; +use gpui::{ + actions, + elements::{Component, StyleableComponent, TooltipStyle}, + Action, AnyElement, AppContext, Element, View, +}; pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; +use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle}; pub mod buffer_search; mod history; @@ -69,4 +74,23 @@ impl SearchOptions { options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); options } + + pub fn as_button( + &self, + active: bool, + icon: &str, + tooltip_style: TooltipStyle, + button_style: ToggleIconButtonStyle, + ) -> AnyElement { + ActionButton::new_dynamic( + self.to_toggle_action(), + format!("Toggle {}", self.label()), + tooltip_style, + ) + .with_contents(theme::components::svg::Svg::new(icon.to_owned())) + .toggleable(active) + .with_style(button_style) + .into_element() + .into_any() + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4de0076825..80e823632a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1,7 +1,9 @@ +pub mod components; mod theme_registry; mod theme_settings; pub mod ui; +use components::ToggleIconButtonStyle; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -13,7 +15,7 @@ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle}; +use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; pub use theme_registry::*; pub use theme_settings::*; @@ -182,7 +184,7 @@ pub struct CopilotAuth { pub prompting: CopilotAuthPrompting, pub not_authorized: CopilotAuthNotAuthorized, pub authorized: CopilotAuthAuthorized, - pub cta_button: ButtonStyle, + pub cta_button: CopilotCTAButton, pub header: IconStyle, } @@ -196,7 +198,7 @@ pub struct CopilotAuthPrompting { #[derive(Deserialize, Default, Clone, JsonSchema)] pub struct DeviceCode { pub text: TextStyle, - pub cta: ButtonStyle, + pub cta: CopilotCTAButton, pub left: f32, pub left_container: ContainerStyle, pub right: f32, @@ -420,6 +422,7 @@ pub struct Search { pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, pub option_button: Toggleable>, + pub option_button_component: ToggleIconButtonStyle, pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, @@ -887,12 +890,32 @@ pub struct Interactive { pub disabled: Option, } +impl Interactive<()> { + pub fn new_blank() -> Self { + Self { + default: (), + hovered: None, + clicked: None, + disabled: None, + } + } +} + #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] pub struct Toggleable { active: T, inactive: T, } +impl Toggleable<()> { + pub fn new_blank() -> Self { + Self { + active: (), + inactive: (), + } + } +} + impl Toggleable { pub fn new(active: T, inactive: T) -> Self { Self { active, inactive } diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index f4a249e74e..7f0b05731e 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -145,12 +145,12 @@ pub fn keystroke_label( .with_style(label_style.container) } -pub type ButtonStyle = Interactive; +pub type CopilotCTAButton = Interactive; pub fn cta_button( label: L, max_width: f32, - style: &ButtonStyle, + style: &CopilotCTAButton, cx: &mut ViewContext, f: F, ) -> MouseEventHandler diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3b295af802..528b1e2029 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -320,7 +320,6 @@ impl Pane { can_drop: Rc::new(|_, _| true), can_split: true, render_tab_bar_buttons: Rc::new(move |pane, cx| { - let tooltip_style = theme::current(cx).tooltip.clone(); Flex::row() // New menu .with_child(Self::render_tab_bar_button( diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4c0df69804..4493634a8e 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -96,6 +96,63 @@ export default function search(): any { }, }, }), + option_button_component: toggleable({ + base: interactive({ + base: { + icon_size: 14, + color: foreground(theme.highest, "variant"), + + button_width: 32, + background: background(theme.highest, "on"), + corner_radius: 2, + margin: { right: 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: 14, + button_width: 32, + 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"), + }, + }, + }, + }), action_button: toggleable({ base: interactive({ base: { From 6eba0ef63092e75b6647074b2e8d73d842f182fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Aug 2023 15:31:27 -0700 Subject: [PATCH 287/326] Return to master branch of alacritty --- Cargo.lock | 6 +++--- crates/terminal/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb71ea51dd..69285a1abf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ dependencies = [ [[package]] name = "alacritty_config" version = "0.1.2-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5" +source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca" dependencies = [ "log", "serde", @@ -136,7 +136,7 @@ dependencies = [ [[package]] name = "alacritty_config_derive" version = "0.2.2-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5" +source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca" dependencies = [ "proc-macro2", "quote", @@ -146,7 +146,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" version = "0.20.0-dev" -source = "git+https://github.com/zed-industries/alacritty?rev=f6d001ba8080ebfab6822106a436c64b677a44d5#f6d001ba8080ebfab6822106a436c64b677a44d5" +source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca" dependencies = [ "alacritty_config", "alacritty_config_derive", diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 18c0f8be3c..ce2a158e20 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -16,7 +16,7 @@ db = { path = "../db" } theme = { path = "../theme" } util = { path = "../util" } -alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "f6d001ba8080ebfab6822106a436c64b677a44d5" } +alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } smallvec.workspace = true smol.workspace = true From cd2ef784ea3046b9a8d227feb449a186e15d0f01 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Aug 2023 16:12:52 -0700 Subject: [PATCH 288/326] Translate coordinates using the primary screen not the main screen --- crates/gpui/src/platform/mac/screen.rs | 45 ++++++++++++-------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/crates/gpui/src/platform/mac/screen.rs b/crates/gpui/src/platform/mac/screen.rs index e44ea35480..002f355baa 100644 --- a/crates/gpui/src/platform/mac/screen.rs +++ b/crates/gpui/src/platform/mac/screen.rs @@ -25,43 +25,38 @@ pub struct Screen { } impl Screen { + /// Get the screen with the given UUID. pub fn find_by_id(uuid: Uuid) -> Option { - unsafe { - let native_screens = NSScreen::screens(nil); - (0..NSArray::count(native_screens)) - .into_iter() - .map(|ix| Screen { - native_screen: native_screens.objectAtIndex(ix), - }) - .find(|screen| platform::Screen::display_uuid(screen) == Some(uuid)) - } + Self::all().find(|screen| platform::Screen::display_uuid(screen) == Some(uuid)) } - pub fn all() -> Vec { - let mut screens = Vec::new(); + /// Get the primary screen - the one with the menu bar, and whose bottom left + /// corner is at the origin of the AppKit coordinate system. + fn primary() -> Self { + Self::all().next().unwrap() + } + + pub fn all() -> impl Iterator { unsafe { let native_screens = NSScreen::screens(nil); - for ix in 0..NSArray::count(native_screens) { - screens.push(Screen { - native_screen: native_screens.objectAtIndex(ix), - }); - } + (0..NSArray::count(native_screens)).map(move |ix| Screen { + native_screen: native_screens.objectAtIndex(ix), + }) } - screens } /// Convert the given rectangle in screen coordinates from GPUI's /// coordinate system to the AppKit coordinate system. /// - /// In GPUI's coordinates, the origin is at the top left of the main screen, with + /// In GPUI's coordinates, the origin is at the top left of the primary screen, with /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the - /// bottom left of the main screen, with the Y axis pointing upward. + /// bottom left of the primary screen, with the Y axis pointing upward. pub(crate) fn screen_rect_to_native(rect: RectF) -> NSRect { - let main_screen_height = unsafe { NSScreen::mainScreen(nil).frame().size.height }; + let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height }; NSRect::new( NSPoint::new( rect.origin_x() as f64, - main_screen_height - rect.origin_y() as f64 - rect.height() as f64, + primary_screen_height - rect.origin_y() as f64 - rect.height() as f64, ), NSSize::new(rect.width() as f64, rect.height() as f64), ) @@ -70,15 +65,15 @@ impl Screen { /// Convert the given rectangle in screen coordinates from the AppKit /// coordinate system to GPUI's coordinate system. /// - /// In GPUI's coordinates, the origin is at the top left of the main screen, with + /// In GPUI's coordinates, the origin is at the top left of the primary screen, with /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the - /// bottom left of the main screen, with the Y axis pointing upward. + /// bottom left of the primary screen, with the Y axis pointing upward. pub(crate) fn screen_rect_from_native(rect: NSRect) -> RectF { - let main_screen_height = unsafe { NSScreen::mainScreen(nil).frame().size.height }; + let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height }; RectF::new( vec2f( rect.origin.x as f32, - (main_screen_height - rect.origin.y - rect.size.height) as f32, + (primary_screen_height - rect.origin.y - rect.size.height) as f32, ), vec2f(rect.size.width as f32, rect.size.height as f32), ) From b0ba0f885175b466ee8cf1db88e569b00d64800e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 17 Aug 2023 17:03:54 -0600 Subject: [PATCH 289/326] Fix visual objects Adds 'a'/'i' in visual mode --- assets/keymaps/vim.json | 20 +++++++- crates/vim/src/object.rs | 29 +++++++++++ crates/vim/src/visual.rs | 54 +++++++++++++++----- crates/vim/test_data/test_visual_object.json | 19 +++++++ 4 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 crates/vim/test_data/test_visual_object.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 5281ec4213..a93d8aa3ec 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -366,7 +366,7 @@ } }, { - "context": "Editor && vim_mode == visual && !VimWaiting", + "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject", "bindings": { "u": "editor::Undo", "o": "vim::OtherEnd", @@ -400,7 +400,23 @@ "Normal" ], ">": "editor::Indent", - "<": "editor::Outdent" + "<": "editor::Outdent", + "i": [ + "vim::PushOperator", + { + "Object": { + "around": false + } + } + ], + "a": [ + "vim::PushOperator", + { + "Object": { + "around": true + } + } + ], } }, { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 14166d2dff..c203a89f72 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -84,6 +84,35 @@ impl Object { | Object::SquareBrackets => true, } } + + pub fn always_expands_both_ways(self) -> bool { + match self { + Object::Word { .. } | Object::Sentence => false, + Object::Quotes + | Object::BackQuotes + | Object::DoubleQuotes + | Object::Parentheses + | Object::SquareBrackets + | Object::CurlyBrackets + | Object::AngleBrackets => true, + } + } + + pub fn target_visual_mode(self, current_mode: Mode) -> Mode { + match self { + Object::Word { .. } if current_mode == Mode::VisualLine => Mode::Visual, + Object::Word { .. } => current_mode, + Object::Sentence + | Object::Quotes + | Object::BackQuotes + | Object::DoubleQuotes + | Object::Parentheses + | Object::SquareBrackets + | Object::CurlyBrackets + | Object::AngleBrackets => Mode::Visual, + } + } + pub fn range( self, map: &DisplaySnapshot, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 4065657e59..df7c8cfa45 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, cmp, sync::Arc}; use collections::HashMap; use editor::{ @@ -198,6 +198,11 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { if let Some(Operator::Object { around }) = vim.active_operator() { vim.pop_operator(cx); + let current_mode = vim.state().mode; + let target_mode = object.target_visual_mode(current_mode); + if target_mode != current_mode { + vim.switch_mode(target_mode, true, cx); + } vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { @@ -213,20 +218,21 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { if let Some(range) = object.range(map, head, around) { if !range.is_empty() { - let expand_both_ways = if selection.is_empty() { - true - // contains only one character - } else if let Some((_, start)) = - map.reverse_chars_at(selection.end).next() - { - selection.start == start - } else { - false - }; + let expand_both_ways = + if object.always_expands_both_ways() || selection.is_empty() { + true + // contains only one character + } else if let Some((_, start)) = + map.reverse_chars_at(selection.end).next() + { + selection.start == start + } else { + false + }; if expand_both_ways { - selection.start = range.start; - selection.end = range.end; + selection.start = cmp::min(selection.start, range.start); + selection.end = cmp::max(selection.end, range.end); } else if selection.reversed { selection.start = range.start; } else { @@ -1030,6 +1036,28 @@ mod test { .await; } + #[gpui::test] + async fn test_visual_object(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("hello (in [parˇens] o)").await; + cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await; + cx.simulate_shared_keystrokes(["a", "]"]).await; + cx.assert_shared_state("hello (in «[parens]ˇ» o)").await; + assert_eq!(cx.mode(), Mode::Visual); + cx.simulate_shared_keystrokes(["i", "("]).await; + cx.assert_shared_state("hello («in [parens] oˇ»)").await; + + cx.set_shared_state("hello in a wˇord again.").await; + cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"]) + .await; + cx.assert_shared_state("hello in a w«ordˇ» again.").await; + assert_eq!(cx.mode(), Mode::VisualBlock); + cx.simulate_shared_keystrokes(["o", "a", "s"]).await; + cx.assert_shared_state("«ˇhello in a word» again.").await; + assert_eq!(cx.mode(), Mode::Visual); + } + #[gpui::test] async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/test_data/test_visual_object.json b/crates/vim/test_data/test_visual_object.json new file mode 100644 index 0000000000..7c95a8dc73 --- /dev/null +++ b/crates/vim/test_data/test_visual_object.json @@ -0,0 +1,19 @@ +{"Put":{"state":"hello (in [parˇens] o)"}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"a"} +{"Key":"]"} +{"Get":{"state":"hello (in «[parens]ˇ» o)","mode":"Visual"}} +{"Key":"i"} +{"Key":"("} +{"Get":{"state":"hello («in [parens] oˇ»)","mode":"Visual"}} +{"Put":{"state":"hello in a wˇord again."}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"hello in a w«ordˇ» again.","mode":"VisualBlock"}} +{"Key":"o"} +{"Key":"a"} +{"Key":"s"} +{"Get":{"state":"«ˇhello in a word» again.","mode":"VisualBlock"}} From 21fa6090b8bc8d270b203b41544ad66d69727efb Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 17 Aug 2023 15:30:40 -0700 Subject: [PATCH 290/326] Add action button component for rendering the search options --- crates/search/src/buffer_search.rs | 37 ++-- crates/search/src/search.rs | 11 +- crates/theme/src/components.rs | 339 +++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+), 22 deletions(-) create mode 100644 crates/theme/src/components.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 0c5b5717d6..4078cb572d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -168,16 +168,13 @@ impl View for BufferSearchBar { cx, ) }; - let render_search_option = |options: bool, icon, option| { - options.then(|| { - let is_active = self.search_options.contains(option); - option.as_button( - is_active, - icon, - theme.tooltip.clone(), - theme.search.option_button_component.clone(), - ) - }) + let search_option_button = |option| { + let is_active = self.search_options.contains(option); + option.as_button( + is_active, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + ) }; let match_count = self .active_searchable_item @@ -233,16 +230,16 @@ impl View for BufferSearchBar { .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( Flex::row() - .with_children(render_search_option( - supported_options.case, - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - )) - .with_children(render_search_option( - supported_options.word, - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - )) + .with_children( + supported_options + .case + .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), + ) + .with_children( + supported_options + .word + .then(|| search_option_button(SearchOptions::WHOLE_WORD)), + ) .flex_float() .contained(), ) diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index ec6f97b04d..c31ea6f2f8 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -56,6 +56,14 @@ impl SearchOptions { } } + pub fn icon(&self) -> &'static str { + match *self { + SearchOptions::WHOLE_WORD => "icons/word_search_12.svg", + SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg", + _ => panic!("{:?} is not a named SearchOption", self), + } + } + pub fn to_toggle_action(&self) -> Box { match *self { SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), @@ -78,7 +86,6 @@ impl SearchOptions { pub fn as_button( &self, active: bool, - icon: &str, tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { @@ -87,7 +94,7 @@ impl SearchOptions { format!("Toggle {}", self.label()), tooltip_style, ) - .with_contents(theme::components::svg::Svg::new(icon.to_owned())) + .with_contents(theme::components::svg::Svg::new(self.icon())) .toggleable(active) .with_style(button_style) .into_element() diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs new file mode 100644 index 0000000000..fce7ad825c --- /dev/null +++ b/crates/theme/src/components.rs @@ -0,0 +1,339 @@ +use gpui::elements::StyleableComponent; + +use crate::{Interactive, Toggleable}; + +use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; + +pub type ToggleIconButtonStyle = Toggleable>>; + +pub trait ComponentExt { + fn toggleable(self, active: bool) -> Toggle; +} + +impl ComponentExt for C { + fn toggleable(self, active: bool) -> Toggle { + Toggle::new(self, active) + } +} + +pub mod toggle { + use gpui::elements::{GeneralComponent, StyleableComponent}; + + use crate::Toggleable; + + pub struct Toggle { + style: S, + active: bool, + component: C, + } + + impl Toggle { + pub fn new(component: C, active: bool) -> Self { + Toggle { + active, + component, + style: (), + } + } + } + + impl StyleableComponent for Toggle { + type Style = Toggleable; + + type Output = Toggle; + + fn with_style(self, style: Self::Style) -> Self::Output { + Toggle { + active: self.active, + component: self.component, + style, + } + } + } + + impl GeneralComponent for Toggle> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + self.component + .with_style(self.style.in_state(self.active).clone()) + .render(v, cx) + } + } +} + +pub mod action_button { + use std::borrow::Cow; + + use gpui::{ + elements::{ + ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle, + }, + platform::{CursorStyle, MouseButton}, + Action, Element, TypeTag, View, + }; + use schemars::JsonSchema; + use serde_derive::Deserialize; + + use crate::Interactive; + + pub struct ActionButton { + action: Box, + tooltip: Cow<'static, str>, + tooltip_style: TooltipStyle, + tag: TypeTag, + contents: C, + style: Interactive, + } + + #[derive(Clone, Deserialize, Default, JsonSchema)] + pub struct ButtonStyle { + #[serde(flatten)] + container: ContainerStyle, + button_width: Option, + button_height: Option, + #[serde(flatten)] + contents: C, + } + + impl ActionButton<(), ()> { + pub fn new_dynamic( + action: Box, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + ) -> Self { + Self { + contents: (), + tag: action.type_tag(), + style: Interactive::new_blank(), + tooltip: tooltip.into(), + tooltip_style, + action, + } + } + + pub fn new( + action: A, + tooltip: impl Into>, + tooltip_style: TooltipStyle, + ) -> Self { + Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + } + + pub fn with_contents(self, contents: C) -> ActionButton { + ActionButton { + action: self.action, + tag: self.tag, + style: self.style, + tooltip: self.tooltip, + tooltip_style: self.tooltip_style, + contents, + } + } + } + + impl StyleableComponent for ActionButton { + type Style = Interactive>; + type Output = ActionButton>; + + fn with_style(self, style: Self::Style) -> Self::Output { + ActionButton { + action: self.action, + tag: self.tag, + contents: self.contents, + tooltip: self.tooltip, + tooltip_style: self.tooltip_style, + style, + } + } + } + + impl GeneralComponent for ActionButton> { + fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + let style = self.style.style_for(state); + let mut contents = self + .contents + .with_style(style.contents.to_owned()) + .render(v, cx) + .contained() + .with_style(style.container) + .constrained(); + + if let Some(height) = style.button_height { + contents = contents.with_height(height); + } + + if let Some(width) = style.button_width { + contents = contents.with_width(width); + } + + contents.into_any() + }) + .on_click(MouseButton::Left, { + let action = self.action.boxed_clone(); + move |_, _, cx| { + cx.window() + .dispatch_action(cx.view_id(), action.as_ref(), cx); + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_dynamic_tooltip( + self.tag, + 0, + self.tooltip, + Some(self.action), + self.tooltip_style, + cx, + ) + .into_any() + } + } +} + +pub mod svg { + use std::borrow::Cow; + + use gpui::{ + elements::{GeneralComponent, StyleableComponent}, + Element, + }; + use schemars::JsonSchema; + use serde::Deserialize; + + #[derive(Clone, Default, JsonSchema)] + pub struct SvgStyle { + icon_width: f32, + icon_height: f32, + color: gpui::color::Color, + } + + impl<'de> Deserialize<'de> for SvgStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + pub enum IconSize { + IconSize { icon_size: f32 }, + Dimensions { width: f32, height: f32 }, + } + + #[derive(Deserialize)] + struct SvgStyleHelper { + #[serde(flatten)] + size: IconSize, + color: gpui::color::Color, + } + + let json = SvgStyleHelper::deserialize(deserializer)?; + let color = json.color; + + let result = match json.size { + IconSize::IconSize { icon_size } => SvgStyle { + icon_width: icon_size, + icon_height: icon_size, + color, + }, + IconSize::Dimensions { width, height } => SvgStyle { + icon_width: width, + icon_height: height, + color, + }, + }; + + Ok(result) + } + } + + pub struct Svg { + path: Cow<'static, str>, + style: S, + } + + impl Svg<()> { + pub fn new(path: impl Into>) -> Self { + Self { + path: path.into(), + style: (), + } + } + } + + impl StyleableComponent for Svg<()> { + type Style = SvgStyle; + + type Output = Svg; + + fn with_style(self, style: Self::Style) -> Self::Output { + Svg { + path: self.path, + style, + } + } + } + + impl GeneralComponent for Svg { + fn render( + self, + _: &mut V, + _: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + gpui::elements::Svg::new(self.path) + .with_color(self.style.color) + .constrained() + .with_width(self.style.icon_width) + .with_height(self.style.icon_height) + .into_any() + } + } +} + +pub mod label { + use std::borrow::Cow; + + use gpui::{ + elements::{GeneralComponent, LabelStyle, StyleableComponent}, + Element, + }; + + pub struct Label { + text: Cow<'static, str>, + style: S, + } + + impl Label<()> { + pub fn new(text: impl Into>) -> Self { + Self { + text: text.into(), + style: (), + } + } + } + + impl StyleableComponent for Label<()> { + type Style = LabelStyle; + + type Output = Label; + + fn with_style(self, style: Self::Style) -> Self::Output { + Label { + text: self.text, + style, + } + } + } + + impl GeneralComponent for Label { + fn render( + self, + _: &mut V, + _: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new(self.text, self.style).into_any() + } + } +} From c0f042b39a595da6f93de2268e5990a3c5604912 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 17 Aug 2023 17:25:50 -0700 Subject: [PATCH 291/326] Remove semantic search UI --- crates/gpui/src/elements/component.rs | 51 +++-- crates/search/src/buffer_search.rs | 7 +- crates/search/src/mode.rs | 20 +- crates/search/src/project_search.rs | 296 ++++---------------------- crates/search/src/search.rs | 7 +- crates/settings/src/settings.rs | 2 + crates/theme/src/components.rs | 9 +- crates/workspace/src/pane.rs | 4 - 8 files changed, 89 insertions(+), 307 deletions(-) diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 018dc644c6..a26355a539 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use pathfinder_geometry::{rect::RectF, vector::Vector2F}; use crate::{ @@ -52,22 +50,26 @@ impl Component for C { } } -pub struct ComponentAdapter { +pub struct ComponentAdapter { component: Option, - phantom: PhantomData, + element: Option>, + #[cfg(debug_assertions)] + _component_name: &'static str, } -impl ComponentAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), - phantom: PhantomData, + element: None, + #[cfg(debug_assertions)] + _component_name: std::any::type_name::(), } } } impl + 'static> Element for ComponentAdapter { - type LayoutState = AnyElement; + type LayoutState = (); type PaintState = (); @@ -77,10 +79,12 @@ impl + 'static> Element for ComponentAdapter { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let component = self.component.take().unwrap(); - let mut element = component.render(view, cx.view_context()); - let constraint = element.layout(constraint, view, cx); - (constraint, element) + if self.element.is_none() { + let component = self.component.take().unwrap(); + self.element = Some(component.render(view, cx.view_context())); + } + let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx); + (constraint, ()) } fn paint( @@ -88,11 +92,14 @@ impl + 'static> Element for ComponentAdapter { scene: &mut SceneBuilder, bounds: RectF, visible_bounds: RectF, - layout: &mut Self::LayoutState, + _: &mut Self::LayoutState, view: &mut V, cx: &mut PaintContext, ) -> Self::PaintState { - layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + self.element + .as_mut() + .unwrap() + .paint(scene, bounds.origin(), visible_bounds, view, cx) } fn rect_for_text_range( @@ -100,25 +107,35 @@ impl + 'static> Element for ComponentAdapter { range_utf16: std::ops::Range, _: RectF, _: RectF, - element: &Self::LayoutState, + _: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> Option { - element.rect_for_text_range(range_utf16, view, cx) + self.element + .as_ref() + .unwrap() + .rect_for_text_range(range_utf16, view, cx) } fn debug( &self, _: RectF, - element: &Self::LayoutState, + _: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> serde_json::Value { + #[cfg(debug_assertions)] + let component_name = self._component_name; + + #[cfg(not(debug_assertions))] + let component_name = "Unknown"; + serde_json::json!({ "type": "ComponentAdapter", - "child": element.debug(view, cx), + "child": self.element.as_ref().unwrap().debug(view, cx), + "component_name": component_name }) } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4078cb572d..b65c7222a4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -523,11 +523,6 @@ impl BufferSearchBar { } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { - assert_ne!( - mode, - SearchMode::Semantic, - "Semantic search is not supported in buffer search" - ); if mode == self.current_mode { return; } @@ -802,7 +797,7 @@ impl BufferSearchBar { } } fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { - self.activate_search_mode(next_mode(&self.current_mode, false), cx); + self.activate_search_mode(next_mode(&self.current_mode), cx); } fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { let mut should_propagate = true; diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 2c180be761..0163528951 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -1,12 +1,11 @@ use gpui::Action; -use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; +use crate::{ActivateRegexMode, ActivateTextMode}; // TODO: Update the default search mode to get from config #[derive(Copy, Clone, Debug, Default, PartialEq)] pub enum SearchMode { #[default] Text, - Semantic, Regex, } @@ -20,7 +19,6 @@ impl SearchMode { pub(crate) fn label(&self) -> &'static str { match self { SearchMode::Text => "Text", - SearchMode::Semantic => "Semantic", SearchMode::Regex => "Regex", } } @@ -28,7 +26,6 @@ impl SearchMode { pub(crate) fn region_id(&self) -> usize { match self { SearchMode::Text => 3, - SearchMode::Semantic => 4, SearchMode::Regex => 5, } } @@ -36,7 +33,6 @@ impl SearchMode { pub(crate) fn tooltip_text(&self) -> &'static str { match self { SearchMode::Text => "Activate Text Search", - SearchMode::Semantic => "Activate Semantic Search", SearchMode::Regex => "Activate Regex Search", } } @@ -44,7 +40,6 @@ impl SearchMode { pub(crate) fn activate_action(&self) -> Box { match self { SearchMode::Text => Box::new(ActivateTextMode), - SearchMode::Semantic => Box::new(ActivateSemanticMode), SearchMode::Regex => Box::new(ActivateRegexMode), } } @@ -53,7 +48,6 @@ impl SearchMode { match self { SearchMode::Regex => true, SearchMode::Text => true, - SearchMode::Semantic => true, } } @@ -67,22 +61,14 @@ impl SearchMode { pub(crate) fn button_side(&self) -> Option { match self { SearchMode::Text => Some(Side::Left), - SearchMode::Semantic => None, SearchMode::Regex => Some(Side::Right), } } } -pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { - let next_text_state = if semantic_enabled { - SearchMode::Semantic - } else { - SearchMode::Regex - }; - +pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode { match mode { - SearchMode::Text => next_text_state, - SearchMode::Semantic => SearchMode::Regex, + SearchMode::Text => SearchMode::Regex, SearchMode::Regex => SearchMode::Text, } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e15bd0e27a..abb7395e50 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,10 +2,10 @@ use crate::{ history::SearchHistory, mode::SearchMode, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, - ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, + SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; -use anyhow::{Context, Result}; +use anyhow::Context; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, @@ -13,8 +13,6 @@ use editor::{ }; use futures::StreamExt; -use gpui::platform::PromptLevel; - use gpui::{ actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, @@ -22,12 +20,10 @@ use gpui::{ }; use menu::Confirm; -use postage::stream::Stream; use project::{ - search::{PathMatcher, SearchInputs, SearchQuery}, + search::{PathMatcher, SearchQuery}, Entry, Project, }; -use semantic_index::SemanticIndex; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -118,8 +114,6 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, - semantic_state: Option, - semantic_permissioned: Option, search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, @@ -131,12 +125,6 @@ pub struct ProjectSearchView { current_mode: SearchMode, } -struct SemanticSearchState { - file_count: usize, - outstanding_file_count: usize, - _progress_task: Task<()>, -} - pub struct ProjectSearchBar { active_project_search: Option>, subscription: Option, @@ -218,60 +206,6 @@ impl ProjectSearch { })); cx.notify(); } - - fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { - let search = SemanticIndex::global(cx).map(|index| { - index.update(cx, |semantic_index, cx| { - semantic_index.search_project( - self.project.clone(), - inputs.as_str().to_owned(), - 10, - inputs.files_to_include().to_vec(), - inputs.files_to_exclude().to_vec(), - cx, - ) - }) - }); - self.search_id += 1; - self.match_ranges.clear(); - self.search_history.add(inputs.as_str().to_string()); - self.no_results = Some(true); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - let results = search?.await.log_err()?; - - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])) - .collect(); - - excerpts.stream_excerpts_with_context_lines(matches, 3, cx) - }) - }); - - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } - this.no_results = Some(false); - cx.notify(); - }); - } - - this.update(&mut cx, |this, cx| { - this.pending_search.take(); - cx.notify(); - }); - - None - })); - cx.notify(); - } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -311,27 +245,10 @@ impl View for ProjectSearchView { } else { match current_mode { SearchMode::Text => Cow::Borrowed("Text search all files and folders"), - SearchMode::Semantic => { - Cow::Borrowed("Search all code objects using Natural Language") - } SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), } }; - let semantic_status = if let Some(semantic) = &self.semantic_state { - if semantic.outstanding_file_count > 0 { - format!( - "Indexing: {} of {}...", - semantic.file_count - semantic.outstanding_file_count, - semantic.file_count - ) - } else { - "Indexing complete".to_string() - } - } else { - "Indexing: ...".to_string() - }; - let minor_text = if let Some(no_results) = model.no_results { if model.pending_search.is_none() && no_results { vec!["No results found in this project for the provided query".to_owned()] @@ -339,19 +256,11 @@ impl View for ProjectSearchView { vec![] } } else { - match current_mode { - SearchMode::Semantic => vec![ - "".to_owned(), - semantic_status, - "Simply explain the code you are looking to find.".to_owned(), - "ex. 'prompt user for permissions to index their project'".to_owned(), - ], - _ => vec![ - "".to_owned(), - "Include/exclude specific paths with the filter option.".to_owned(), - "Matching exact word and/or casing is available too.".to_owned(), - ], - } + vec![ + "".to_owned(), + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ] }; let previous_query_keystrokes = @@ -630,49 +539,6 @@ impl ProjectSearchView { self.search_options.toggle(option); } - fn index_project(&mut self, cx: &mut ViewContext) { - if let Some(semantic_index) = SemanticIndex::global(cx) { - // Semantic search uses no options - self.search_options = SearchOptions::none(); - - let project = self.model.read(cx).project.clone(); - let index_task = semantic_index.update(cx, |semantic_index, cx| { - semantic_index.index_project(project, cx) - }); - - cx.spawn(|search_view, mut cx| async move { - let (files_to_index, mut files_remaining_rx) = index_task.await?; - - search_view.update(&mut cx, |search_view, cx| { - cx.notify(); - search_view.semantic_state = Some(SemanticSearchState { - file_count: files_to_index, - outstanding_file_count: files_to_index, - _progress_task: cx.spawn(|search_view, mut cx| async move { - while let Some(count) = files_remaining_rx.recv().await { - search_view - .update(&mut cx, |search_view, cx| { - if let Some(semantic_search_state) = - &mut search_view.semantic_state - { - semantic_search_state.outstanding_file_count = count; - cx.notify(); - if count == 0 { - return; - } - } - }) - .ok(); - } - }), - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - fn clear_search(&mut self, cx: &mut ViewContext) { self.model.update(cx, |model, cx| { model.pending_search = None; @@ -696,56 +562,7 @@ impl ProjectSearchView { self.current_mode = mode; match mode { - SearchMode::Semantic => { - let has_permission = self.semantic_permissioned(cx); - self.active_match_index = None; - cx.spawn(|this, mut cx| async move { - let has_permission = has_permission.await?; - - if !has_permission { - let mut answer = this.update(&mut cx, |this, cx| { - let project = this.model.read(cx).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 semantic search? This requires sending code to the OpenAI API", project_name, - if is_plural { - "s" - } else {""}); - 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 { - this.update(&mut cx, |this, cx| { - this.semantic_permissioned = Some(false); - debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected"); - this.activate_search_mode(previous_mode, cx); - })?; - return anyhow::Ok(()); - } - } - - this.update(&mut cx, |this, cx| { - this.index_project(cx); - })?; - - anyhow::Ok(()) - }).detach_and_log_err(cx); - } SearchMode::Regex | SearchMode::Text => { - self.semantic_state = None; self.active_match_index = None; } } @@ -843,8 +660,6 @@ impl ProjectSearchView { model, query_editor, results_editor, - semantic_state: None, - semantic_permissioned: None, search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, @@ -858,18 +673,6 @@ impl ProjectSearchView { this } - fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { - if let Some(value) = self.semantic_permissioned { - return Task::ready(Ok(value)); - } - - SemanticIndex::global(cx) - .map(|semantic| { - let project = self.model.read(cx).project.clone(); - semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx)) - }) - .unwrap_or(Task::ready(Ok(false))) - } pub fn new_search_in_directory( workspace: &mut Workspace, dir_entry: &Entry, @@ -945,26 +748,8 @@ impl ProjectSearchView { } fn search(&mut self, cx: &mut ViewContext) { - let mode = self.current_mode; - match mode { - SearchMode::Semantic => { - if let Some(semantic) = &mut self.semantic_state { - if semantic.outstanding_file_count > 0 { - return; - } - - if let Some(query) = self.build_search_query(cx) { - self.model - .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx)); - } - } - } - - _ => { - if let Some(query) = self.build_search_query(cx) { - self.model.update(cx, |model, cx| model.search(query, cx)); - } - } + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); } } @@ -1164,8 +949,7 @@ impl ProjectSearchBar { .and_then(|item| item.downcast::()) { search_view.update(cx, |this, cx| { - let new_mode = - crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); + let new_mode = crate::mode::next_mode(&this.current_mode); this.activate_search_mode(new_mode, cx); }) } @@ -1288,18 +1072,18 @@ impl ProjectSearchBar { } } - fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Regex, cx) - }); - } else { - cx.propagate_action(); - } - } + // fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { + // if let Some(search_view) = pane + // .active_item() + // .and_then(|item| item.downcast::()) + // { + // search_view.update(cx, |view, cx| { + // view.activate_search_mode(SearchMode::Regex, cx) + // }); + // } else { + // cx.propagate_action(); + // } + // } fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { @@ -1400,7 +1184,6 @@ impl View for ProjectSearchBar { theme.search.editor.input.container }; - let row_spacing = theme.workspace.toolbar.container.padding.bottom; let search = _search.read(cx); let filter_button = render_option_button_icon( search.filters_enabled, @@ -1413,8 +1196,7 @@ impl View for ProjectSearchBar { }, cx, ); - let search = _search.read(cx); - let is_semantic_disabled = search.semantic_state.is_none(); + let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), @@ -1428,17 +1210,17 @@ impl View for ProjectSearchBar { cx, ) }; - let case_sensitive = is_semantic_disabled.then(|| { - render_option_button_icon( - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - ) - }); + let case_sensitive = render_option_button_icon( + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + ); - let whole_word = is_semantic_disabled.then(|| { - render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) - }); + let whole_word = render_option_button_icon( + "icons/word_search_12.svg", + SearchOptions::WHOLE_WORD, + cx, + ); let search = _search.read(cx); let icon_style = theme.search.editor_icon.clone(); @@ -1454,8 +1236,8 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) + .with_child(case_sensitive) + .with_child(whole_word) .flex(1., false) .constrained() .contained(), @@ -1554,8 +1336,7 @@ impl View for ProjectSearchBar { ) }; let is_active = search.active_match_index.is_some(); - let semantic_index = SemanticIndex::enabled(cx) - .then(|| search_button_for_mode(SearchMode::Semantic, cx)); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, @@ -1581,7 +1362,6 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) .with_child(search_button_for_mode(SearchMode::Regex, cx)) .contained() .with_style(theme.search.modes_container), diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index c31ea6f2f8..7132efa5e3 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -33,7 +35,6 @@ actions!( NextHistoryQuery, PreviousHistoryQuery, ActivateTextMode, - ActivateSemanticMode, ActivateRegexMode ] ); @@ -94,7 +95,7 @@ impl SearchOptions { format!("Toggle {}", self.label()), tooltip_style, ) - .with_contents(theme::components::svg::Svg::new(self.icon())) + .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) .into_element() diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 8c3587d942..5287c999e8 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case, non_upper_case_globals)] + mod keymap_file; mod settings_file; mod settings_store; diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fce7ad825c..a74b9ed4a4 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -175,8 +175,13 @@ pub mod action_button { .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { - cx.window() - .dispatch_action(cx.view_id(), action.as_ref(), cx); + let window = cx.window(); + let view = cx.view_id(); + let action = action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action(view, action.as_ref(), &mut cx); + }) + .detach(); } }) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 528b1e2029..60d22bbdae 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -286,10 +286,6 @@ impl Pane { context_menu.update(cx, |menu, _| { menu.set_position_mode(OverlayPositionMode::Local) }); - let theme = theme::current(cx).workspace.tab_bar.clone(); - - let nav_button_height = theme.height; - let button_style = theme.nav_button; Self { items: Vec::new(), From 66e94aa199fcfb1f9b0beb93c4c64254d51afec8 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 17 Aug 2023 17:53:21 -0700 Subject: [PATCH 292/326] Make search re-query eagerly when changing modes Fix a bug where focus could be lost when clearing the search results --- crates/search/src/project_search.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index abb7395e50..2cec9610f1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -241,7 +241,7 @@ impl View for ProjectSearchView { let major_text = if model.pending_search.is_some() { Cow::Borrowed("Searching...") } else if model.no_results.is_some_and(|v| v) { - Cow::Borrowed("No Results...") + Cow::Borrowed("No Results") } else { match current_mode { SearchMode::Text => Cow::Borrowed("Text search all files and folders"), @@ -552,23 +552,20 @@ impl ProjectSearchView { } fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { - self.clear_search(cx); - let previous_mode = self.current_mode; if previous_mode == mode { return; } + self.clear_search(cx); self.current_mode = mode; + self.active_match_index = None; - match mode { - SearchMode::Regex | SearchMode::Text => { - self.active_match_index = None; - } - } + self.search(cx); cx.notify(); } + fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { let project; let excerpts; @@ -951,6 +948,7 @@ impl ProjectSearchBar { search_view.update(cx, |this, cx| { let new_mode = crate::mode::next_mode(&this.current_mode); this.activate_search_mode(new_mode, cx); + cx.focus(&this.query_editor); }) } } @@ -1062,8 +1060,9 @@ impl ProjectSearchBar { fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, _cx| { + search_view.update(cx, |search_view, cx| { search_view.toggle_search_option(option); + search_view.search(cx); }); cx.notify(); true From 269dad5a9c23cbe994459cccc3ef5ac1122a9486 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 16:03:12 +0300 Subject: [PATCH 293/326] Respect completion resolve server capabilities Only query for additional edits if the server supports it Co-Authored-By: Julia Risley --- crates/collab/src/tests/integration_tests.rs | 1 + crates/editor/src/editor_tests.rs | 2 ++ crates/project/src/project.rs | 18 ++++++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index a03e2ff16f..9bee8d434c 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4163,6 +4163,7 @@ async fn test_collaborating_with_completion( capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), ..Default::default() }), ..Default::default() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ec1cc12498..e031edf538 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5237,6 +5237,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), ..Default::default() }), ..Default::default() @@ -7528,6 +7529,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), ..Default::default() }), ..Default::default() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 933f259700..adb067b403 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4454,10 +4454,20 @@ impl Project { }; cx.spawn(|this, mut cx| async move { - let additional_text_edits = lang_server - .request::(completion.lsp_completion) - .await? - .additional_text_edits; + let can_resolve = lang_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + let additional_text_edits = if can_resolve { + lang_server + .request::(completion.lsp_completion) + .await? + .additional_text_edits + } else { + completion.lsp_completion.additional_text_edits + }; if let Some(edits) = additional_text_edits { let edits = this .update(&mut cx, |this, cx| { From e3c0e93e464dc613d2eb9909ab5bba93f71b95a0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 18 Aug 2023 10:38:06 -0600 Subject: [PATCH 294/326] Fix cursor adjustment on scroll Fixes: zed-industries/community#1929 Also preserves visual modes correctly. --- crates/editor/src/scroll.rs | 3 ++- crates/vim/src/normal/scroll.rs | 40 +++++++++++++++------------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index f5edb00d58..d87bc0ae4f 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -29,6 +29,7 @@ use self::{ }; pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); +pub const VERTICAL_SCROLL_MARGIN: f32 = 3.; const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); #[derive(Default)] @@ -136,7 +137,7 @@ pub struct ScrollManager { impl ScrollManager { pub fn new() -> Self { ScrollManager { - vertical_scroll_margin: 3.0, + vertical_scroll_margin: VERTICAL_SCROLL_MARGIN, anchor: ScrollAnchor::new(), ongoing: OngoingScroll::new(), autoscroll_request: None, diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 7b068cd793..a2bbab0478 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -1,7 +1,9 @@ -use std::cmp::Ordering; - use crate::Vim; -use editor::{display_map::ToDisplayPoint, scroll::scroll_amount::ScrollAmount, Editor}; +use editor::{ + display_map::ToDisplayPoint, + scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN}, + DisplayPoint, Editor, +}; use gpui::{actions, AppContext, ViewContext}; use language::Bias; use workspace::Workspace; @@ -53,13 +55,9 @@ fn scroll(cx: &mut ViewContext, by: fn(c: Option) -> ScrollAmoun fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext) { let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); + editor.scroll_screen(amount, cx); if should_move_cursor { - let selection_ordering = editor.newest_selection_on_screen(cx); - if selection_ordering.is_eq() { - return; - } - let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { visible_rows as u32 } else { @@ -69,21 +67,19 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex let top_anchor = editor.scroll_manager.anchor().anchor; editor.change_selections(None, cx, |s| { - s.replace_cursors_with(|snapshot| { - let mut new_point = top_anchor.to_display_point(&snapshot); + s.move_heads_with(|map, head, goal| { + let top = top_anchor.to_display_point(map); + let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32; + let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1; - match selection_ordering { - Ordering::Less => { - new_point = snapshot.clip_point(new_point, Bias::Right); - } - Ordering::Greater => { - *new_point.row_mut() += visible_rows - 1; - new_point = snapshot.clip_point(new_point, Bias::Left); - } - Ordering::Equal => unreachable!(), - } - - vec![new_point] + let new_head = if head.row() < min_row { + map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left) + } else if head.row() > max_row { + map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left) + } else { + head + }; + (new_head, goal) }) }); } From 5029587e3b65187e8a8addd7c564b696944834cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 18 Aug 2023 11:39:48 -0600 Subject: [PATCH 295/326] Add ctrl-q as an alias for ctrl-v Fixes: zed-industries/community#871 --- assets/keymaps/vim.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index a93d8aa3ec..458232b9b0 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -104,6 +104,7 @@ "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", + "ctrl-q": "vim::ToggleVisualBlock", "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion From ffdca77124ebe2472ddd77f0bad7d96c267a3e6f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 18 Aug 2023 14:07:17 -0400 Subject: [PATCH 296/326] Add python file icon --- assets/icons/file_icons/file_types.json | 5 ++++- assets/icons/file_icons/python.svg | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 assets/icons/file_icons/python.svg diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 9ea75d0730..bbe112e0c5 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -74,7 +74,7 @@ "profile": "terminal", "ps1": "terminal", "psd": "image", - "py": "code", + "py": "python", "rb": "code", "rkt": "code", "rs": "rust", @@ -148,6 +148,9 @@ "prettier": { "icon": "icons/file_icons/prettier.svg" }, + "python": { + "icon": "icons/file_icons/python.svg" + }, "rust": { "icon": "icons/file_icons/rust.svg" }, diff --git a/assets/icons/file_icons/python.svg b/assets/icons/file_icons/python.svg new file mode 100644 index 0000000000..dbc8565f90 --- /dev/null +++ b/assets/icons/file_icons/python.svg @@ -0,0 +1,6 @@ + + + + + + From ab73375a6143cef7e124ae592d37dc28bc38b5a5 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 18 Aug 2023 14:07:55 -0400 Subject: [PATCH 297/326] Alphabetize list --- assets/icons/file_icons/file_types.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index bbe112e0c5..1a738a9cd7 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -24,16 +24,16 @@ "eslintrc": "eslint", "eslintrc.js": "eslint", "eslintrc.json": "eslint", + "fish": "terminal", + "flac": "audio", "fmp": "storage", "fp7": "storage", - "flac": "audio", - "fish": "terminal", "frm": "storage", "gdb": "storage", + "gif": "image", "gitattributes": "vcs", "gitignore": "vcs", "gitmodules": "vcs", - "gif": "image", "go": "code", "h": "code", "handlebars": "code", @@ -51,16 +51,16 @@ "ldf": "storage", "lock": "lock", "log": "log", - "mdb": "storage", "md": "document", + "mdb": "storage", "mdf": "storage", "mdx": "document", "mp3": "audio", "mp4": "video", "myd": "storage", "myi": "storage", - "ods": "document", "odp": "document", + "ods": "document", "odt": "document", "ogg": "video", "pdb": "storage", @@ -81,17 +81,17 @@ "rtf": "document", "sav": "storage", "scm": "code", + "sdf": "storage", "sh": "terminal", "sqlite": "storage", - "sdf": "storage", "svelte": "template", "svg": "image", "swift": "code", - "ts": "typescript", - "tsx": "code", "tiff": "image", "toml": "toml", + "ts": "typescript", "tsv": "storage", + "tsx": "code", "txt": "document", "wav": "audio", "webm": "video", @@ -103,9 +103,9 @@ "zlogin": "terminal", "zsh": "terminal", "zsh_aliases": "terminal", - "zshenv": "terminal", "zsh_histfile": "terminal", "zsh_profile": "terminal", + "zshenv": "terminal", "zshrc": "terminal" }, "types": { From 93461d366c863ae7f7bf3a83eb14890d51d4bd93 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 18 Aug 2023 12:24:39 -0600 Subject: [PATCH 298/326] Fix to go to non-whitespace character Fixes: zed-industries/community#831 --- crates/vim/src/motion.rs | 13 ++++++++++++- crates/vim/test_data/test_next_line_start.json | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 crates/vim/test_data/test_next_line_start.json diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 29a1ba7df8..13c22afdaa 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -651,7 +651,10 @@ fn find_backward( fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { let new_row = (point.row() + times as u32).min(map.max_buffer_row()); - map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left) + first_non_whitespace( + map, + map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), + ) } #[cfg(test)] @@ -799,4 +802,12 @@ mod test { cx.simulate_shared_keystrokes([","]).await; cx.assert_shared_state("one two thˇree four").await; } + + #[gpui::test] + async fn test_next_line_start(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state("ˇone\n two\nthree").await; + cx.simulate_shared_keystrokes(["enter"]).await; + cx.assert_shared_state("one\n ˇtwo\nthree").await; + } } diff --git a/crates/vim/test_data/test_next_line_start.json b/crates/vim/test_data/test_next_line_start.json new file mode 100644 index 0000000000..90ed4a4f03 --- /dev/null +++ b/crates/vim/test_data/test_next_line_start.json @@ -0,0 +1,3 @@ +{"Put":{"state":"ˇone\n two\nthree"}} +{"Key":"enter"} +{"Get":{"state":"one\n ˇtwo\nthree","mode":"Normal"}} From 09fcacdfd18b1f95d604584a70434a7a9ae1d0c3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Aug 2023 11:31:40 -0700 Subject: [PATCH 299/326] Upgrade Tree-sitter for error recovery bug fix --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/language/src/syntax_map.rs | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69285a1abf..aab5504a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8126,7 +8126,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=1c65ca24bc9a734ab70115188f465e12eecf224e#1c65ca24bc9a734ab70115188f465e12eecf224e" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" dependencies = [ "cc", "regex", diff --git a/Cargo.toml b/Cargo.toml index 1938e832e9..7ea79138c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,7 +135,7 @@ tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1c65ca24bc9a734ab70115188f465e12eecf224e" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 1590294b1a..b6e1d16e18 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -72,7 +72,7 @@ pub struct SyntaxMapMatch<'a> { struct SyntaxMapCapturesLayer<'a> { depth: usize, - captures: QueryCaptures<'a, 'a, TextProvider<'a>>, + captures: QueryCaptures<'a, 'a, TextProvider<'a>, &'a [u8]>, next_capture: Option>, grammar_index: usize, _query_cursor: QueryCursorHandle, @@ -83,7 +83,7 @@ struct SyntaxMapMatchesLayer<'a> { next_pattern_index: usize, next_captures: Vec>, has_next: bool, - matches: QueryMatches<'a, 'a, TextProvider<'a>>, + matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>, grammar_index: usize, _query_cursor: QueryCursorHandle, } @@ -1279,7 +1279,9 @@ fn get_injections( } for (language, mut included_ranges) in combined_injection_ranges.drain() { - included_ranges.sort_unstable(); + included_ranges.sort_unstable_by(|a, b| { + Ord::cmp(&a.start_byte, &b.start_byte).then_with(|| Ord::cmp(&a.end_byte, &b.end_byte)) + }); queue.push(ParseStep { depth, language: ParseStepLanguage::Loaded { language }, @@ -1697,7 +1699,7 @@ impl std::fmt::Debug for SyntaxLayer { } } -impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> { +impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> { type I = ByteChunks<'a>; fn text(&mut self, node: tree_sitter::Node) -> Self::I { From e26d6d7a49b905e99b15466427da1f4912f7da9c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 18 Aug 2023 14:54:53 -0400 Subject: [PATCH 300/326] Add elixir icons --- assets/icons/file_icons/elixir.svg | 3 +++ assets/icons/file_icons/file_types.json | 10 ++++++++++ assets/icons/file_icons/phoenix.svg | 4 ++++ 3 files changed, 17 insertions(+) create mode 100644 assets/icons/file_icons/elixir.svg create mode 100644 assets/icons/file_icons/phoenix.svg diff --git a/assets/icons/file_icons/elixir.svg b/assets/icons/file_icons/elixir.svg new file mode 100644 index 0000000000..15a1004328 --- /dev/null +++ b/assets/icons/file_icons/elixir.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 1a738a9cd7..a65155d4eb 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -21,9 +21,12 @@ "dll": "storage", "doc": "document", "docx": "document", + "eex": "elixir", "eslintrc": "eslint", "eslintrc.js": "eslint", "eslintrc.json": "eslint", + "ex": "elixir", + "exs": "elixir", "fish": "terminal", "flac": "audio", "fmp": "storage", @@ -38,6 +41,7 @@ "h": "code", "handlebars": "code", "hbs": "template", + "heex": "elixir", "htm": "template", "html": "template", "ib": "storage", @@ -127,6 +131,9 @@ "document": { "icon": "icons/file_icons/book.svg" }, + "elixir": { + "icon": "icons/file_icons/elixir.svg" + }, "eslint": { "icon": "icons/file_icons/eslint.svg" }, @@ -145,6 +152,9 @@ "log": { "icon": "icons/file_icons/info.svg" }, + "phoenix": { + "icon": "icons/file_icons/phoenix.svg" + }, "prettier": { "icon": "icons/file_icons/prettier.svg" }, diff --git a/assets/icons/file_icons/phoenix.svg b/assets/icons/file_icons/phoenix.svg new file mode 100644 index 0000000000..424b753806 --- /dev/null +++ b/assets/icons/file_icons/phoenix.svg @@ -0,0 +1,4 @@ + + + + From 1c46749ad707ad562f0ec099a7ccd06d8a585636 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Aug 2023 12:58:09 -0700 Subject: [PATCH 301/326] Fix regression in Buffer::language_scope_at Co-authored-by: Julia --- crates/editor/src/editor.rs | 6 +-- crates/language/src/buffer.rs | 47 ++++++++++++++------ crates/language/src/buffer_tests.rs | 69 ++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 45a37db983..a38145f48c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2103,12 +2103,12 @@ impl Editor { for (selection, autoclose_region) in self.selections_with_autoclose_regions(selections, &snapshot) { - if let Some(language) = snapshot.language_scope_at(selection.head()) { + if let Some(scope) = snapshot.language_scope_at(selection.head()) { // Determine if the inserted text matches the opening or closing // bracket of any of this language's bracket pairs. let mut bracket_pair = None; let mut is_bracket_pair_start = false; - for (pair, enabled) in language.brackets() { + for (pair, enabled) in scope.brackets() { if enabled && pair.close && pair.start.ends_with(text.as_ref()) { bracket_pair = Some(pair.clone()); is_bracket_pair_start = true; @@ -2130,7 +2130,7 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .map_or(true, |c| language.should_autoclose_before(c)); + .map_or(true, |c| scope.should_autoclose_before(c)); let preceding_text_matches_prefix = prefix_len == 0 || (selection.start.column >= (prefix_len as u32) && snapshot.contains_str_at( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0b10432a9f..e6ad3469ea 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2145,27 +2145,46 @@ impl BufferSnapshot { pub fn language_scope_at(&self, position: D) -> Option { let offset = position.to_offset(self); - let mut range = 0..self.len(); - let mut scope = self.language.clone().map(|language| LanguageScope { - language, - override_id: None, - }); + let mut scope = None; + let mut smallest_range: Option> = None; // Use the layer that has the smallest node intersecting the given point. for layer in self.syntax.layers_for_range(offset..offset, &self.text) { let mut cursor = layer.node().walk(); - while cursor.goto_first_child_for_byte(offset).is_some() {} - let node_range = cursor.node().byte_range(); - if node_range.to_inclusive().contains(&offset) && node_range.len() < range.len() { - range = node_range; - scope = Some(LanguageScope { - language: layer.language.clone(), - override_id: layer.override_id(offset, &self.text), - }); + + let mut range = None; + loop { + let child_range = cursor.node().byte_range(); + if !child_range.to_inclusive().contains(&offset) { + break; + } + + range = Some(child_range); + if cursor.goto_first_child_for_byte(offset).is_none() { + break; + } + } + + if let Some(range) = range { + if smallest_range + .as_ref() + .map_or(true, |smallest_range| range.len() < smallest_range.len()) + { + smallest_range = Some(range); + scope = Some(LanguageScope { + language: layer.language.clone(), + override_id: layer.override_id(offset, &self.text), + }); + } } } - scope + scope.or_else(|| { + self.language.clone().map(|language| LanguageScope { + language, + override_id: None, + }) + }) } pub fn surrounding_word(&self, start: T) -> (Range, Option) { diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 399ca85e56..9d4b9c38fe 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1631,7 +1631,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { } #[gpui::test] -fn test_language_scope_at(cx: &mut AppContext) { +fn test_language_scope_at_with_javascript(cx: &mut AppContext) { init_settings(cx, |_| {}); cx.add_model(|cx| { @@ -1718,6 +1718,73 @@ fn test_language_scope_at(cx: &mut AppContext) { }); } +#[gpui::test] +fn test_language_scope_at_with_rust(cx: &mut AppContext) { + init_settings(cx, |_| {}); + + cx.add_model(|cx| { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".into(), + end: "}".into(), + close: true, + newline: false, + }, + BracketPair { + start: "'".into(), + end: "'".into(), + close: true, + newline: false, + }, + ], + disabled_scopes_by_bracket_ix: vec![ + Vec::new(), // + vec!["string".into()], + ], + }, + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_override_query( + r#" + (string_literal) @string + "#, + ) + .unwrap(); + + let text = r#" + const S: &'static str = "hello"; + "# + .unindent(); + + let buffer = Buffer::new(0, text.clone(), cx).with_language(Arc::new(language), cx); + let snapshot = buffer.snapshot(); + + // By default, all brackets are enabled + let config = snapshot.language_scope_at(0).unwrap(); + assert_eq!( + config.brackets().map(|e| e.1).collect::>(), + &[true, true] + ); + + // Within a string, the quotation brackets are disabled. + let string_config = snapshot + .language_scope_at(text.find("ello").unwrap()) + .unwrap(); + assert_eq!( + string_config.brackets().map(|e| e.1).collect::>(), + &[true, false] + ); + + buffer + }); +} + #[gpui::test] fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { init_settings(cx, |_| {}); From 3ab3042e95890fad79f9d72ddf6ebc835fae631b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 18 Aug 2023 15:50:34 -0600 Subject: [PATCH 302/326] Don't show vim mode when disabled --- crates/vim/src/editor_events.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index f1b01f460d..994a09aaf9 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -22,9 +22,11 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { vim.set_active_editor(editor.clone(), cx); - cx.emit_global(VimEvent::ModeChanged { - mode: vim.state().mode, - }); + if vim.enabled { + cx.emit_global(VimEvent::ModeChanged { + mode: vim.state().mode, + }); + } }); }); } From 8ef671d7a104ea8adc5244b54bdd5802cfe7009e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 14:53:30 -0700 Subject: [PATCH 303/326] Move the collab panel to the left by default Increase the indent size on channels Switch font UI mono --- assets/settings/default.json | 2 +- crates/collab_ui/src/collab_panel.rs | 7 +++- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements/component.rs | 53 +++++++++++++++++++++++++++ styles/src/style_tree/collab_panel.ts | 32 ++++++++-------- styles/src/style_tree/context_menu.ts | 2 +- 6 files changed, 78 insertions(+), 22 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2ddf4a137f..08faedbed6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -126,7 +126,7 @@ // 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", + "dock": "left", // Default width of the channels panel. "default_width": 240 }, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0e7bd5f929..fb0547cd44 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2354,7 +2354,7 @@ impl View for CollabPanel { .into_any() }) .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) - .into_any_named("channels panel") + .into_any_named("collab panel") } } @@ -2404,7 +2404,10 @@ impl Panel for CollabPanel { } fn icon_tooltip(&self) -> (String, Option>) { - ("Channels Panel".to_string(), Some(Box::new(ToggleFocus))) + ( + "Collaboration Panel".to_string(), + Some(Box::new(ToggleFocus)), + ) } fn should_change_position_on_event(event: &Self::Event) -> bool { diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index cf695ea834..ad38b5893c 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .into_element(), + .element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .into_element(), + .element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index a26355a539..035637c377 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,6 +9,12 @@ use super::Empty; pub trait GeneralComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn element(self) -> ComponentAdapter + where + Self: Sized, + { + ComponentAdapter::new(self) + } } pub trait StyleableComponent { @@ -50,6 +56,53 @@ impl Component for C { } } +// StylableComponent -> GeneralComponent +pub struct StylableComponentAdapter, V: View> { + component: C, + phantom: std::marker::PhantomData, +} + +impl, V: View> StylableComponentAdapter { + fn new(component: C) -> Self { + Self { + component, + phantom: std::marker::PhantomData, + } + } +} + +impl StyleableComponent for StylableComponentAdapter { + type Style = (); + + type Output = C; + + fn with_style(self, _: Self::Style) -> Self::Output { + self.component + } +} + +// Element -> Component +pub struct ElementAdapter { + element: AnyElement, + _phantom: std::marker::PhantomData, +} + +impl ElementAdapter { + pub fn new(element: AnyElement) -> Self { + Self { + element, + _phantom: std::marker::PhantomData, + } + } +} + +impl Component for ElementAdapter { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + self.element + } +} + +// Component -> Element pub struct ComponentAdapter { component: Option, element: Option>, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 7f0fd5f423..a102ee7691 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -37,7 +37,7 @@ export default function contacts_panel(): any { width: 14, }, name: { - ...text(layer, "ui_sans", { size: "sm" }), + ...text(layer, "sans", { size: "sm" }), margin: { left: NAME_MARGIN, right: 4, @@ -69,7 +69,7 @@ export default function contacts_panel(): any { const subheader_row = toggleable({ base: interactive({ base: { - ...text(layer, "ui_sans", { size: "sm" }), + ...text(layer, "sans", { size: "sm" }), padding: { left: SPACING, right: SPACING, @@ -87,7 +87,7 @@ export default function contacts_panel(): any { state: { active: { default: { - ...text(theme.lowest, "ui_sans", { size: "sm" }), + ...text(theme.lowest, "sans", { size: "sm" }), background: background(theme.lowest), }, clicked: { @@ -100,8 +100,8 @@ export default function contacts_panel(): any { const filter_input = { background: background(layer, "on"), corner_radius: 6, - text: text(layer, "ui_sans", "base"), - placeholder_text: text(layer, "ui_sans", "base", "disabled", { + text: text(layer, "sans", "base"), + placeholder_text: text(layer, "sans", "base", "disabled", { size: "xs", }), selection: theme.players[0], @@ -140,7 +140,7 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(theme.lowest, "ui_sans", { size: "sm" }), + ...text(theme.lowest, "sans", { size: "sm" }), background: background(theme.lowest), }, clicked: { @@ -194,10 +194,10 @@ export default function contacts_panel(): any { add_channel_button: header_icon_button, leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, - channel_indent: INDENT_SIZE, + channel_indent: INDENT_SIZE * 2, section_icon_size: 14, header_row: { - ...text(layer, "ui_sans", { size: "sm", weight: "bold" }), + ...text(layer, "sans", { size: "sm", weight: "bold" }), margin: { top: SPACING }, padding: { left: SPACING, @@ -251,7 +251,7 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(theme.lowest, "ui_sans", { size: "sm" }), + ...text(theme.lowest, "sans", { size: "sm" }), background: background(theme.lowest), }, clicked: { @@ -262,7 +262,7 @@ export default function contacts_panel(): any { }), channel_row: item_row, channel_name: { - ...text(layer, "ui_sans", { size: "sm" }), + ...text(layer, "sans", { size: "sm" }), margin: { left: NAME_MARGIN, }, @@ -279,7 +279,7 @@ export default function contacts_panel(): any { list_empty_state: toggleable({ base: interactive({ base: { - ...text(layer, "ui_sans", "variant", { size: "sm" }), + ...text(layer, "sans", "variant", { size: "sm" }), padding: { top: SPACING / 2, bottom: SPACING / 2, @@ -301,7 +301,7 @@ export default function contacts_panel(): any { }, active: { default: { - ...text(theme.lowest, "ui_sans", { size: "sm" }), + ...text(theme.lowest, "sans", { size: "sm" }), background: background(theme.lowest), }, clicked: { @@ -325,12 +325,12 @@ export default function contacts_panel(): any { right: 4, }, background: background(layer, "hovered"), - ...text(layer, "ui_sans", "hovered", { size: "xs" }) + ...text(layer, "sans", "hovered", { size: "xs" }) }, contact_status_free: indicator({ layer, color: "positive" }), contact_status_busy: indicator({ layer, color: "negative" }), contact_username: { - ...text(layer, "ui_sans", { size: "sm" }), + ...text(layer, "sans", { size: "sm" }), margin: { left: NAME_MARGIN, }, @@ -347,7 +347,7 @@ export default function contacts_panel(): any { color: foreground(layer, "on"), }, calling_indicator: { - ...text(layer, "mono", "variant", { size: "xs" }), + ...text(layer, "sans", "variant", { size: "xs" }), }, tree_branch: toggleable({ base: interactive({ @@ -380,7 +380,7 @@ export default function contacts_panel(): any { }, name: { ...project_row.name, - ...text(layer, "mono", { size: "sm" }), + ...text(layer, "sans", { size: "sm" }), }, }, state: { diff --git a/styles/src/style_tree/context_menu.ts b/styles/src/style_tree/context_menu.ts index 84688c0971..2225cee527 100644 --- a/styles/src/style_tree/context_menu.ts +++ b/styles/src/style_tree/context_menu.ts @@ -19,7 +19,7 @@ export default function context_menu(): any { icon_width: 14, padding: { left: 6, right: 6, top: 2, bottom: 2 }, corner_radius: 6, - label: text(theme.middle, "ui_sans", { size: "sm" }), + label: text(theme.middle, "sans", { size: "sm" }), keystroke: { ...text(theme.middle, "sans", "variant", { size: "sm", From 84f35d4e6b7e9d81fe05b3b840c5b402e402811c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 15:12:56 -0700 Subject: [PATCH 304/326] Add pub --- crates/gpui/src/elements/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 035637c377..94b136e8d6 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -63,7 +63,7 @@ pub struct StylableComponentAdapter, V: View> { } impl, V: View> StylableComponentAdapter { - fn new(component: C) -> Self { + pub fn new(component: C) -> Self { Self { component, phantom: std::marker::PhantomData, From 4c3227ce2addd5bc490b7db35f29297f81a36948 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 15:17:24 -0700 Subject: [PATCH 305/326] Fix example compile erorr --- crates/gpui/src/elements/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 94b136e8d6..9d745347d0 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -42,7 +42,7 @@ impl StyleableComponent for () { pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn into_element(self) -> ComponentAdapter + fn element(self) -> ComponentAdapter where Self: Sized, { From 4c15f26eba0fc181b39167d293715218d70f5a49 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 15:26:36 -0700 Subject: [PATCH 306/326] Finish rename correctly --- crates/search/src/search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 7132efa5e3..67cb876c32 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -98,7 +98,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .into_element() + .element() .into_any() } } From 24fbea1557bd9d2f2903c6ccc2791db19093ce55 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 18 Aug 2023 15:34:35 -0700 Subject: [PATCH 307/326] Update collab_panel.rs --- crates/collab_ui/src/collab_panel.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index fb0547cd44..c49011b86b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -86,7 +86,7 @@ impl_actions!( ] ); -const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; +const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); @@ -464,7 +464,7 @@ impl CollabPanel { cx.spawn(|mut cx| async move { let serialized_panel = if let Some(panel) = cx .background() - .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) }) + .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) .await .log_err() .flatten() @@ -493,7 +493,7 @@ impl CollabPanel { async move { KEY_VALUE_STORE .write_kvp( - CHANNELS_PANEL_KEY.into(), + COLLABORATION_PANEL_KEY.into(), serde_json::to_string(&SerializedChannelsPanel { width })?, ) .await?; From 64cbfe088a452ad2962709baaf0aef7533c09fae Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 18 Aug 2023 15:52:21 -0700 Subject: [PATCH 308/326] Update bump-version.sh --- script/lib/bump-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/bump-version.sh b/script/lib/bump-version.sh index ce95536950..0e1dfa5131 100755 --- a/script/lib/bump-version.sh +++ b/script/lib/bump-version.sh @@ -12,7 +12,7 @@ if [[ -n $(git status --short --untracked-files=no) ]]; then exit 1 fi -which cargo-set-version > /dev/null || cargo install cargo-edit +which cargo-set-version > /dev/null || cargo install cargo-edit --features vendored-openssl which jq > /dev/null || brew install jq cargo set-version --package $package --bump $version_increment cargo check --quiet From 48553d7c8fb3a38a1c8047acd2d011f5f427b607 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 15:59:26 -0700 Subject: [PATCH 309/326] Revert "Remove semantic search UI" This reverts commit c0f042b39a595da6f93de2268e5990a3c5604912. --- crates/gpui/src/elements/component.rs | 51 ++--- crates/search/src/buffer_search.rs | 7 +- crates/search/src/mode.rs | 20 +- crates/search/src/project_search.rs | 304 ++++++++++++++++++++++---- crates/search/src/search.rs | 7 +- crates/settings/src/settings.rs | 2 - crates/theme/src/components.rs | 9 +- 7 files changed, 309 insertions(+), 91 deletions(-) diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 9d745347d0..c2158356e9 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,3 +1,5 @@ +use std::marker::PhantomData; + use pathfinder_geometry::{rect::RectF, vector::Vector2F}; use crate::{ @@ -103,26 +105,22 @@ impl Component for ElementAdapter { } // Component -> Element -pub struct ComponentAdapter { +pub struct ComponentAdapter { component: Option, - element: Option>, - #[cfg(debug_assertions)] - _component_name: &'static str, + phantom: PhantomData, } -impl ComponentAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), - element: None, - #[cfg(debug_assertions)] - _component_name: std::any::type_name::(), + phantom: PhantomData, } } } impl + 'static> Element for ComponentAdapter { - type LayoutState = (); + type LayoutState = AnyElement; type PaintState = (); @@ -132,12 +130,10 @@ impl + 'static> Element for ComponentAdapter { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - if self.element.is_none() { - let component = self.component.take().unwrap(); - self.element = Some(component.render(view, cx.view_context())); - } - let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx); - (constraint, ()) + let component = self.component.take().unwrap(); + let mut element = component.render(view, cx.view_context()); + let constraint = element.layout(constraint, view, cx); + (constraint, element) } fn paint( @@ -145,14 +141,11 @@ impl + 'static> Element for ComponentAdapter { scene: &mut SceneBuilder, bounds: RectF, visible_bounds: RectF, - _: &mut Self::LayoutState, + layout: &mut Self::LayoutState, view: &mut V, cx: &mut PaintContext, ) -> Self::PaintState { - self.element - .as_mut() - .unwrap() - .paint(scene, bounds.origin(), visible_bounds, view, cx) + layout.paint(scene, bounds.origin(), visible_bounds, view, cx) } fn rect_for_text_range( @@ -160,35 +153,25 @@ impl + 'static> Element for ComponentAdapter { range_utf16: std::ops::Range, _: RectF, _: RectF, - _: &Self::LayoutState, + element: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> Option { - self.element - .as_ref() - .unwrap() - .rect_for_text_range(range_utf16, view, cx) + element.rect_for_text_range(range_utf16, view, cx) } fn debug( &self, _: RectF, - _: &Self::LayoutState, + element: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> serde_json::Value { - #[cfg(debug_assertions)] - let component_name = self._component_name; - - #[cfg(not(debug_assertions))] - let component_name = "Unknown"; - serde_json::json!({ "type": "ComponentAdapter", - "child": self.element.as_ref().unwrap().debug(view, cx), - "component_name": component_name + "child": element.debug(view, cx), }) } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b65c7222a4..4078cb572d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -523,6 +523,11 @@ impl BufferSearchBar { } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + assert_ne!( + mode, + SearchMode::Semantic, + "Semantic search is not supported in buffer search" + ); if mode == self.current_mode { return; } @@ -797,7 +802,7 @@ impl BufferSearchBar { } } fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { - self.activate_search_mode(next_mode(&self.current_mode), cx); + self.activate_search_mode(next_mode(&self.current_mode, false), cx); } fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { let mut should_propagate = true; diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 0163528951..2c180be761 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -1,11 +1,12 @@ use gpui::Action; -use crate::{ActivateRegexMode, ActivateTextMode}; +use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; // TODO: Update the default search mode to get from config #[derive(Copy, Clone, Debug, Default, PartialEq)] pub enum SearchMode { #[default] Text, + Semantic, Regex, } @@ -19,6 +20,7 @@ impl SearchMode { pub(crate) fn label(&self) -> &'static str { match self { SearchMode::Text => "Text", + SearchMode::Semantic => "Semantic", SearchMode::Regex => "Regex", } } @@ -26,6 +28,7 @@ impl SearchMode { pub(crate) fn region_id(&self) -> usize { match self { SearchMode::Text => 3, + SearchMode::Semantic => 4, SearchMode::Regex => 5, } } @@ -33,6 +36,7 @@ impl SearchMode { pub(crate) fn tooltip_text(&self) -> &'static str { match self { SearchMode::Text => "Activate Text Search", + SearchMode::Semantic => "Activate Semantic Search", SearchMode::Regex => "Activate Regex Search", } } @@ -40,6 +44,7 @@ impl SearchMode { pub(crate) fn activate_action(&self) -> Box { match self { SearchMode::Text => Box::new(ActivateTextMode), + SearchMode::Semantic => Box::new(ActivateSemanticMode), SearchMode::Regex => Box::new(ActivateRegexMode), } } @@ -48,6 +53,7 @@ impl SearchMode { match self { SearchMode::Regex => true, SearchMode::Text => true, + SearchMode::Semantic => true, } } @@ -61,14 +67,22 @@ impl SearchMode { pub(crate) fn button_side(&self) -> Option { match self { SearchMode::Text => Some(Side::Left), + SearchMode::Semantic => None, SearchMode::Regex => Some(Side::Right), } } } -pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode { +pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { + let next_text_state = if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Regex + }; + match mode { - SearchMode::Text => SearchMode::Regex, + SearchMode::Text => next_text_state, + SearchMode::Semantic => SearchMode::Regex, SearchMode::Regex => SearchMode::Text, } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2cec9610f1..dada928d6e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,10 +2,10 @@ use crate::{ history::SearchHistory, mode::SearchMode, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, - SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, }; -use anyhow::Context; +use anyhow::{Context, Result}; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, @@ -13,6 +13,8 @@ use editor::{ }; use futures::StreamExt; +use gpui::platform::PromptLevel; + use gpui::{ actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, @@ -20,10 +22,12 @@ use gpui::{ }; use menu::Confirm; +use postage::stream::Stream; use project::{ - search::{PathMatcher, SearchQuery}, + search::{PathMatcher, SearchInputs, SearchQuery}, Entry, Project, }; +use semantic_index::SemanticIndex; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -60,7 +64,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::cycle_mode); cx.add_action(ProjectSearchBar::next_history_query); cx.add_action(ProjectSearchBar::previous_history_query); - // cx.add_action(ProjectSearchBar::activate_regex_mode); + cx.add_action(ProjectSearchBar::activate_regex_mode); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); @@ -114,6 +118,8 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, + semantic_state: Option, + semantic_permissioned: Option, search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, @@ -125,6 +131,12 @@ pub struct ProjectSearchView { current_mode: SearchMode, } +struct SemanticSearchState { + file_count: usize, + outstanding_file_count: usize, + _progress_task: Task<()>, +} + pub struct ProjectSearchBar { active_project_search: Option>, subscription: Option, @@ -206,6 +218,60 @@ impl ProjectSearch { })); cx.notify(); } + + fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { + let search = SemanticIndex::global(cx).map(|index| { + index.update(cx, |semantic_index, cx| { + semantic_index.search_project( + self.project.clone(), + inputs.as_str().to_owned(), + 10, + inputs.files_to_include().to_vec(), + inputs.files_to_exclude().to_vec(), + cx, + ) + }) + }); + self.search_id += 1; + self.match_ranges.clear(); + self.search_history.add(inputs.as_str().to_string()); + self.no_results = Some(true); + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + let results = search?.await.log_err()?; + + let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])) + .collect(); + + excerpts.stream_excerpts_with_context_lines(matches, 3, cx) + }) + }); + + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + this.no_results = Some(false); + cx.notify(); + }); + } + + this.update(&mut cx, |this, cx| { + this.pending_search.take(); + cx.notify(); + }); + + None + })); + cx.notify(); + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -245,10 +311,27 @@ impl View for ProjectSearchView { } else { match current_mode { SearchMode::Text => Cow::Borrowed("Text search all files and folders"), + SearchMode::Semantic => { + Cow::Borrowed("Search all code objects using Natural Language") + } SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), } }; + let semantic_status = if let Some(semantic) = &self.semantic_state { + if semantic.outstanding_file_count > 0 { + format!( + "Indexing: {} of {}...", + semantic.file_count - semantic.outstanding_file_count, + semantic.file_count + ) + } else { + "Indexing complete".to_string() + } + } else { + "Indexing: ...".to_string() + }; + let minor_text = if let Some(no_results) = model.no_results { if model.pending_search.is_none() && no_results { vec!["No results found in this project for the provided query".to_owned()] @@ -256,11 +339,19 @@ impl View for ProjectSearchView { vec![] } } else { - vec![ - "".to_owned(), - "Include/exclude specific paths with the filter option.".to_owned(), - "Matching exact word and/or casing is available too.".to_owned(), - ] + match current_mode { + SearchMode::Semantic => vec![ + "".to_owned(), + semantic_status, + "Simply explain the code you are looking to find.".to_owned(), + "ex. 'prompt user for permissions to index their project'".to_owned(), + ], + _ => vec![ + "".to_owned(), + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ], + } }; let previous_query_keystrokes = @@ -539,6 +630,49 @@ impl ProjectSearchView { self.search_options.toggle(option); } + fn index_project(&mut self, cx: &mut ViewContext) { + if let Some(semantic_index) = SemanticIndex::global(cx) { + // Semantic search uses no options + self.search_options = SearchOptions::none(); + + let project = self.model.read(cx).project.clone(); + let index_task = semantic_index.update(cx, |semantic_index, cx| { + semantic_index.index_project(project, cx) + }); + + cx.spawn(|search_view, mut cx| async move { + let (files_to_index, mut files_remaining_rx) = index_task.await?; + + search_view.update(&mut cx, |search_view, cx| { + cx.notify(); + search_view.semantic_state = Some(SemanticSearchState { + file_count: files_to_index, + outstanding_file_count: files_to_index, + _progress_task: cx.spawn(|search_view, mut cx| async move { + while let Some(count) = files_remaining_rx.recv().await { + search_view + .update(&mut cx, |search_view, cx| { + if let Some(semantic_search_state) = + &mut search_view.semantic_state + { + semantic_search_state.outstanding_file_count = count; + cx.notify(); + if count == 0 { + return; + } + } + }) + .ok(); + } + }), + }); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + fn clear_search(&mut self, cx: &mut ViewContext) { self.model.update(cx, |model, cx| { model.pending_search = None; @@ -561,7 +695,61 @@ impl ProjectSearchView { self.current_mode = mode; self.active_match_index = None; - self.search(cx); + match mode { + SearchMode::Semantic => { + let has_permission = self.semantic_permissioned(cx); + self.active_match_index = None; + cx.spawn(|this, mut cx| async move { + let has_permission = has_permission.await?; + + if !has_permission { + let mut answer = this.update(&mut cx, |this, cx| { + let project = this.model.read(cx).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 semantic search? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + 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 { + this.update(&mut cx, |this, cx| { + this.semantic_permissioned = Some(false); + debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected"); + this.activate_search_mode(previous_mode, cx); + })?; + return anyhow::Ok(()); + } + } + + this.update(&mut cx, |this, cx| { + this.index_project(cx); + })?; + + anyhow::Ok(()) + }).detach_and_log_err(cx); + } + SearchMode::Regex | SearchMode::Text => { + self.semantic_state = None; + self.active_match_index = None; + self.search(cx); + } + } cx.notify(); } @@ -657,6 +845,8 @@ impl ProjectSearchView { model, query_editor, results_editor, + semantic_state: None, + semantic_permissioned: None, search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, @@ -670,6 +860,18 @@ impl ProjectSearchView { this } + fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + SemanticIndex::global(cx) + .map(|semantic| { + let project = self.model.read(cx).project.clone(); + semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) + } pub fn new_search_in_directory( workspace: &mut Workspace, dir_entry: &Entry, @@ -745,8 +947,26 @@ impl ProjectSearchView { } fn search(&mut self, cx: &mut ViewContext) { - if let Some(query) = self.build_search_query(cx) { - self.model.update(cx, |model, cx| model.search(query, cx)); + let mode = self.current_mode; + match mode { + SearchMode::Semantic => { + if let Some(semantic) = &mut self.semantic_state { + if semantic.outstanding_file_count > 0 { + return; + } + + if let Some(query) = self.build_search_query(cx) { + self.model + .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx)); + } + } + } + + _ => { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } + } } } @@ -946,7 +1166,8 @@ impl ProjectSearchBar { .and_then(|item| item.downcast::()) { search_view.update(cx, |this, cx| { - let new_mode = crate::mode::next_mode(&this.current_mode); + let new_mode = + crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); this.activate_search_mode(new_mode, cx); cx.focus(&this.query_editor); }) @@ -1071,18 +1292,18 @@ impl ProjectSearchBar { } } - // fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { - // if let Some(search_view) = pane - // .active_item() - // .and_then(|item| item.downcast::()) - // { - // search_view.update(cx, |view, cx| { - // view.activate_search_mode(SearchMode::Regex, cx) - // }); - // } else { - // cx.propagate_action(); - // } - // } + fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Regex, cx) + }); + } else { + cx.propagate_action(); + } + } fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { @@ -1195,7 +1416,8 @@ impl View for ProjectSearchBar { }, cx, ); - + let search = _search.read(cx); + let is_semantic_disabled = search.semantic_state.is_none(); let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), @@ -1209,17 +1431,17 @@ impl View for ProjectSearchBar { cx, ) }; - let case_sensitive = render_option_button_icon( - "icons/case_insensitive_12.svg", - SearchOptions::CASE_SENSITIVE, - cx, - ); + let case_sensitive = is_semantic_disabled.then(|| { + render_option_button_icon( + "icons/case_insensitive_12.svg", + SearchOptions::CASE_SENSITIVE, + cx, + ) + }); - let whole_word = render_option_button_icon( - "icons/word_search_12.svg", - SearchOptions::WHOLE_WORD, - cx, - ); + let whole_word = is_semantic_disabled.then(|| { + render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) + }); let search = _search.read(cx); let icon_style = theme.search.editor_icon.clone(); @@ -1235,8 +1457,8 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .with_child(filter_button) - .with_child(case_sensitive) - .with_child(whole_word) + .with_children(case_sensitive) + .with_children(whole_word) .flex(1., false) .constrained() .contained(), @@ -1335,7 +1557,8 @@ impl View for ProjectSearchBar { ) }; let is_active = search.active_match_index.is_some(); - + let semantic_index = SemanticIndex::enabled(cx) + .then(|| search_button_for_mode(SearchMode::Semantic, cx)); let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, @@ -1361,6 +1584,7 @@ impl View for ProjectSearchBar { .with_child( Flex::row() .with_child(search_button_for_mode(SearchMode::Text, cx)) + .with_children(semantic_index) .with_child(search_button_for_mode(SearchMode::Regex, cx)) .contained() .with_style(theme.search.modes_container), diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 67cb876c32..8d8c02c8d7 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,9 +8,7 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{ - action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, -}; +use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle}; pub mod buffer_search; mod history; @@ -35,6 +33,7 @@ actions!( NextHistoryQuery, PreviousHistoryQuery, ActivateTextMode, + ActivateSemanticMode, ActivateRegexMode ] ); @@ -95,7 +94,7 @@ impl SearchOptions { format!("Toggle {}", self.label()), tooltip_style, ) - .with_contents(Svg::new(self.icon())) + .with_contents(theme::components::svg::Svg::new(self.icon())) .toggleable(active) .with_style(button_style) .element() diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5287c999e8..8c3587d942 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case, non_upper_case_globals)] - mod keymap_file; mod settings_file; mod settings_store; diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index a74b9ed4a4..fce7ad825c 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -175,13 +175,8 @@ pub mod action_button { .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { - let window = cx.window(); - let view = cx.view_id(); - let action = action.boxed_clone(); - cx.spawn(|_, mut cx| async move { - window.dispatch_action(view, action.as_ref(), &mut cx); - }) - .detach(); + cx.window() + .dispatch_action(cx.view_id(), action.as_ref(), cx); } }) .with_cursor_style(CursorStyle::PointingHand) From e964137d79946ebf38004540f3eafb980340218d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Aug 2023 16:23:33 -0700 Subject: [PATCH 310/326] Reorganize source files of collab::db * Move all sea_orm tables into a 'tables' module * Move TestDb into its own file * Move id types into their own module --- crates/collab/src/db.rs | 377 ++++-------------- .../collab/src/db/{tests.rs => db_tests.rs} | 5 +- crates/collab/src/db/ids.rs | 125 ++++++ crates/collab/src/db/signup.rs | 57 --- crates/collab/src/db/tables.rs | 20 + .../src/db/{ => tables}/access_token.rs | 2 +- crates/collab/src/db/{ => tables}/channel.rs | 8 +- .../src/db/{ => tables}/channel_member.rs | 4 +- .../src/db/{ => tables}/channel_path.rs | 2 +- crates/collab/src/db/{ => tables}/contact.rs | 28 +- crates/collab/src/db/{ => tables}/follower.rs | 5 +- .../src/db/{ => tables}/language_server.rs | 2 +- crates/collab/src/db/{ => tables}/project.rs | 2 +- .../db/{ => tables}/project_collaborator.rs | 2 +- crates/collab/src/db/{ => tables}/room.rs | 2 +- .../src/db/{ => tables}/room_participant.rs | 2 +- crates/collab/src/db/{ => tables}/server.rs | 2 +- crates/collab/src/db/tables/signup.rs | 28 ++ crates/collab/src/db/{ => tables}/user.rs | 2 +- crates/collab/src/db/{ => tables}/worktree.rs | 2 +- .../worktree_diagnostic_summary.rs | 2 +- .../src/db/{ => tables}/worktree_entry.rs | 2 +- .../db/{ => tables}/worktree_repository.rs | 2 +- .../worktree_repository_statuses.rs | 2 +- .../db/{ => tables}/worktree_settings_file.rs | 2 +- crates/collab/src/db/test_db.rs | 120 ++++++ crates/collab/src/tests.rs | 2 +- 27 files changed, 397 insertions(+), 412 deletions(-) rename crates/collab/src/db/{tests.rs => db_tests.rs} (99%) create mode 100644 crates/collab/src/db/ids.rs delete mode 100644 crates/collab/src/db/signup.rs create mode 100644 crates/collab/src/db/tables.rs rename crates/collab/src/db/{ => tables}/access_token.rs (94%) rename crates/collab/src/db/{ => tables}/channel.rs (81%) rename crates/collab/src/db/{ => tables}/channel_member.rs (94%) rename crates/collab/src/db/{ => tables}/channel_path.rs (93%) rename crates/collab/src/db/{ => tables}/contact.rs (59%) rename crates/collab/src/db/{ => tables}/follower.rs (93%) rename crates/collab/src/db/{ => tables}/language_server.rs (96%) rename crates/collab/src/db/{ => tables}/project.rs (97%) rename crates/collab/src/db/{ => tables}/project_collaborator.rs (92%) rename crates/collab/src/db/{ => tables}/room.rs (97%) rename crates/collab/src/db/{ => tables}/room_participant.rs (94%) rename crates/collab/src/db/{ => tables}/server.rs (93%) create mode 100644 crates/collab/src/db/tables/signup.rs rename crates/collab/src/db/{ => tables}/user.rs (98%) rename crates/collab/src/db/{ => tables}/worktree.rs (97%) rename crates/collab/src/db/{ => tables}/worktree_diagnostic_summary.rs (95%) rename crates/collab/src/db/{ => tables}/worktree_entry.rs (96%) rename crates/collab/src/db/{ => tables}/worktree_repository.rs (95%) rename crates/collab/src/db/{ => tables}/worktree_repository_statuses.rs (95%) rename crates/collab/src/db/{ => tables}/worktree_settings_file.rs (94%) create mode 100644 crates/collab/src/db/test_db.rs diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index b457c4c116..f676e01339 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,56 +1,47 @@ -mod access_token; -mod channel; -mod channel_member; -mod channel_path; -mod contact; -mod follower; -mod language_server; -mod project; -mod project_collaborator; -mod room; -mod room_participant; -mod server; -mod signup; #[cfg(test)] -mod tests; -mod user; -mod worktree; -mod worktree_diagnostic_summary; -mod worktree_entry; -mod worktree_repository; -mod worktree_repository_statuses; -mod worktree_settings_file; +mod db_tests; +mod ids; +mod tables; +#[cfg(test)] +pub mod test_db; use crate::executor::Executor; use crate::{Error, Result}; use anyhow::anyhow; use collections::{BTreeMap, HashMap, HashSet}; -pub use contact::Contact; use dashmap::DashMap; use futures::StreamExt; use hyper::StatusCode; use rand::prelude::StdRng; use rand::{Rng, SeedableRng}; use rpc::{proto, ConnectionId}; -use sea_orm::Condition; -pub use sea_orm::ConnectOptions; use sea_orm::{ - entity::prelude::*, ActiveValue, ConnectionTrait, DatabaseConnection, DatabaseTransaction, - DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, - Statement, TransactionTrait, + entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, + DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, + QueryOrder, QuerySelect, Statement, TransactionTrait, }; use sea_query::{Alias, Expr, OnConflict, Query}; use serde::{Deserialize, Serialize}; -pub use signup::{Invite, NewSignup, WaitlistSummary}; -use sqlx::migrate::{Migrate, Migration, MigrationSource}; -use sqlx::Connection; -use std::fmt::Write as _; -use std::ops::{Deref, DerefMut}; -use std::path::Path; -use std::time::Duration; -use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc}; +use sqlx::{ + migrate::{Migrate, Migration, MigrationSource}, + Connection, +}; +use std::{ + fmt::Write as _, + future::Future, + marker::PhantomData, + ops::{Deref, DerefMut}, + path::Path, + rc::Rc, + sync::Arc, + time::Duration, +}; +use tables::*; use tokio::sync::{Mutex, OwnedMutexGuard}; -pub use user::Model as User; + +pub use ids::*; +pub use sea_orm::ConnectOptions; +pub use tables::user::Model as User; pub struct Database { options: ConnectOptions, @@ -4083,6 +4074,60 @@ 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, + }, +} + +impl Contact { + pub fn user_id(&self) -> UserId { + match self { + Contact::Accepted { user_id, .. } => *user_id, + Contact::Outgoing { user_id } => *user_id, + Contact::Incoming { user_id, .. } => *user_id, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] +pub struct Invite { + pub email_address: String, + pub email_confirmation_code: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct NewSignup { + pub email_address: String, + pub platform_mac: bool, + pub platform_windows: bool, + pub platform_linux: bool, + pub editor_features: Vec, + pub programming_languages: Vec, + pub device_id: Option, + pub added_to_mailing_list: bool, + pub created_at: Option, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] +pub struct WaitlistSummary { + pub count: i64, + pub linux_count: i64, + pub mac_count: i64, + pub windows_count: i64, + pub unknown_count: i64, +} + #[derive(Debug, Serialize, Deserialize)] pub struct NewUserParams { pub github_login: String, @@ -4120,139 +4165,6 @@ fn random_email_confirmation_code() -> String { nanoid::nanoid!(64) } -macro_rules! id_type { - ($name:ident) => { - #[derive( - Clone, - Copy, - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Serialize, - Deserialize, - )] - #[serde(transparent)] - pub struct $name(pub i32); - - impl $name { - #[allow(unused)] - pub const MAX: Self = Self(i32::MAX); - - #[allow(unused)] - pub fn from_proto(value: u64) -> Self { - Self(value as i32) - } - - #[allow(unused)] - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - self.0.fmt(f) - } - } - - impl From<$name> for sea_query::Value { - fn from(value: $name) -> Self { - sea_query::Value::Int(Some(value.0)) - } - } - - impl sea_orm::TryGetable for $name { - fn try_get( - res: &sea_orm::QueryResult, - pre: &str, - col: &str, - ) -> Result { - Ok(Self(i32::try_get(res, pre, col)?)) - } - } - - impl sea_query::ValueType for $name { - fn try_from(v: Value) -> Result { - match v { - Value::TinyInt(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::SmallInt(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::Int(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::BigInt(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::TinyUnsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::SmallUnsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::Unsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - Value::BigUnsigned(Some(int)) => { - Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?)) - } - _ => Err(sea_query::ValueTypeErr), - } - } - - fn type_name() -> String { - stringify!($name).into() - } - - fn array_type() -> sea_query::ArrayType { - sea_query::ArrayType::Int - } - - fn column_type() -> sea_query::ColumnType { - sea_query::ColumnType::Integer(None) - } - } - - impl sea_orm::TryFromU64 for $name { - fn try_from_u64(n: u64) -> Result { - Ok(Self(n.try_into().map_err(|_| { - DbErr::ConvertFromU64(concat!( - "error converting ", - stringify!($name), - " to u64" - )) - })?)) - } - } - - impl sea_query::Nullable for $name { - fn null() -> Value { - Value::Int(None) - } - } - }; -} - -id_type!(AccessTokenId); -id_type!(ChannelId); -id_type!(ChannelMemberId); -id_type!(ContactId); -id_type!(FollowerId); -id_type!(RoomId); -id_type!(RoomParticipantId); -id_type!(ProjectId); -id_type!(ProjectCollaboratorId); -id_type!(ReplicaId); -id_type!(ServerId); -id_type!(SignupId); -id_type!(UserId); - #[derive(Clone)] pub struct JoinRoom { pub room: proto::Room, @@ -4370,130 +4282,3 @@ pub struct WorktreeSettingsFile { enum QueryUserIds { UserId, } - -#[cfg(test)] -pub use test::*; - -#[cfg(test)] -mod test { - use super::*; - use gpui::executor::Background; - use parking_lot::Mutex; - use sea_orm::ConnectionTrait; - use sqlx::migrate::MigrateDatabase; - use std::sync::Arc; - - pub struct TestDb { - pub db: Option>, - pub connection: Option, - } - - impl TestDb { - pub fn sqlite(background: Arc) -> Self { - let url = format!("sqlite::memory:"); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - 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)) - .await - .unwrap(); - let sql = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/migrations.sqlite/20221109000000_test_schema.sql" - )); - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - sql.into(), - )) - .await - .unwrap(); - db - }); - - db.runtime = Some(runtime); - - Self { - db: Some(Arc::new(db)), - connection: None, - } - } - - pub fn postgres(background: Arc) -> Self { - static LOCK: Mutex<()> = Mutex::new(()); - - let _guard = LOCK.lock(); - let mut rng = StdRng::from_entropy(); - let url = format!( - "postgres://postgres@localhost/zed-test-{}", - rng.gen::() - ); - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .unwrap(); - - let mut db = runtime.block_on(async { - sqlx::Postgres::create_database(&url) - .await - .expect("failed to create test db"); - let mut options = ConnectOptions::new(url); - options - .max_connections(5) - .idle_timeout(Duration::from_secs(0)); - let 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 - }); - - db.runtime = Some(runtime); - - Self { - db: Some(Arc::new(db)), - connection: None, - } - } - - pub fn db(&self) -> &Arc { - self.db.as_ref().unwrap() - } - } - - impl Drop for TestDb { - fn drop(&mut self) { - let db = self.db.take().unwrap(); - if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { - db.runtime.as_ref().unwrap().block_on(async { - use util::ResultExt; - let query = " - SELECT pg_terminate_backend(pg_stat_activity.pid) - FROM pg_stat_activity - WHERE - pg_stat_activity.datname = current_database() AND - pid <> pg_backend_pid(); - "; - db.pool - .execute(sea_orm::Statement::from_string( - db.pool.get_database_backend(), - query.into(), - )) - .await - .log_err(); - sqlx::Postgres::drop_database(db.options.get_url()) - .await - .log_err(); - }) - } - } - } -} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/db_tests.rs similarity index 99% rename from crates/collab/src/db/tests.rs rename to crates/collab/src/db/db_tests.rs index dbbf162d12..8e9a80dbab 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/db_tests.rs @@ -1,9 +1,8 @@ use super::*; use gpui::executor::{Background, Deterministic}; -use std::sync::Arc; - -#[cfg(test)] use pretty_assertions::{assert_eq, assert_ne}; +use std::sync::Arc; +use test_db::TestDb; macro_rules! test_both_dbs { ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => { diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs new file mode 100644 index 0000000000..514c973dad --- /dev/null +++ b/crates/collab/src/db/ids.rs @@ -0,0 +1,125 @@ +use crate::Result; +use sea_orm::DbErr; +use sea_query::{Value, ValueTypeErr}; +use serde::{Deserialize, Serialize}; + +macro_rules! id_type { + ($name:ident) => { + #[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + )] + #[serde(transparent)] + pub struct $name(pub i32); + + impl $name { + #[allow(unused)] + pub const MAX: Self = Self(i32::MAX); + + #[allow(unused)] + pub fn from_proto(value: u64) -> Self { + Self(value as i32) + } + + #[allow(unused)] + pub fn to_proto(self) -> u64 { + self.0 as u64 + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl From<$name> for sea_query::Value { + fn from(value: $name) -> Self { + sea_query::Value::Int(Some(value.0)) + } + } + + impl sea_orm::TryGetable for $name { + fn try_get( + res: &sea_orm::QueryResult, + pre: &str, + col: &str, + ) -> Result { + Ok(Self(i32::try_get(res, pre, col)?)) + } + } + + impl sea_query::ValueType for $name { + fn try_from(v: Value) -> Result { + Ok(Self(value_to_integer(v)?)) + } + + fn type_name() -> String { + stringify!($name).into() + } + + fn array_type() -> sea_query::ArrayType { + sea_query::ArrayType::Int + } + + fn column_type() -> sea_query::ColumnType { + sea_query::ColumnType::Integer(None) + } + } + + impl sea_orm::TryFromU64 for $name { + fn try_from_u64(n: u64) -> Result { + Ok(Self(n.try_into().map_err(|_| { + DbErr::ConvertFromU64(concat!( + "error converting ", + stringify!($name), + " to u64" + )) + })?)) + } + } + + impl sea_query::Nullable for $name { + fn null() -> Value { + Value::Int(None) + } + } + }; +} + +fn value_to_integer(v: Value) -> Result { + match v { + Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr), + _ => Err(ValueTypeErr), + } +} + +id_type!(AccessTokenId); +id_type!(ChannelId); +id_type!(ChannelMemberId); +id_type!(ContactId); +id_type!(FollowerId); +id_type!(RoomId); +id_type!(RoomParticipantId); +id_type!(ProjectId); +id_type!(ProjectCollaboratorId); +id_type!(ReplicaId); +id_type!(ServerId); +id_type!(SignupId); +id_type!(UserId); diff --git a/crates/collab/src/db/signup.rs b/crates/collab/src/db/signup.rs deleted file mode 100644 index 6368482de9..0000000000 --- a/crates/collab/src/db/signup.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::{SignupId, UserId}; -use sea_orm::{entity::prelude::*, FromQueryResult}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "signups")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: SignupId, - pub email_address: String, - pub email_confirmation_code: String, - pub email_confirmation_sent: bool, - pub created_at: DateTime, - pub device_id: Option, - pub user_id: Option, - pub inviting_user_id: Option, - pub platform_mac: bool, - pub platform_linux: bool, - pub platform_windows: bool, - pub platform_unknown: bool, - pub editor_features: Option>, - pub programming_languages: Option>, - pub added_to_mailing_list: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} - -#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] -pub struct Invite { - pub email_address: String, - pub email_confirmation_code: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct NewSignup { - pub email_address: String, - pub platform_mac: bool, - pub platform_windows: bool, - pub platform_linux: bool, - pub editor_features: Vec, - pub programming_languages: Vec, - pub device_id: Option, - pub added_to_mailing_list: bool, - pub created_at: Option, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] -pub struct WaitlistSummary { - pub count: i64, - pub linux_count: i64, - pub mac_count: i64, - pub windows_count: i64, - pub unknown_count: i64, -} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs new file mode 100644 index 0000000000..c4c7e4f312 --- /dev/null +++ b/crates/collab/src/db/tables.rs @@ -0,0 +1,20 @@ +pub mod access_token; +pub mod channel; +pub mod channel_member; +pub mod channel_path; +pub mod contact; +pub mod follower; +pub mod language_server; +pub mod project; +pub mod project_collaborator; +pub mod room; +pub mod room_participant; +pub mod server; +pub mod signup; +pub mod user; +pub mod worktree; +pub mod worktree_diagnostic_summary; +pub mod worktree_entry; +pub mod worktree_repository; +pub mod worktree_repository_statuses; +pub mod worktree_settings_file; diff --git a/crates/collab/src/db/access_token.rs b/crates/collab/src/db/tables/access_token.rs similarity index 94% rename from crates/collab/src/db/access_token.rs rename to crates/collab/src/db/tables/access_token.rs index f5caa4843d..da7392b98c 100644 --- a/crates/collab/src/db/access_token.rs +++ b/crates/collab/src/db/tables/access_token.rs @@ -1,4 +1,4 @@ -use super::{AccessTokenId, UserId}; +use crate::db::{AccessTokenId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/channel.rs b/crates/collab/src/db/tables/channel.rs similarity index 81% rename from crates/collab/src/db/channel.rs rename to crates/collab/src/db/tables/channel.rs index 8834190645..f00b4ced62 100644 --- a/crates/collab/src/db/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use super::ChannelId; +use crate::db::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -30,9 +30,3 @@ impl Related for Entity { Relation::Room.def() } } - -// impl Related for Entity { -// fn to() -> RelationDef { -// Relation::Follower.def() -// } -// } diff --git a/crates/collab/src/db/channel_member.rs b/crates/collab/src/db/tables/channel_member.rs similarity index 94% rename from crates/collab/src/db/channel_member.rs rename to crates/collab/src/db/tables/channel_member.rs index f0f1a852cb..ba3db5a155 100644 --- a/crates/collab/src/db/channel_member.rs +++ b/crates/collab/src/db/tables/channel_member.rs @@ -1,6 +1,4 @@ -use crate::db::channel_member; - -use super::{ChannelId, ChannelMemberId, UserId}; +use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/channel_path.rs b/crates/collab/src/db/tables/channel_path.rs similarity index 93% rename from crates/collab/src/db/channel_path.rs rename to crates/collab/src/db/tables/channel_path.rs index 08ecbddb56..323f116dae 100644 --- a/crates/collab/src/db/channel_path.rs +++ b/crates/collab/src/db/tables/channel_path.rs @@ -1,4 +1,4 @@ -use super::ChannelId; +use crate::db::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/contact.rs b/crates/collab/src/db/tables/contact.rs similarity index 59% rename from crates/collab/src/db/contact.rs rename to crates/collab/src/db/tables/contact.rs index c39d6643b3..38af8b782b 100644 --- a/crates/collab/src/db/contact.rs +++ b/crates/collab/src/db/tables/contact.rs @@ -1,4 +1,4 @@ -use super::{ContactId, UserId}; +use crate::db::{ContactId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -30,29 +30,3 @@ pub enum Relation { } impl ActiveModelBehavior for ActiveModel {} - -#[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, - }, -} - -impl Contact { - pub fn user_id(&self) -> UserId { - match self { - Contact::Accepted { user_id, .. } => *user_id, - Contact::Outgoing { user_id } => *user_id, - Contact::Incoming { user_id, .. } => *user_id, - } - } -} diff --git a/crates/collab/src/db/follower.rs b/crates/collab/src/db/tables/follower.rs similarity index 93% rename from crates/collab/src/db/follower.rs rename to crates/collab/src/db/tables/follower.rs index f1243dc99e..ffd45434e9 100644 --- a/crates/collab/src/db/follower.rs +++ b/crates/collab/src/db/tables/follower.rs @@ -1,9 +1,8 @@ -use super::{FollowerId, ProjectId, RoomId, ServerId}; +use crate::db::{FollowerId, ProjectId, RoomId, ServerId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; -use serde::Serialize; -#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "followers")] pub struct Model { #[sea_orm(primary_key)] diff --git a/crates/collab/src/db/language_server.rs b/crates/collab/src/db/tables/language_server.rs similarity index 96% rename from crates/collab/src/db/language_server.rs rename to crates/collab/src/db/tables/language_server.rs index d2c045c121..9ff8c75fc6 100644 --- a/crates/collab/src/db/language_server.rs +++ b/crates/collab/src/db/tables/language_server.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/project.rs b/crates/collab/src/db/tables/project.rs similarity index 97% rename from crates/collab/src/db/project.rs rename to crates/collab/src/db/tables/project.rs index 5b1f9f8467..8c26836046 100644 --- a/crates/collab/src/db/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -1,4 +1,4 @@ -use super::{ProjectId, Result, RoomId, ServerId, UserId}; +use crate::db::{ProjectId, Result, RoomId, ServerId, UserId}; use anyhow::anyhow; use rpc::ConnectionId; use sea_orm::entity::prelude::*; diff --git a/crates/collab/src/db/project_collaborator.rs b/crates/collab/src/db/tables/project_collaborator.rs similarity index 92% rename from crates/collab/src/db/project_collaborator.rs rename to crates/collab/src/db/tables/project_collaborator.rs index 60b5f284e9..ac57befa63 100644 --- a/crates/collab/src/db/project_collaborator.rs +++ b/crates/collab/src/db/tables/project_collaborator.rs @@ -1,4 +1,4 @@ -use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId}; +use crate::db::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; diff --git a/crates/collab/src/db/room.rs b/crates/collab/src/db/tables/room.rs similarity index 97% rename from crates/collab/src/db/room.rs rename to crates/collab/src/db/tables/room.rs index c1624f0f2a..f72f7000a7 100644 --- a/crates/collab/src/db/room.rs +++ b/crates/collab/src/db/tables/room.rs @@ -1,4 +1,4 @@ -use super::{ChannelId, RoomId}; +use crate::db::{ChannelId, RoomId}; use sea_orm::entity::prelude::*; #[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs similarity index 94% rename from crates/collab/src/db/room_participant.rs rename to crates/collab/src/db/tables/room_participant.rs index f939a3bfb8..537cac9f14 100644 --- a/crates/collab/src/db/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -1,4 +1,4 @@ -use super::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; +use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/server.rs b/crates/collab/src/db/tables/server.rs similarity index 93% rename from crates/collab/src/db/server.rs rename to crates/collab/src/db/tables/server.rs index e3905f2448..ea847bdf74 100644 --- a/crates/collab/src/db/server.rs +++ b/crates/collab/src/db/tables/server.rs @@ -1,4 +1,4 @@ -use super::ServerId; +use crate::db::ServerId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/tables/signup.rs b/crates/collab/src/db/tables/signup.rs new file mode 100644 index 0000000000..79d9f0580c --- /dev/null +++ b/crates/collab/src/db/tables/signup.rs @@ -0,0 +1,28 @@ +use crate::db::{SignupId, UserId}; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "signups")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: SignupId, + pub email_address: String, + pub email_confirmation_code: String, + pub email_confirmation_sent: bool, + pub created_at: DateTime, + pub device_id: Option, + pub user_id: Option, + pub inviting_user_id: Option, + pub platform_mac: bool, + pub platform_linux: bool, + pub platform_windows: bool, + pub platform_unknown: bool, + pub editor_features: Option>, + pub programming_languages: Option>, + pub added_to_mailing_list: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/user.rs b/crates/collab/src/db/tables/user.rs similarity index 98% rename from crates/collab/src/db/user.rs rename to crates/collab/src/db/tables/user.rs index 2d0e2fdf0b..402b06c2a7 100644 --- a/crates/collab/src/db/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -1,4 +1,4 @@ -use super::UserId; +use crate::db::UserId; use sea_orm::entity::prelude::*; use serde::Serialize; diff --git a/crates/collab/src/db/worktree.rs b/crates/collab/src/db/tables/worktree.rs similarity index 97% rename from crates/collab/src/db/worktree.rs rename to crates/collab/src/db/tables/worktree.rs index fce72722db..46d9877dff 100644 --- a/crates/collab/src/db/worktree.rs +++ b/crates/collab/src/db/tables/worktree.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_diagnostic_summary.rs b/crates/collab/src/db/tables/worktree_diagnostic_summary.rs similarity index 95% rename from crates/collab/src/db/worktree_diagnostic_summary.rs rename to crates/collab/src/db/tables/worktree_diagnostic_summary.rs index f3dd8083fb..5620ed255f 100644 --- a/crates/collab/src/db/worktree_diagnostic_summary.rs +++ b/crates/collab/src/db/tables/worktree_diagnostic_summary.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_entry.rs b/crates/collab/src/db/tables/worktree_entry.rs similarity index 96% rename from crates/collab/src/db/worktree_entry.rs rename to crates/collab/src/db/tables/worktree_entry.rs index cf5090ab6d..81bf6e2d53 100644 --- a/crates/collab/src/db/worktree_entry.rs +++ b/crates/collab/src/db/tables/worktree_entry.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_repository.rs b/crates/collab/src/db/tables/worktree_repository.rs similarity index 95% rename from crates/collab/src/db/worktree_repository.rs rename to crates/collab/src/db/tables/worktree_repository.rs index 116d7b3ed9..6f86ff0c2d 100644 --- a/crates/collab/src/db/worktree_repository.rs +++ b/crates/collab/src/db/tables/worktree_repository.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_repository_statuses.rs b/crates/collab/src/db/tables/worktree_repository_statuses.rs similarity index 95% rename from crates/collab/src/db/worktree_repository_statuses.rs rename to crates/collab/src/db/tables/worktree_repository_statuses.rs index fc15efc816..cab016749d 100644 --- a/crates/collab/src/db/worktree_repository_statuses.rs +++ b/crates/collab/src/db/tables/worktree_repository_statuses.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/worktree_settings_file.rs b/crates/collab/src/db/tables/worktree_settings_file.rs similarity index 94% rename from crates/collab/src/db/worktree_settings_file.rs rename to crates/collab/src/db/tables/worktree_settings_file.rs index f8e87f6e59..92348c1ec9 100644 --- a/crates/collab/src/db/worktree_settings_file.rs +++ b/crates/collab/src/db/tables/worktree_settings_file.rs @@ -1,4 +1,4 @@ -use super::ProjectId; +use crate::db::ProjectId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/test_db.rs new file mode 100644 index 0000000000..064f85c700 --- /dev/null +++ b/crates/collab/src/db/test_db.rs @@ -0,0 +1,120 @@ +use super::*; +use gpui::executor::Background; +use parking_lot::Mutex; +use sea_orm::ConnectionTrait; +use sqlx::migrate::MigrateDatabase; +use std::sync::Arc; + +pub struct TestDb { + pub db: Option>, + pub connection: Option, +} + +impl TestDb { + pub fn sqlite(background: Arc) -> Self { + let url = format!("sqlite::memory:"); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .unwrap(); + + 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)) + .await + .unwrap(); + let sql = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/migrations.sqlite/20221109000000_test_schema.sql" + )); + db.pool + .execute(sea_orm::Statement::from_string( + db.pool.get_database_backend(), + sql.into(), + )) + .await + .unwrap(); + db + }); + + db.runtime = Some(runtime); + + Self { + db: Some(Arc::new(db)), + connection: None, + } + } + + pub fn postgres(background: Arc) -> Self { + static LOCK: Mutex<()> = Mutex::new(()); + + let _guard = LOCK.lock(); + let mut rng = StdRng::from_entropy(); + let url = format!( + "postgres://postgres@localhost/zed-test-{}", + rng.gen::() + ); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .unwrap(); + + let mut db = runtime.block_on(async { + sqlx::Postgres::create_database(&url) + .await + .expect("failed to create test db"); + let mut options = ConnectOptions::new(url); + options + .max_connections(5) + .idle_timeout(Duration::from_secs(0)); + let 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 + }); + + db.runtime = Some(runtime); + + Self { + db: Some(Arc::new(db)), + connection: None, + } + } + + pub fn db(&self) -> &Arc { + self.db.as_ref().unwrap() + } +} + +impl Drop for TestDb { + fn drop(&mut self) { + let db = self.db.take().unwrap(); + if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() { + db.runtime.as_ref().unwrap().block_on(async { + use util::ResultExt; + let query = " + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE + pg_stat_activity.datname = current_database() AND + pid <> pg_backend_pid(); + "; + db.pool + .execute(sea_orm::Statement::from_string( + db.pool.get_database_backend(), + query.into(), + )) + .await + .log_err(); + sqlx::Postgres::drop_database(db.options.get_url()) + .await + .log_err(); + }) + } + } +} diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 46cbcb0213..c9f358ca5b 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,5 +1,5 @@ use crate::{ - db::{NewUserParams, TestDb, UserId}, + db::{test_db::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT}, AppState, From b35aaf144b08478be3bc5f259bcdf30be5aef0d4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Aug 2023 16:54:19 -0700 Subject: [PATCH 311/326] Break up Database impl into multiple files, organized by topic --- crates/collab/src/db.rs | 3752 +---------------- crates/collab/src/db/queries.rs | 10 + crates/collab/src/db/queries/access_tokens.rs | 53 + crates/collab/src/db/queries/channels.rs | 697 +++ crates/collab/src/db/queries/contacts.rs | 298 ++ crates/collab/src/db/queries/projects.rs | 926 ++++ crates/collab/src/db/queries/rooms.rs | 1073 +++++ crates/collab/src/db/queries/servers.rs | 81 + crates/collab/src/db/queries/signups.rs | 349 ++ crates/collab/src/db/queries/users.rs | 243 ++ 10 files changed, 3736 insertions(+), 3746 deletions(-) create mode 100644 crates/collab/src/db/queries.rs create mode 100644 crates/collab/src/db/queries/access_tokens.rs create mode 100644 crates/collab/src/db/queries/channels.rs create mode 100644 crates/collab/src/db/queries/contacts.rs create mode 100644 crates/collab/src/db/queries/projects.rs create mode 100644 crates/collab/src/db/queries/rooms.rs create mode 100644 crates/collab/src/db/queries/servers.rs create mode 100644 crates/collab/src/db/queries/signups.rs create mode 100644 crates/collab/src/db/queries/users.rs diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index f676e01339..d322b03589 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,19 +1,18 @@ #[cfg(test)] mod db_tests; -mod ids; -mod tables; #[cfg(test)] pub mod test_db; -use crate::executor::Executor; -use crate::{Error, Result}; +mod ids; +mod queries; +mod tables; + +use crate::{executor::Executor, Error, Result}; use anyhow::anyhow; use collections::{BTreeMap, HashMap, HashSet}; use dashmap::DashMap; use futures::StreamExt; -use hyper::StatusCode; -use rand::prelude::StdRng; -use rand::{Rng, SeedableRng}; +use rand::{prelude::StdRng, Rng, SeedableRng}; use rpc::{proto, ConnectionId}; use sea_orm::{ entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection, @@ -112,3732 +111,6 @@ impl Database { Ok(new_migrations) } - pub async fn create_server(&self, environment: &str) -> Result { - self.transaction(|tx| async move { - let server = server::ActiveModel { - environment: ActiveValue::set(environment.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - Ok(server.id) - }) - .await - } - - pub async fn stale_room_ids( - &self, - environment: &str, - new_server_id: ServerId, - ) -> Result> { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - RoomId, - } - - let stale_server_epochs = self - .stale_server_ids(environment, new_server_id, &tx) - .await?; - Ok(room_participant::Entity::find() - .select_only() - .column(room_participant::Column::RoomId) - .distinct() - .filter( - room_participant::Column::AnsweringConnectionServerId - .is_in(stale_server_epochs), - ) - .into_values::<_, QueryAs>() - .all(&*tx) - .await?) - }) - .await - } - - pub async fn refresh_room( - &self, - room_id: RoomId, - new_server_id: ServerId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let stale_participant_filter = Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add(room_participant::Column::AnsweringConnectionId.is_not_null()) - .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id)); - - let stale_participant_user_ids = room_participant::Entity::find() - .filter(stale_participant_filter.clone()) - .all(&*tx) - .await? - .into_iter() - .map(|participant| participant.user_id) - .collect::>(); - - // Delete participants who failed to reconnect and cancel their calls. - let mut canceled_calls_to_user_ids = Vec::new(); - room_participant::Entity::delete_many() - .filter(stale_participant_filter) - .exec(&*tx) - .await?; - let called_participants = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::CallingUserId - .is_in(stale_participant_user_ids.iter().copied()), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .all(&*tx) - .await?; - room_participant::Entity::delete_many() - .filter( - room_participant::Column::Id - .is_in(called_participants.iter().map(|participant| participant.id)), - ) - .exec(&*tx) - .await?; - canceled_calls_to_user_ids.extend( - called_participants - .into_iter() - .map(|participant| participant.user_id), - ); - - 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?; - } else { - channel_members = Vec::new(); - - // Delete the room if it becomes empty. - if room.participants.is_empty() { - project::Entity::delete_many() - .filter(project::Column::RoomId.eq(room_id)) - .exec(&*tx) - .await?; - room::Entity::delete_by_id(room_id).exec(&*tx).await?; - } - }; - - Ok(RefreshedRoom { - room, - channel_id, - channel_members, - stale_participant_user_ids, - canceled_calls_to_user_ids, - }) - }) - .await - } - - pub async fn delete_stale_servers( - &self, - environment: &str, - new_server_id: ServerId, - ) -> Result<()> { - self.transaction(|tx| async move { - server::Entity::delete_many() - .filter( - Condition::all() - .add(server::Column::Environment.eq(environment)) - .add(server::Column::Id.ne(new_server_id)), - ) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - async fn stale_server_ids( - &self, - environment: &str, - new_server_id: ServerId, - tx: &DatabaseTransaction, - ) -> Result> { - let stale_servers = server::Entity::find() - .filter( - Condition::all() - .add(server::Column::Environment.eq(environment)) - .add(server::Column::Id.ne(new_server_id)), - ) - .all(&*tx) - .await?; - Ok(stale_servers.into_iter().map(|server| server.id).collect()) - } - - // users - - pub async fn create_user( - &self, - email_address: &str, - admin: bool, - params: NewUserParams, - ) -> Result { - self.transaction(|tx| async { - let tx = tx; - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(Some(email_address.into())), - github_login: ActiveValue::set(params.github_login.clone()), - github_user_id: ActiveValue::set(Some(params.github_user_id)), - admin: ActiveValue::set(admin), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .on_conflict( - OnConflict::column(user::Column::GithubLogin) - .update_column(user::Column::GithubLogin) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - Ok(NewUserResult { - user_id: user.id, - metrics_id: user.metrics_id.to_string(), - signup_device_id: None, - inviting_user_id: None, - }) - }) - .await - } - - pub async fn get_user_by_id(&self, id: UserId) -> Result> { - self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) - .await - } - - pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { - self.transaction(|tx| async { - let tx = tx; - Ok(user::Entity::find() - .filter(user::Column::Id.is_in(ids.iter().copied())) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(&*tx) - .await?) - }) - .await - } - - pub async fn get_or_create_user_by_github_account( - &self, - github_login: &str, - github_user_id: Option, - github_email: Option<&str>, - ) -> Result> { - self.transaction(|tx| async move { - let tx = &*tx; - if let Some(github_user_id) = github_user_id { - if let Some(user_by_github_user_id) = user::Entity::find() - .filter(user::Column::GithubUserId.eq(github_user_id)) - .one(tx) - .await? - { - let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); - user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); - Ok(Some(user_by_github_user_id.update(tx).await?)) - } else if let Some(user_by_github_login) = user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await? - { - let mut user_by_github_login = user_by_github_login.into_active_model(); - user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); - Ok(Some(user_by_github_login.update(tx).await?)) - } else { - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(github_email.map(|email| email.into())), - github_login: ActiveValue::set(github_login.into()), - github_user_id: ActiveValue::set(Some(github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(0), - invite_code: ActiveValue::set(None), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - Ok(Some(user)) - } - } else { - Ok(user::Entity::find() - .filter(user::Column::GithubLogin.eq(github_login)) - .one(tx) - .await?) - } - }) - .await - } - - pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .order_by_asc(user::Column::GithubLogin) - .limit(limit as u64) - .offset(page as u64 * limit as u64) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_users_with_no_invites( - &self, - invited_by_another_user: bool, - ) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .filter( - user::Column::InviteCount - .eq(0) - .and(if invited_by_another_user { - user::Column::InviterId.is_not_null() - } else { - user::Column::InviterId.is_null() - }), - ) - .all(&*tx) - .await?) - }) - .await - } - - pub async fn get_user_metrics_id(&self, id: UserId) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - MetricsId, - } - - self.transaction(|tx| async move { - let metrics_id: Uuid = user::Entity::find_by_id(id) - .select_only() - .column(user::Column::MetricsId) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("could not find user"))?; - Ok(metrics_id.to_string()) - }) - .await - } - - pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { - self.transaction(|tx| async move { - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - admin: ActiveValue::set(is_admin), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { - self.transaction(|tx| async move { - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - connected_once: ActiveValue::set(connected_once), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn destroy_user(&self, id: UserId) -> Result<()> { - self.transaction(|tx| async move { - access_token::Entity::delete_many() - .filter(access_token::Column::UserId.eq(id)) - .exec(&*tx) - .await?; - user::Entity::delete_by_id(id).exec(&*tx).await?; - Ok(()) - }) - .await - } - - // contacts - - pub async fn get_contacts(&self, user_id: UserId) -> Result> { - #[derive(Debug, FromQueryResult)] - struct ContactWithUserBusyStatuses { - user_id_a: UserId, - user_id_b: UserId, - a_to_b: bool, - accepted: bool, - should_notify: bool, - user_a_busy: bool, - user_b_busy: bool, - } - - self.transaction(|tx| async move { - let user_a_participant = Alias::new("user_a_participant"); - let user_b_participant = Alias::new("user_b_participant"); - let mut db_contacts = contact::Entity::find() - .column_as( - Expr::tbl(user_a_participant.clone(), room_participant::Column::Id) - .is_not_null(), - "user_a_busy", - ) - .column_as( - Expr::tbl(user_b_participant.clone(), room_participant::Column::Id) - .is_not_null(), - "user_b_busy", - ) - .filter( - contact::Column::UserIdA - .eq(user_id) - .or(contact::Column::UserIdB.eq(user_id)), - ) - .join_as( - JoinType::LeftJoin, - contact::Relation::UserARoomParticipant.def(), - user_a_participant, - ) - .join_as( - JoinType::LeftJoin, - contact::Relation::UserBRoomParticipant.def(), - user_b_participant, - ) - .into_model::() - .stream(&*tx) - .await?; - - let mut contacts = Vec::new(); - while let Some(db_contact) = db_contacts.next().await { - let db_contact = db_contact?; - if db_contact.user_id_a == user_id { - 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 { - contacts.push(Contact::Outgoing { - user_id: db_contact.user_id_b, - }) - } 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 { - user_id: db_contact.user_id_a, - }); - } - } - - contacts.sort_unstable_by_key(|contact| contact.user_id()); - - Ok(contacts) - }) - .await - } - - pub async fn is_user_busy(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let participant = room_participant::Entity::find() - .filter(room_participant::Column::UserId.eq(user_id)) - .one(&*tx) - .await?; - Ok(participant.is_some()) - }) - .await - } - - pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { - self.transaction(|tx| async move { - let (id_a, id_b) = if user_id_1 < user_id_2 { - (user_id_1, user_id_2) - } else { - (user_id_2, user_id_1) - }; - - Ok(contact::Entity::find() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::Accepted.eq(true)), - ) - .one(&*tx) - .await? - .is_some()) - }) - .await - } - - 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) - } else { - (receiver_id, sender_id, false) - }; - - let rows_affected = contact::Entity::insert(contact::ActiveModel { - user_id_a: ActiveValue::set(id_a), - user_id_b: ActiveValue::set(id_b), - a_to_b: ActiveValue::set(a_to_b), - accepted: ActiveValue::set(false), - should_notify: ActiveValue::set(true), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB]) - .values([ - (contact::Column::Accepted, true.into()), - (contact::Column::ShouldNotify, false.into()), - ]) - .action_and_where( - contact::Column::Accepted.eq(false).and( - contact::Column::AToB - .eq(a_to_b) - .and(contact::Column::UserIdA.eq(id_b)) - .or(contact::Column::AToB - .ne(a_to_b) - .and(contact::Column::UserIdA.eq(id_a))), - ), - ) - .to_owned(), - ) - .exec_without_returning(&*tx) - .await?; - - if rows_affected == 1 { - Ok(()) - } else { - Err(anyhow!("contact already requested"))? - } - }) - .await - } - - /// Returns a bool indicating whether the removed contact had originally accepted or not - /// - /// Deletes the contact identified by the requester and responder ids, and then returns - /// whether the deleted contact had originally accepted or was a pending contact request. - /// - /// # Arguments - /// - /// * `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 { - self.transaction(|tx| async move { - let (id_a, id_b) = if responder_id < requester_id { - (responder_id, requester_id) - } else { - (requester_id, responder_id) - }; - - let contact = contact::Entity::find() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such contact"))?; - - contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; - Ok(contact.accepted) - }) - .await - } - - pub async fn dismiss_contact_notification( - &self, - user_id: UserId, - contact_user_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if user_id < contact_user_id { - (user_id, contact_user_id, true) - } else { - (contact_user_id, user_id, false) - }; - - let result = contact::Entity::update_many() - .set(contact::ActiveModel { - should_notify: ActiveValue::set(false), - ..Default::default() - }) - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and( - contact::Column::AToB - .eq(a_to_b) - .and(contact::Column::Accepted.eq(true)) - .or(contact::Column::AToB - .ne(a_to_b) - .and(contact::Column::Accepted.eq(false))), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("no such contact request"))? - } else { - Ok(()) - } - }) - .await - } - - pub async fn respond_to_contact_request( - &self, - responder_id: UserId, - requester_id: UserId, - accept: bool, - ) -> Result<()> { - self.transaction(|tx| async move { - let (id_a, id_b, a_to_b) = if responder_id < requester_id { - (responder_id, requester_id, false) - } else { - (requester_id, responder_id, true) - }; - let rows_affected = if accept { - let result = contact::Entity::update_many() - .set(contact::ActiveModel { - accepted: ActiveValue::set(true), - should_notify: ActiveValue::set(true), - ..Default::default() - }) - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::AToB.eq(a_to_b)), - ) - .exec(&*tx) - .await?; - result.rows_affected - } else { - let result = contact::Entity::delete_many() - .filter( - contact::Column::UserIdA - .eq(id_a) - .and(contact::Column::UserIdB.eq(id_b)) - .and(contact::Column::AToB.eq(a_to_b)) - .and(contact::Column::Accepted.eq(false)), - ) - .exec(&*tx) - .await?; - - result.rows_affected - }; - - if rows_affected == 1 { - Ok(()) - } else { - Err(anyhow!("no such contact request"))? - } - }) - .await - } - - pub fn fuzzy_like_string(string: &str) -> String { - let mut result = String::with_capacity(string.len() * 2 + 1); - for c in string.chars() { - if c.is_alphanumeric() { - result.push('%'); - result.push(c); - } - } - result.push('%'); - result - } - - pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { - self.transaction(|tx| async { - let tx = tx; - let like_string = Self::fuzzy_like_string(name_query); - let query = " - SELECT users.* - FROM users - WHERE github_login ILIKE $1 - ORDER BY github_login <-> $2 - LIMIT $3 - "; - - Ok(user::Entity::find() - .from_raw_sql(Statement::from_sql_and_values( - self.pool.get_database_backend(), - query.into(), - vec![like_string.into(), name_query.into(), limit.into()], - )) - .all(&*tx) - .await?) - }) - .await - } - - // signups - - pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> { - self.transaction(|tx| async move { - signup::Entity::insert(signup::ActiveModel { - email_address: ActiveValue::set(signup.email_address.clone()), - email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), - email_confirmation_sent: ActiveValue::set(false), - platform_mac: ActiveValue::set(signup.platform_mac), - platform_windows: ActiveValue::set(signup.platform_windows), - platform_linux: ActiveValue::set(signup.platform_linux), - platform_unknown: ActiveValue::set(false), - editor_features: ActiveValue::set(Some(signup.editor_features.clone())), - programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), - device_id: ActiveValue::set(signup.device_id.clone()), - added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list), - ..Default::default() - }) - .on_conflict( - OnConflict::column(signup::Column::EmailAddress) - .update_columns([ - signup::Column::PlatformMac, - signup::Column::PlatformWindows, - signup::Column::PlatformLinux, - signup::Column::EditorFeatures, - signup::Column::ProgrammingLanguages, - signup::Column::DeviceId, - signup::Column::AddedToMailingList, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_signup(&self, email_address: &str) -> Result { - self.transaction(|tx| async move { - let signup = signup::Entity::find() - .filter(signup::Column::EmailAddress.eq(email_address)) - .one(&*tx) - .await? - .ok_or_else(|| { - anyhow!("signup with email address {} doesn't exist", email_address) - })?; - - Ok(signup) - }) - .await - } - - pub async fn get_waitlist_summary(&self) -> Result { - self.transaction(|tx| async move { - let query = " - SELECT - COUNT(*) as count, - COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count, - COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count, - COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count, - COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count - FROM ( - SELECT * - FROM signups - WHERE - NOT email_confirmation_sent - ) AS unsent - "; - Ok( - WaitlistSummary::find_by_statement(Statement::from_sql_and_values( - self.pool.get_database_backend(), - query.into(), - vec![], - )) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid result"))?, - ) - }) - .await - } - - pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> { - let emails = invites - .iter() - .map(|s| s.email_address.as_str()) - .collect::>(); - self.transaction(|tx| async { - let tx = tx; - signup::Entity::update_many() - .filter(signup::Column::EmailAddress.is_in(emails.iter().copied())) - .set(signup::ActiveModel { - email_confirmation_sent: ActiveValue::set(true), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_unsent_invites(&self, count: usize) -> Result> { - self.transaction(|tx| async move { - Ok(signup::Entity::find() - .select_only() - .column(signup::Column::EmailAddress) - .column(signup::Column::EmailConfirmationCode) - .filter( - signup::Column::EmailConfirmationSent.eq(false).and( - signup::Column::PlatformMac - .eq(true) - .or(signup::Column::PlatformUnknown.eq(true)), - ), - ) - .order_by_asc(signup::Column::CreatedAt) - .limit(count as u64) - .into_model() - .all(&*tx) - .await?) - }) - .await - } - - // invite codes - - pub async fn create_invite_from_code( - &self, - code: &str, - email_address: &str, - device_id: Option<&str>, - added_to_mailing_list: bool, - ) -> Result { - self.transaction(|tx| async move { - let existing_user = user::Entity::find() - .filter(user::Column::EmailAddress.eq(email_address)) - .one(&*tx) - .await?; - - if existing_user.is_some() { - Err(anyhow!("email address is already in use"))?; - } - - let inviting_user_with_invites = match user::Entity::find() - .filter( - user::Column::InviteCode - .eq(code) - .and(user::Column::InviteCount.gt(0)), - ) - .one(&*tx) - .await? - { - Some(inviting_user) => inviting_user, - None => { - return Err(Error::Http( - StatusCode::UNAUTHORIZED, - "unable to find an invite code with invites remaining".to_string(), - ))? - } - }; - user::Entity::update_many() - .filter( - user::Column::Id - .eq(inviting_user_with_invites.id) - .and(user::Column::InviteCount.gt(0)), - ) - .col_expr( - user::Column::InviteCount, - Expr::col(user::Column::InviteCount).sub(1), - ) - .exec(&*tx) - .await?; - - let signup = signup::Entity::insert(signup::ActiveModel { - email_address: ActiveValue::set(email_address.into()), - email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), - email_confirmation_sent: ActiveValue::set(false), - inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)), - platform_linux: ActiveValue::set(false), - platform_mac: ActiveValue::set(false), - platform_windows: ActiveValue::set(false), - platform_unknown: ActiveValue::set(true), - device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())), - added_to_mailing_list: ActiveValue::set(added_to_mailing_list), - ..Default::default() - }) - .on_conflict( - OnConflict::column(signup::Column::EmailAddress) - .update_column(signup::Column::InvitingUserId) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - Ok(Invite { - email_address: signup.email_address, - email_confirmation_code: signup.email_confirmation_code, - }) - }) - .await - } - - pub async fn create_user_from_invite( - &self, - invite: &Invite, - user: NewUserParams, - ) -> Result> { - self.transaction(|tx| async { - let tx = tx; - let signup = signup::Entity::find() - .filter( - signup::Column::EmailAddress - .eq(invite.email_address.as_str()) - .and( - signup::Column::EmailConfirmationCode - .eq(invite.email_confirmation_code.as_str()), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?; - - if signup.user_id.is_some() { - return Ok(None); - } - - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(Some(invite.email_address.clone())), - github_login: ActiveValue::set(user.github_login.clone()), - github_user_id: ActiveValue::set(Some(user.github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(user.invite_count), - invite_code: ActiveValue::set(Some(random_invite_code())), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .on_conflict( - OnConflict::column(user::Column::GithubLogin) - .update_columns([ - user::Column::EmailAddress, - user::Column::GithubUserId, - user::Column::Admin, - ]) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - let mut signup = signup.into_active_model(); - signup.user_id = ActiveValue::set(Some(user.id)); - let signup = signup.update(&*tx).await?; - - if let Some(inviting_user_id) = signup.inviting_user_id { - let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id { - (inviting_user_id, user.id, true) - } else { - (user.id, inviting_user_id, false) - }; - - contact::Entity::insert(contact::ActiveModel { - user_id_a: ActiveValue::set(user_id_a), - user_id_b: ActiveValue::set(user_id_b), - a_to_b: ActiveValue::set(a_to_b), - should_notify: ActiveValue::set(true), - accepted: ActiveValue::set(true), - ..Default::default() - }) - .on_conflict(OnConflict::new().do_nothing().to_owned()) - .exec_without_returning(&*tx) - .await?; - } - - Ok(Some(NewUserResult { - user_id: user.id, - metrics_id: user.metrics_id.to_string(), - inviting_user_id: signup.inviting_user_id, - signup_device_id: signup.device_id, - })) - }) - .await - } - - pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> { - self.transaction(|tx| async move { - if count > 0 { - user::Entity::update_many() - .filter( - user::Column::Id - .eq(id) - .and(user::Column::InviteCode.is_null()), - ) - .set(user::ActiveModel { - invite_code: ActiveValue::set(Some(random_invite_code())), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - invite_count: ActiveValue::set(count), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_invite_code_for_user(&self, id: UserId) -> Result> { - self.transaction(|tx| async move { - match user::Entity::find_by_id(id).one(&*tx).await? { - Some(user) if user.invite_code.is_some() => { - Ok(Some((user.invite_code.unwrap(), user.invite_count))) - } - _ => Ok(None), - } - }) - .await - } - - pub async fn get_user_for_invite_code(&self, code: &str) -> Result { - self.transaction(|tx| async move { - user::Entity::find() - .filter(user::Column::InviteCode.eq(code)) - .one(&*tx) - .await? - .ok_or_else(|| { - Error::Http( - StatusCode::NOT_FOUND, - "that invite code does not exist".to_string(), - ) - }) - }) - .await - } - - // rooms - - pub async fn incoming_call_for_user( - &self, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let pending_participant = room_participant::Entity::find() - .filter( - room_participant::Column::UserId - .eq(user_id) - .and(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .one(&*tx) - .await?; - - if let Some(pending_participant) = pending_participant { - let room = self.get_room(pending_participant.room_id, &tx).await?; - Ok(Self::build_incoming_call(&room, user_id)) - } else { - Ok(None) - } - }) - .await - } - - pub async fn create_room( - &self, - user_id: UserId, - connection: ConnectionId, - live_kit_room: &str, - ) -> Result { - self.transaction(|tx| async move { - let room = room::ActiveModel { - live_kit_room: ActiveValue::set(live_kit_room.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - 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, - ))), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room.id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn call( - &self, - room_id: RoomId, - calling_user_id: UserId, - calling_connection: ConnectionId, - called_user_id: UserId, - initial_project_id: Option, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - room_participant::ActiveModel { - room_id: ActiveValue::set(room_id), - user_id: ActiveValue::set(called_user_id), - answering_connection_lost: ActiveValue::set(false), - calling_user_id: ActiveValue::set(calling_user_id), - calling_connection_id: ActiveValue::set(calling_connection.id as i32), - calling_connection_server_id: ActiveValue::set(Some(ServerId( - calling_connection.owner_id as i32, - ))), - initial_project_id: ActiveValue::set(initial_project_id), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - let incoming_call = Self::build_incoming_call(&room, called_user_id) - .ok_or_else(|| anyhow!("failed to build incoming call"))?; - Ok((room, incoming_call)) - }) - .await - } - - pub async fn call_failed( - &self, - room_id: RoomId, - called_user_id: UserId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - room_participant::Entity::delete_many() - .filter( - room_participant::Column::RoomId - .eq(room_id) - .and(room_participant::Column::UserId.eq(called_user_id)), - ) - .exec(&*tx) - .await?; - let room = self.get_room(room_id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn decline_call( - &self, - expected_room_id: Option, - user_id: UserId, - ) -> Result>> { - self.optional_room_transaction(|tx| async move { - let mut filter = Condition::all() - .add(room_participant::Column::UserId.eq(user_id)) - .add(room_participant::Column::AnsweringConnectionId.is_null()); - if let Some(room_id) = expected_room_id { - filter = filter.add(room_participant::Column::RoomId.eq(room_id)); - } - let participant = room_participant::Entity::find() - .filter(filter) - .one(&*tx) - .await?; - - let participant = if let Some(participant) = participant { - participant - } else if expected_room_id.is_some() { - return Err(anyhow!("could not find call to decline"))?; - } else { - return Ok(None); - }; - - let room_id = participant.room_id; - room_participant::Entity::delete(participant.into_active_model()) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok(Some((room_id, room))) - }) - .await - } - - pub async fn cancel_call( - &self, - room_id: RoomId, - calling_connection: ConnectionId, - called_user_id: UserId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add(room_participant::Column::UserId.eq(called_user_id)) - .add(room_participant::Column::RoomId.eq(room_id)) - .add( - room_participant::Column::CallingConnectionId - .eq(calling_connection.id as i32), - ) - .add( - room_participant::Column::CallingConnectionServerId - .eq(calling_connection.owner_id as i32), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no call to cancel"))?; - - room_participant::Entity::delete(participant.into_active_model()) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok(room) - }) - .await - } - - pub async fn is_current_room_different_channel( - &self, - user_id: UserId, - channel_id: ChannelId, - ) -> Result { - self.transaction(|tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - ChannelId, - } - - let channel_id_model: Option = room_participant::Entity::find() - .select_only() - .column_as(room::Column::ChannelId, QueryAs::ChannelId) - .inner_join(room::Entity) - .filter(room_participant::Column::UserId.eq(user_id)) - .into_values::<_, QueryAs>() - .one(&*tx) - .await?; - - let result = channel_id_model - .map(|channel_id_model| channel_id_model != channel_id) - .unwrap_or(false); - - Ok(result) - }) - .await - } - - pub async fn join_room( - &self, - room_id: RoomId, - user_id: UserId, - connection: ConnectionId, - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryChannelId { - ChannelId, - } - let channel_id: Option = room::Entity::find() - .select_only() - .column(room::Column::ChannelId) - .filter(room::Column::Id.eq(room_id)) - .into_values::<_, QueryChannelId>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such room"))?; - - 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), - 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, - ))), - ..Default::default() - }]) - .on_conflict( - OnConflict::columns([room_participant::Column::UserId]) - .update_columns([ - room_participant::Column::AnsweringConnectionId, - room_participant::Column::AnsweringConnectionServerId, - room_participant::Column::AnsweringConnectionLost, - ]) - .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 { - 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"))?; - } - } - - 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? - } else { - Vec::new() - }; - Ok(JoinRoom { - room, - channel_id, - channel_members, - }) - }) - .await - } - - pub async fn rejoin_room( - &self, - rejoin_room: proto::RejoinRoom, - user_id: UserId, - connection: ConnectionId, - ) -> Result> { - let room_id = RoomId::from_proto(rejoin_room.id); - self.room_transaction(room_id, |tx| async { - let tx = tx; - let participant_update = 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_not_null()) - .add( - Condition::any() - .add(room_participant::Column::AnsweringConnectionLost.eq(true)) - .add( - room_participant::Column::AnsweringConnectionServerId - .ne(connection.owner_id as i32), - ), - ), - ) - .set(room_participant::ActiveModel { - 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 participant_update.rows_affected == 0 { - return Err(anyhow!("room does not exist or was already joined"))?; - } - - let mut reshared_projects = Vec::new(); - for reshared_project in &rejoin_room.reshared_projects { - let project_id = ProjectId::from_proto(reshared_project.project_id); - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project does not exist"))?; - if project.host_user_id != user_id { - return Err(anyhow!("no such project"))?; - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let host_ix = collaborators - .iter() - .position(|collaborator| { - collaborator.user_id == user_id && collaborator.is_host - }) - .ok_or_else(|| anyhow!("host not found among collaborators"))?; - let host = collaborators.swap_remove(host_ix); - let old_connection_id = host.connection(); - - project::Entity::update(project::ActiveModel { - host_connection_id: ActiveValue::set(Some(connection.id as i32)), - host_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..project.into_active_model() - }) - .exec(&*tx) - .await?; - project_collaborator::Entity::update(project_collaborator::ActiveModel { - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - ..host.into_active_model() - }) - .exec(&*tx) - .await?; - - self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx) - .await?; - - reshared_projects.push(ResharedProject { - id: project_id, - old_connection_id, - collaborators: collaborators - .iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees: reshared_project.worktrees.clone(), - }); - } - - project::Entity::delete_many() - .filter( - Condition::all() - .add(project::Column::RoomId.eq(room_id)) - .add(project::Column::HostUserId.eq(user_id)) - .add( - project::Column::Id - .is_not_in(reshared_projects.iter().map(|project| project.id)), - ), - ) - .exec(&*tx) - .await?; - - let mut rejoined_projects = Vec::new(); - for rejoined_project in &rejoin_room.rejoined_projects { - let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? else { continue }; - - let mut worktrees = Vec::new(); - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - for db_worktree in db_worktrees { - let mut worktree = RejoinedWorktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - updated_entries: Default::default(), - removed_entries: Default::default(), - updated_repositories: Default::default(), - removed_repositories: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }; - - let rejoined_worktree = rejoined_project - .worktrees - .iter() - .find(|worktree| worktree.id == db_worktree.id as u64); - - // File entries - { - let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { - worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_entry::Column::IsDeleted.eq(false) - }; - - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project.id)) - .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) - .add(entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if db_entry.is_deleted { - worktree.removed_entries.push(db_entry.id as u64); - } else { - worktree.updated_entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Repository Entries - { - let repository_entry_filter = - if let Some(rejoined_worktree) = rejoined_worktree { - worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) - } else { - worktree_repository::Column::IsDeleted.eq(false) - }; - - let mut db_repositories = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project.id)) - .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) - .add(repository_entry_filter), - ) - .stream(&*tx) - .await?; - - while let Some(db_repository) = db_repositories.next().await { - let db_repository = db_repository?; - if db_repository.is_deleted { - worktree - .removed_repositories - .push(db_repository.work_directory_id as u64); - } else { - worktree.updated_repositories.push(proto::RepositoryEntry { - work_directory_id: db_repository.work_directory_id as u64, - branch: db_repository.branch, - }); - } - } - } - - worktrees.push(worktree); - } - - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await? - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect::>(); - - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = worktrees - .iter_mut() - .find(|w| w.id == db_settings_file.worktree_id as u64) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let self_collaborator = if let Some(self_collaborator_ix) = collaborators - .iter() - .position(|collaborator| collaborator.user_id == user_id) - { - collaborators.swap_remove(self_collaborator_ix) - } else { - continue; - }; - let old_connection_id = self_collaborator.connection(); - project_collaborator::Entity::update(project_collaborator::ActiveModel { - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - ..self_collaborator.into_active_model() - }) - .exec(&*tx) - .await?; - - let collaborators = collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect::>(); - - rejoined_projects.push(RejoinedProject { - id: project_id, - old_connection_id, - collaborators, - worktrees, - language_servers, - }); - } - - 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? - } else { - Vec::new() - }; - - Ok(RejoinedRoom { - room, - channel_id, - channel_members, - rejoined_projects, - reshared_projects, - }) - }) - .await - } - - pub async fn leave_room( - &self, - connection: ConnectionId, - ) -> Result>> { - self.optional_room_transaction(|tx| async move { - let leaving_participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await?; - - if let Some(leaving_participant) = leaving_participant { - // Leave room. - let room_id = leaving_participant.room_id; - room_participant::Entity::delete_by_id(leaving_participant.id) - .exec(&*tx) - .await?; - - // Cancel pending calls initiated by the leaving user. - let called_participants = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::CallingUserId - .eq(leaving_participant.user_id), - ) - .add(room_participant::Column::AnsweringConnectionId.is_null()), - ) - .all(&*tx) - .await?; - room_participant::Entity::delete_many() - .filter( - room_participant::Column::Id - .is_in(called_participants.iter().map(|participant| participant.id)), - ) - .exec(&*tx) - .await?; - let canceled_calls_to_user_ids = called_participants - .into_iter() - .map(|participant| participant.user_id) - .collect(); - - // Detect left projects. - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryProjectIds { - ProjectId, - } - let project_ids: Vec = project_collaborator::Entity::find() - .select_only() - .column_as( - project_collaborator::Column::ProjectId, - QueryProjectIds::ProjectId, - ) - .filter( - Condition::all() - .add( - project_collaborator::Column::ConnectionId.eq(connection.id as i32), - ) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .into_values::<_, QueryProjectIds>() - .all(&*tx) - .await?; - let mut left_projects = HashMap::default(); - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.is_in(project_ids)) - .stream(&*tx) - .await?; - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - let left_project = - left_projects - .entry(collaborator.project_id) - .or_insert(LeftProject { - id: collaborator.project_id, - host_user_id: Default::default(), - connection_ids: Default::default(), - host_connection_id: Default::default(), - }); - - let collaborator_connection_id = collaborator.connection(); - if collaborator_connection_id != connection { - left_project.connection_ids.push(collaborator_connection_id); - } - - if collaborator.is_host { - left_project.host_user_id = collaborator.user_id; - left_project.host_connection_id = collaborator_connection_id; - } - } - drop(collaborators); - - // Leave projects. - project_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add( - project_collaborator::Column::ConnectionId.eq(connection.id as i32), - ) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - - // Unshare projects. - project::Entity::delete_many() - .filter( - Condition::all() - .add(project::Column::RoomId.eq(room_id)) - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - - let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; - let deleted = if room.participants.is_empty() { - let result = room::Entity::delete_by_id(room_id) - .filter(room::Column::ChannelId.is_null()) - .exec(&*tx) - .await?; - result.rows_affected > 0 - } else { - false - }; - - let channel_members = if let Some(channel_id) = channel_id { - self.get_channel_members_internal(channel_id, &tx).await? - } else { - Vec::new() - }; - let left_room = LeftRoom { - room, - channel_id, - channel_members, - left_projects, - canceled_calls_to_user_ids, - deleted, - }; - - if left_room.room.participants.is_empty() { - self.rooms.remove(&room_id); - } - - Ok(Some((room_id, left_room))) - } else { - Ok(None) - } - }) - .await - } - - pub async fn follow( - &self, - project_id: ProjectId, - leader_connection: ConnectionId, - follower_connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - follower::ActiveModel { - room_id: ActiveValue::set(room_id), - project_id: ActiveValue::set(project_id), - leader_connection_server_id: ActiveValue::set(ServerId( - leader_connection.owner_id as i32, - )), - leader_connection_id: ActiveValue::set(leader_connection.id as i32), - follower_connection_server_id: ActiveValue::set(ServerId( - follower_connection.owner_id as i32, - )), - follower_connection_id: ActiveValue::set(follower_connection.id as i32), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &*tx).await?; - Ok(room) - }) - .await - } - - pub async fn unfollow( - &self, - project_id: ProjectId, - leader_connection: ConnectionId, - follower_connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - follower::Entity::delete_many() - .filter( - Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) - .add( - follower::Column::LeaderConnectionServerId - .eq(leader_connection.owner_id), - ) - .add(follower::Column::LeaderConnectionId.eq(leader_connection.id)) - .add( - follower::Column::FollowerConnectionServerId - .eq(follower_connection.owner_id), - ) - .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)), - ) - .exec(&*tx) - .await?; - - let room = self.get_room(room_id, &*tx).await?; - Ok(room) - }) - .await - } - - pub async fn update_room_participant_location( - &self, - room_id: RoomId, - connection: ConnectionId, - location: proto::ParticipantLocation, - ) -> Result> { - self.room_transaction(room_id, |tx| async { - let tx = tx; - let location_kind; - let location_project_id; - match location - .variant - .as_ref() - .ok_or_else(|| anyhow!("invalid location"))? - { - proto::participant_location::Variant::SharedProject(project) => { - location_kind = 0; - location_project_id = Some(ProjectId::from_proto(project.id)); - } - proto::participant_location::Variant::UnsharedProject(_) => { - location_kind = 1; - location_project_id = None; - } - proto::participant_location::Variant::External(_) => { - location_kind = 2; - location_project_id = None; - } - } - - let result = room_participant::Entity::update_many() - .filter( - Condition::all() - .add(room_participant::Column::RoomId.eq(room_id)) - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .set(room_participant::ActiveModel { - location_kind: ActiveValue::set(Some(location_kind)), - location_project_id: ActiveValue::set(location_project_id), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if result.rows_affected == 1 { - let room = self.get_room(room_id, &tx).await?; - Ok(room) - } else { - Err(anyhow!("could not update room participant location"))? - } - }) - .await - } - - pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { - self.transaction(|tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("not a participant in any room"))?; - - room_participant::Entity::update(room_participant::ActiveModel { - answering_connection_lost: ActiveValue::set(true), - ..participant.into_active_model() - }) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - fn build_incoming_call( - room: &proto::Room, - called_user_id: UserId, - ) -> Option { - let pending_participant = room - .pending_participants - .iter() - .find(|participant| participant.user_id == called_user_id.to_proto())?; - - Some(proto::IncomingCall { - room_id: room.id, - calling_user_id: pending_participant.calling_user_id, - participant_user_ids: room - .participants - .iter() - .map(|participant| participant.user_id) - .collect(), - initial_project: room.participants.iter().find_map(|participant| { - let initial_project_id = pending_participant.initial_project_id?; - participant - .projects - .iter() - .find(|project| project.id == initial_project_id) - .cloned() - }), - }) - } - async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { - let (_, room) = self.get_channel_room(room_id, tx).await?; - Ok(room) - } - - async fn get_channel_room( - &self, - room_id: RoomId, - tx: &DatabaseTransaction, - ) -> Result<(Option, proto::Room)> { - let db_room = room::Entity::find_by_id(room_id) - .one(tx) - .await? - .ok_or_else(|| anyhow!("could not find room"))?; - - let mut db_participants = db_room - .find_related(room_participant::Entity) - .stream(tx) - .await?; - let mut participants = HashMap::default(); - let mut pending_participants = Vec::new(); - while let Some(db_participant) = db_participants.next().await { - let db_participant = db_participant?; - if let Some((answering_connection_id, answering_connection_server_id)) = db_participant - .answering_connection_id - .zip(db_participant.answering_connection_server_id) - { - let location = match ( - db_participant.location_kind, - db_participant.location_project_id, - ) { - (Some(0), Some(project_id)) => { - Some(proto::participant_location::Variant::SharedProject( - proto::participant_location::SharedProject { - id: project_id.to_proto(), - }, - )) - } - (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject( - Default::default(), - )), - _ => Some(proto::participant_location::Variant::External( - Default::default(), - )), - }; - - let answering_connection = ConnectionId { - owner_id: answering_connection_server_id.0 as u32, - id: answering_connection_id as u32, - }; - participants.insert( - answering_connection, - proto::Participant { - user_id: db_participant.user_id.to_proto(), - peer_id: Some(answering_connection.into()), - projects: Default::default(), - location: Some(proto::ParticipantLocation { variant: location }), - }, - ); - } else { - pending_participants.push(proto::PendingParticipant { - user_id: db_participant.user_id.to_proto(), - calling_user_id: db_participant.calling_user_id.to_proto(), - initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()), - }); - } - } - drop(db_participants); - - let mut db_projects = db_room - .find_related(project::Entity) - .find_with_related(worktree::Entity) - .stream(tx) - .await?; - - while let Some(row) = db_projects.next().await { - let (db_project, db_worktree) = row?; - let host_connection = db_project.host_connection()?; - if let Some(participant) = participants.get_mut(&host_connection) { - let project = if let Some(project) = participant - .projects - .iter_mut() - .find(|project| project.id == db_project.id.to_proto()) - { - project - } else { - participant.projects.push(proto::ParticipantProject { - id: db_project.id.to_proto(), - worktree_root_names: Default::default(), - }); - participant.projects.last_mut().unwrap() - }; - - if let Some(db_worktree) = db_worktree { - if db_worktree.visible { - project.worktree_root_names.push(db_worktree.root_name); - } - } - } - } - drop(db_projects); - - let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; - let mut followers = Vec::new(); - while let Some(db_follower) = db_followers.next().await { - let db_follower = db_follower?; - followers.push(proto::Follower { - leader_id: Some(db_follower.leader_connection().into()), - follower_id: Some(db_follower.follower_connection().into()), - project_id: db_follower.project_id.to_proto(), - }); - } - - Ok(( - db_room.channel_id, - proto::Room { - id: db_room.id.to_proto(), - live_kit_room: db_room.live_kit_room, - participants: participants.into_values().collect(), - pending_participants, - followers, - }, - )) - } - - // projects - - pub async fn project_count_excluding_admins(&self) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryAs { - Count, - } - - self.transaction(|tx| async move { - Ok(project::Entity::find() - .select_only() - .column_as(project::Column::Id.count(), QueryAs::Count) - .inner_join(user::Entity) - .filter(user::Column::Admin.eq(false)) - .into_values::<_, QueryAs>() - .one(&*tx) - .await? - .unwrap_or(0i64) as usize) - }) - .await - } - - pub async fn share_project( - &self, - room_id: RoomId, - connection: ConnectionId, - worktrees: &[proto::WorktreeMetadata], - ) -> Result> { - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("could not find participant"))?; - if participant.room_id != room_id { - return Err(anyhow!("shared project on unexpected room"))?; - } - - let project = project::ActiveModel { - room_id: ActiveValue::set(participant.room_id), - host_user_id: ActiveValue::set(participant.user_id), - host_connection_id: ActiveValue::set(Some(connection.id as i32)), - host_connection_server_id: ActiveValue::set(Some(ServerId( - connection.owner_id as i32, - ))), - ..Default::default() - } - .insert(&*tx) - .await?; - - if !worktrees.is_empty() { - worktree::Entity::insert_many(worktrees.iter().map(|worktree| { - worktree::ActiveModel { - id: ActiveValue::set(worktree.id as i64), - project_id: ActiveValue::set(project.id), - abs_path: ActiveValue::set(worktree.abs_path.clone()), - root_name: ActiveValue::set(worktree.root_name.clone()), - visible: ActiveValue::set(worktree.visible), - scan_id: ActiveValue::set(0), - completed_scan_id: ActiveValue::set(0), - } - })) - .exec(&*tx) - .await?; - } - - project_collaborator::ActiveModel { - project_id: ActiveValue::set(project.id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(ReplicaId(0)), - is_host: ActiveValue::set(true), - ..Default::default() - } - .insert(&*tx) - .await?; - - let room = self.get_room(room_id, &tx).await?; - Ok((project.id, room)) - }) - .await - } - - pub async fn unshare_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result)>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project not found"))?; - if project.host_connection()? == connection { - project::Entity::delete(project.into_active_model()) - .exec(&*tx) - .await?; - let room = self.get_room(room_id, &tx).await?; - Ok((room, guest_connection_ids)) - } else { - Err(anyhow!("cannot unshare a project hosted by another user"))? - } - }) - .await - } - - pub async fn update_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - worktrees: &[proto::WorktreeMetadata], - ) -> Result)>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let project = project::Entity::find_by_id(project_id) - .filter( - Condition::all() - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId.eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - - self.update_project_worktrees(project.id, worktrees, &tx) - .await?; - - let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; - let room = self.get_room(project.room_id, &tx).await?; - Ok((room, guest_connection_ids)) - }) - .await - } - - async fn update_project_worktrees( - &self, - project_id: ProjectId, - worktrees: &[proto::WorktreeMetadata], - tx: &DatabaseTransaction, - ) -> Result<()> { - if !worktrees.is_empty() { - worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel { - id: ActiveValue::set(worktree.id as i64), - project_id: ActiveValue::set(project_id), - abs_path: ActiveValue::set(worktree.abs_path.clone()), - root_name: ActiveValue::set(worktree.root_name.clone()), - visible: ActiveValue::set(worktree.visible), - scan_id: ActiveValue::set(0), - completed_scan_id: ActiveValue::set(0), - })) - .on_conflict( - OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id]) - .update_column(worktree::Column::RootName) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - worktree::Entity::delete_many() - .filter(worktree::Column::ProjectId.eq(project_id).and( - worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)), - )) - .exec(&*tx) - .await?; - - Ok(()) - } - - pub async fn update_worktree( - &self, - update: &proto::UpdateWorktree, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let worktree_id = update.worktree_id as i64; - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - // Ensure the update comes from the host. - let _project = project::Entity::find_by_id(project_id) - .filter( - Condition::all() - .add(project::Column::HostConnectionId.eq(connection.id as i32)) - .add( - project::Column::HostConnectionServerId.eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - - // Update metadata. - worktree::Entity::update(worktree::ActiveModel { - id: ActiveValue::set(worktree_id), - project_id: ActiveValue::set(project_id), - root_name: ActiveValue::set(update.root_name.clone()), - scan_id: ActiveValue::set(update.scan_id as i64), - completed_scan_id: if update.is_last_update { - ActiveValue::set(update.scan_id as i64) - } else { - ActiveValue::default() - }, - abs_path: ActiveValue::set(update.abs_path.clone()), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if !update.updated_entries.is_empty() { - worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| { - let mtime = entry.mtime.clone().unwrap_or_default(); - worktree_entry::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - id: ActiveValue::set(entry.id as i64), - is_dir: ActiveValue::set(entry.is_dir), - path: ActiveValue::set(entry.path.clone()), - inode: ActiveValue::set(entry.inode as i64), - mtime_seconds: ActiveValue::set(mtime.seconds as i64), - mtime_nanos: ActiveValue::set(mtime.nanos as i32), - is_symlink: ActiveValue::set(entry.is_symlink), - is_ignored: ActiveValue::set(entry.is_ignored), - is_external: ActiveValue::set(entry.is_external), - git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), - is_deleted: ActiveValue::set(false), - scan_id: ActiveValue::set(update.scan_id as i64), - } - })) - .on_conflict( - OnConflict::columns([ - worktree_entry::Column::ProjectId, - worktree_entry::Column::WorktreeId, - worktree_entry::Column::Id, - ]) - .update_columns([ - worktree_entry::Column::IsDir, - worktree_entry::Column::Path, - worktree_entry::Column::Inode, - worktree_entry::Column::MtimeSeconds, - worktree_entry::Column::MtimeNanos, - worktree_entry::Column::IsSymlink, - worktree_entry::Column::IsIgnored, - worktree_entry::Column::GitStatus, - worktree_entry::Column::ScanId, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !update.removed_entries.is_empty() { - worktree_entry::Entity::update_many() - .filter( - worktree_entry::Column::ProjectId - .eq(project_id) - .and(worktree_entry::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_entry::Column::Id - .is_in(update.removed_entries.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_entry::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - if !update.updated_repositories.is_empty() { - worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( - |repository| worktree_repository::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - work_directory_id: ActiveValue::set(repository.work_directory_id as i64), - scan_id: ActiveValue::set(update.scan_id as i64), - branch: ActiveValue::set(repository.branch.clone()), - is_deleted: ActiveValue::set(false), - }, - )) - .on_conflict( - OnConflict::columns([ - worktree_repository::Column::ProjectId, - worktree_repository::Column::WorktreeId, - worktree_repository::Column::WorkDirectoryId, - ]) - .update_columns([ - worktree_repository::Column::ScanId, - worktree_repository::Column::Branch, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - } - - if !update.removed_repositories.is_empty() { - worktree_repository::Entity::update_many() - .filter( - worktree_repository::Column::ProjectId - .eq(project_id) - .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) - .and( - worktree_repository::Column::WorkDirectoryId - .is_in(update.removed_repositories.iter().map(|id| *id as i64)), - ), - ) - .set(worktree_repository::ActiveModel { - is_deleted: ActiveValue::Set(true), - scan_id: ActiveValue::Set(update.scan_id as i64), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn update_diagnostic_summary( - &self, - update: &proto::UpdateDiagnosticSummary, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let worktree_id = update.worktree_id as i64; - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let summary = update - .summary - .as_ref() - .ok_or_else(|| anyhow!("invalid summary"))?; - - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - // Update summary. - worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - path: ActiveValue::set(summary.path.clone()), - language_server_id: ActiveValue::set(summary.language_server_id as i64), - error_count: ActiveValue::set(summary.error_count as i32), - warning_count: ActiveValue::set(summary.warning_count as i32), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([ - worktree_diagnostic_summary::Column::ProjectId, - worktree_diagnostic_summary::Column::WorktreeId, - worktree_diagnostic_summary::Column::Path, - ]) - .update_columns([ - worktree_diagnostic_summary::Column::LanguageServerId, - worktree_diagnostic_summary::Column::ErrorCount, - worktree_diagnostic_summary::Column::WarningCount, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn start_language_server( - &self, - update: &proto::StartLanguageServer, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let server = update - .server - .as_ref() - .ok_or_else(|| anyhow!("invalid language server"))?; - - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - // Add the newly-started language server. - language_server::Entity::insert(language_server::ActiveModel { - project_id: ActiveValue::set(project_id), - id: ActiveValue::set(server.id as i64), - name: ActiveValue::set(server.name.clone()), - ..Default::default() - }) - .on_conflict( - OnConflict::columns([ - language_server::Column::ProjectId, - language_server::Column::Id, - ]) - .update_column(language_server::Column::Name) - .to_owned(), - ) - .exec(&*tx) - .await?; - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn update_worktree_settings( - &self, - update: &proto::UpdateWorktreeSettings, - connection: ConnectionId, - ) -> Result>> { - let project_id = ProjectId::from_proto(update.project_id); - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - // Ensure the update comes from the host. - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.host_connection()? != connection { - return Err(anyhow!("can't update a project hosted by someone else"))?; - } - - if let Some(content) = &update.content { - worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { - project_id: ActiveValue::Set(project_id), - worktree_id: ActiveValue::Set(update.worktree_id as i64), - path: ActiveValue::Set(update.path.clone()), - content: ActiveValue::Set(content.clone()), - }) - .on_conflict( - OnConflict::columns([ - worktree_settings_file::Column::ProjectId, - worktree_settings_file::Column::WorktreeId, - worktree_settings_file::Column::Path, - ]) - .update_column(worktree_settings_file::Column::Content) - .to_owned(), - ) - .exec(&*tx) - .await?; - } else { - worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { - project_id: ActiveValue::Set(project_id), - worktree_id: ActiveValue::Set(update.worktree_id as i64), - path: ActiveValue::Set(update.path.clone()), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; - Ok(connection_ids) - }) - .await - } - - pub async fn join_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let participant = room_participant::Entity::find() - .filter( - Condition::all() - .add( - room_participant::Column::AnsweringConnectionId - .eq(connection.id as i32), - ) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("must join a room first"))?; - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - if project.room_id != participant.room_id { - return Err(anyhow!("no such project"))?; - } - - let mut collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let replica_ids = collaborators - .iter() - .map(|c| c.replica_id) - .collect::>(); - let mut replica_id = ReplicaId(1); - while replica_ids.contains(&replica_id) { - replica_id.0 += 1; - } - let new_collaborator = project_collaborator::ActiveModel { - project_id: ActiveValue::set(project_id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(replica_id), - is_host: ActiveValue::set(false), - ..Default::default() - } - .insert(&*tx) - .await?; - collaborators.push(new_collaborator); - - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - let mut worktrees = db_worktrees - .into_iter() - .map(|db_worktree| { - ( - db_worktree.id as u64, - Worktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - entries: Default::default(), - repository_entries: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }, - ) - }) - .collect::>(); - - // Populate worktree entries. - { - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project_id)) - .add(worktree_entry::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { - worktree.entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Populate repository entries. - { - let mut db_repository_entries = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project_id)) - .add(worktree_repository::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_repository_entry) = db_repository_entries.next().await { - let db_repository_entry = db_repository_entry?; - if let Some(worktree) = - worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) - { - worktree.repository_entries.insert( - db_repository_entry.work_directory_id as u64, - proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - }, - ); - } - } - } - - // Populate worktree diagnostic summaries. - { - let mut db_summaries = worktree_diagnostic_summary::Entity::find() - .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_summary) = db_summaries.next().await { - let db_summary = db_summary?; - if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { - worktree - .diagnostic_summaries - .push(proto::DiagnosticSummary { - path: db_summary.path, - language_server_id: db_summary.language_server_id as u64, - error_count: db_summary.error_count as u32, - warning_count: db_summary.warning_count as u32, - }); - } - } - } - - // Populate worktree settings files - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = - worktrees.get_mut(&(db_settings_file.worktree_id as u64)) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - // Populate language servers. - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await?; - - let project = Project { - collaborators: collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees, - language_servers: language_servers - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect(), - }; - Ok((project, replica_id as ReplicaId)) - }) - .await - } - - pub async fn leave_project( - &self, - project_id: ProjectId, - connection: ConnectionId, - ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let result = project_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add(project_collaborator::Column::ProjectId.eq(project_id)) - .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) - .add( - project_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("not a collaborator on this project"))?; - } - - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such project"))?; - let collaborators = project - .find_related(project_collaborator::Entity) - .all(&*tx) - .await?; - let connection_ids = collaborators - .into_iter() - .map(|collaborator| collaborator.connection()) - .collect(); - - follower::Entity::delete_many() - .filter( - Condition::any() - .add( - Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) - .add( - follower::Column::LeaderConnectionServerId - .eq(connection.owner_id), - ) - .add(follower::Column::LeaderConnectionId.eq(connection.id)), - ) - .add( - Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) - .add( - follower::Column::FollowerConnectionServerId - .eq(connection.owner_id), - ) - .add(follower::Column::FollowerConnectionId.eq(connection.id)), - ), - ) - .exec(&*tx) - .await?; - - let room = self.get_room(project.room_id, &tx).await?; - let left_project = LeftProject { - id: project_id, - host_user_id: project.host_user_id, - host_connection_id: project.host_connection()?, - connection_ids, - }; - Ok((room, left_project)) - }) - .await - } - - pub async fn project_collaborators( - &self, - project_id: ProjectId, - connection_id: ConnectionId, - ) -> Result>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .all(&*tx) - .await? - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect::>(); - - if collaborators - .iter() - .any(|collaborator| collaborator.connection_id == connection_id) - { - Ok(collaborators) - } else { - Err(anyhow!("no such project"))? - } - }) - .await - } - - pub async fn project_connection_ids( - &self, - project_id: ProjectId, - connection_id: ConnectionId, - ) -> Result>> { - let room_id = self.room_id_for_project(project_id).await?; - self.room_transaction(room_id, |tx| async move { - let mut collaborators = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - - let mut connection_ids = HashSet::default(); - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - connection_ids.insert(collaborator.connection()); - } - - if connection_ids.contains(&connection_id) { - Ok(connection_ids) - } else { - Err(anyhow!("no such project"))? - } - }) - .await - } - - async fn project_guest_connection_ids( - &self, - project_id: ProjectId, - tx: &DatabaseTransaction, - ) -> Result> { - let mut collaborators = project_collaborator::Entity::find() - .filter( - project_collaborator::Column::ProjectId - .eq(project_id) - .and(project_collaborator::Column::IsHost.eq(false)), - ) - .stream(tx) - .await?; - - let mut guest_connection_ids = Vec::new(); - while let Some(collaborator) = collaborators.next().await { - let collaborator = collaborator?; - guest_connection_ids.push(collaborator.connection()); - } - Ok(guest_connection_ids) - } - - async fn room_id_for_project(&self, project_id: ProjectId) -> Result { - self.transaction(|tx| async move { - let project = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("project {} not found", project_id))?; - Ok(project.room_id) - }) - .await - } - - // access tokens - - pub async fn create_access_token( - &self, - user_id: UserId, - access_token_hash: &str, - max_access_token_count: usize, - ) -> Result { - self.transaction(|tx| async { - let tx = tx; - - let token = access_token::ActiveModel { - user_id: ActiveValue::set(user_id), - hash: ActiveValue::set(access_token_hash.into()), - ..Default::default() - } - .insert(&*tx) - .await?; - - access_token::Entity::delete_many() - .filter( - access_token::Column::Id.in_subquery( - Query::select() - .column(access_token::Column::Id) - .from(access_token::Entity) - .and_where(access_token::Column::UserId.eq(user_id)) - .order_by(access_token::Column::Id, sea_orm::Order::Desc) - .limit(10000) - .offset(max_access_token_count as u64) - .to_owned(), - ), - ) - .exec(&*tx) - .await?; - Ok(token.id) - }) - .await - } - - pub async fn get_access_token( - &self, - access_token_id: AccessTokenId, - ) -> Result { - self.transaction(|tx| async move { - Ok(access_token::Entity::find_by_id(access_token_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such access token"))?) - }) - .await - } - - // channels - - pub async fn create_root_channel( - &self, - name: &str, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - self.create_channel(name, None, live_kit_room, creator_id) - .await - } - - pub async fn create_channel( - &self, - name: &str, - parent: Option, - live_kit_room: &str, - creator_id: UserId, - ) -> Result { - let name = Self::sanitize_channel_name(name)?; - self.transaction(move |tx| async move { - if let Some(parent) = parent { - self.check_user_is_channel_admin(parent, creator_id, &*tx) - .await?; - } - - let channel = channel::ActiveModel { - name: ActiveValue::Set(name.to_string()), - ..Default::default() - } - .insert(&*tx) - .await?; - - let channel_paths_stmt; - if let Some(parent) = parent { - let sql = r#" - INSERT INTO channel_paths - (id_path, channel_id) - SELECT - id_path || $1 || '/', $2 - FROM - channel_paths - WHERE - channel_id = $3 - "#; - channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [ - channel.id.to_proto().into(), - channel.id.to_proto().into(), - parent.to_proto().into(), - ], - ); - tx.execute(channel_paths_stmt).await?; - } else { - channel_path::Entity::insert(channel_path::ActiveModel { - channel_id: ActiveValue::Set(channel.id), - id_path: ActiveValue::Set(format!("/{}/", channel.id)), - }) - .exec(&*tx) - .await?; - } - - channel_member::ActiveModel { - channel_id: ActiveValue::Set(channel.id), - user_id: ActiveValue::Set(creator_id), - accepted: ActiveValue::Set(true), - admin: ActiveValue::Set(true), - ..Default::default() - } - .insert(&*tx) - .await?; - - room::ActiveModel { - channel_id: ActiveValue::Set(Some(channel.id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), - ..Default::default() - } - .insert(&*tx) - .await?; - - Ok(channel.id) - }) - .await - } - - pub async fn remove_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result<(Vec, Vec)> { - self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) - .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_keep = channel_path::Entity::find() - .filter( - channel_path::Column::ChannelId - .is_in( - channels_to_remove - .keys() - .copied() - .filter(|&id| id != channel_id), - ) - .and( - channel_path::Column::IdPath - .not_like(&format!("%/{}/%", channel_id)), - ), - ) - .stream(&*tx) - .await?; - while let Some(row) = channels_to_keep.next().await { - let row = row?; - channels_to_remove.remove(&row.channel_id); - } - } - - let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; - let members_to_notify: Vec = channel_member::Entity::find() - .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) - .select_only() - .column(channel_member::Column::UserId) - .distinct() - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; - - channel::Entity::delete_many() - .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) - .exec(&*tx) - .await?; - - Ok((channels_to_remove.into_keys().collect(), members_to_notify)) - }) - .await - } - - pub async fn invite_channel_member( - &self, - channel_id: ChannelId, - invitee_id: UserId, - inviter_id: UserId, - is_admin: bool, - ) -> Result<()> { - self.transaction(move |tx| async move { - self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) - .await?; - - channel_member::ActiveModel { - channel_id: ActiveValue::Set(channel_id), - user_id: ActiveValue::Set(invitee_id), - accepted: ActiveValue::Set(false), - admin: ActiveValue::Set(is_admin), - ..Default::default() - } - .insert(&*tx) - .await?; - - Ok(()) - }) - .await - } - - fn sanitize_channel_name(name: &str) -> Result<&str> { - let new_name = name.trim().trim_start_matches('#'); - if new_name == "" { - Err(anyhow!("channel name can't be blank"))?; - } - Ok(new_name) - } - - pub async fn rename_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - new_name: &str, - ) -> 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 { - id: ActiveValue::Unchanged(channel_id), - name: ActiveValue::Set(new_name.clone()), - ..Default::default() - } - .update(&*tx) - .await?; - - Ok(new_name) - }) - .await - } - - pub async fn respond_to_channel_invite( - &self, - channel_id: ChannelId, - user_id: UserId, - accept: bool, - ) -> Result<()> { - self.transaction(move |tx| async move { - let rows_affected = if accept { - channel_member::Entity::update_many() - .set(channel_member::ActiveModel { - accepted: ActiveValue::Set(accept), - ..Default::default() - }) - .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 - } else { - channel_member::ActiveModel { - channel_id: ActiveValue::Unchanged(channel_id), - user_id: ActiveValue::Unchanged(user_id), - ..Default::default() - } - .delete(&*tx) - .await? - .rows_affected - }; - - if rows_affected == 0 { - Err(anyhow!("no such invitation"))?; - } - - Ok(()) - }) - .await - } - - pub async fn remove_channel_member( - &self, - channel_id: ChannelId, - member_id: UserId, - remover_id: UserId, - ) -> Result<()> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, remover_id, &*tx) - .await?; - - let result = channel_member::Entity::delete_many() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(member_id)), - ) - .exec(&*tx) - .await?; - - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } - - Ok(()) - }) - .await - } - - pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { - self.transaction(|tx| async move { - let channel_invites = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(false)), - ) - .all(&*tx) - .await?; - - let channels = channel::Entity::find() - .filter( - channel::Column::Id.is_in( - channel_invites - .into_iter() - .map(|channel_member| channel_member.channel_id), - ), - ) - .all(&*tx) - .await?; - - let channels = channels - .into_iter() - .map(|channel| Channel { - id: channel.id, - name: channel.name, - parent_id: None, - }) - .collect(); - - Ok(channels) - }) - .await - } - - pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { - self.transaction(|tx| async move { - let tx = tx; - - let channel_memberships = channel_member::Entity::find() - .filter( - channel_member::Column::UserId - .eq(user_id) - .and(channel_member::Column::Accepted.eq(true)), - ) - .all(&*tx) - .await?; - - let parents_by_child_id = self - .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) - .await?; - - let channels_with_admin_privileges = channel_memberships - .iter() - .filter_map(|membership| membership.admin.then_some(membership.channel_id)) - .collect(); - - 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, - parent_id: parents_by_child_id.get(&row.id).copied().flatten(), - }); - } - } - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryUserIdsAndChannelIds { - ChannelId, - UserId, - } - - let mut channel_participants: HashMap> = HashMap::default(); - { - let mut rows = room_participant::Entity::find() - .inner_join(room::Entity) - .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) - .select_only() - .column(room::Column::ChannelId) - .column(room_participant::Column::UserId) - .into_values::<_, QueryUserIdsAndChannelIds>() - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row: (ChannelId, UserId) = row?; - channel_participants.entry(row.0).or_default().push(row.1) - } - } - - Ok(ChannelsForUser { - channels, - channel_participants, - channels_with_admin_privileges, - }) - }) - .await - } - - pub async fn get_channel_members(&self, id: ChannelId) -> Result> { - self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await }) - .await - } - - pub async fn set_channel_member_admin( - &self, - channel_id: ChannelId, - from: UserId, - for_user: UserId, - admin: bool, - ) -> Result<()> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, from, &*tx) - .await?; - - let result = channel_member::Entity::update_many() - .filter( - channel_member::Column::ChannelId - .eq(channel_id) - .and(channel_member::Column::UserId.eq(for_user)), - ) - .set(channel_member::ActiveModel { - admin: ActiveValue::set(admin), - ..Default::default() - }) - .exec(&*tx) - .await?; - - if result.rows_affected == 0 { - Err(anyhow!("no such member"))?; - } - - Ok(()) - }) - .await - } - - pub async fn get_channel_member_details( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - self.check_user_is_channel_admin(channel_id, user_id, &*tx) - .await?; - - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryMemberDetails { - UserId, - Admin, - IsDirectMember, - Accepted, - } - - let tx = tx; - let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; - let mut stream = channel_member::Entity::find() - .distinct() - .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) - .select_only() - .column(channel_member::Column::UserId) - .column(channel_member::Column::Admin) - .column_as( - channel_member::Column::ChannelId.eq(channel_id), - QueryMemberDetails::IsDirectMember, - ) - .column(channel_member::Column::Accepted) - .order_by_asc(channel_member::Column::UserId) - .into_values::<_, QueryMemberDetails>() - .stream(&*tx) - .await?; - - let mut rows = Vec::::new(); - while let Some(row) = stream.next().await { - let (user_id, is_admin, is_direct_member, is_invite_accepted): ( - UserId, - bool, - bool, - bool, - ) = row?; - let kind = match (is_direct_member, is_invite_accepted) { - (true, true) => proto::channel_member::Kind::Member, - (true, false) => proto::channel_member::Kind::Invitee, - (false, true) => proto::channel_member::Kind::AncestorMember, - (false, false) => continue, - }; - 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 = is_admin; - } - continue; - } - } - rows.push(proto::ChannelMember { - user_id, - kind, - admin: is_admin, - }); - } - - Ok(rows) - }) - .await - } - - pub async fn get_channel_members_internal( - &self, - id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let ancestor_ids = self.get_channel_ancestors(id, tx).await?; - let user_ids = channel_member::Entity::find() - .distinct() - .filter( - channel_member::Column::ChannelId - .is_in(ancestor_ids.iter().copied()) - .and(channel_member::Column::Accepted.eq(true)), - ) - .select_only() - .column(channel_member::Column::UserId) - .into_values::<_, QueryUserIds>() - .all(&*tx) - .await?; - Ok(user_ids) - } - - 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(()) - } - - async fn check_user_is_channel_admin( - &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)) - .and(channel_member::Column::Admin.eq(true)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; - Ok(()) - } - - async fn get_channel_ancestors( - &self, - channel_id: ChannelId, - tx: &DatabaseTransaction, - ) -> Result> { - let paths = channel_path::Entity::find() - .filter(channel_path::Column::ChannelId.eq(channel_id)) - .all(tx) - .await?; - let mut channel_ids = Vec::new(); - for path in paths { - for id in path.id_path.trim_matches('/').split('/') { - if let Ok(id) = id.parse() { - let id = ChannelId::from_proto(id); - if let Err(ix) = channel_ids.binary_search(&id) { - channel_ids.insert(ix, id); - } - } - } - } - Ok(channel_ids) - } - - 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 = 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); - } - } - parents_by_child_id.insert(path.channel_id, 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 - pub async fn get_channel( - &self, - channel_id: ChannelId, - user_id: UserId, - ) -> Result> { - self.transaction(|tx| async move { - let tx = tx; - - let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; - - 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, - name: channel.name, - parent_id: None, - }, - is_accepted, - ))) - } else { - Ok(None) - } - }) - .await - } - - pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { - self.transaction(|tx| async move { - let tx = tx; - let room = channel::Model { - id: channel_id, - ..Default::default() - } - .find_related(room::Entity) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid channel"))?; - Ok(room.id) - }) - .await - } - async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, @@ -4157,14 +430,6 @@ pub struct ChannelsForUser { pub channels_with_admin_privileges: HashSet, } -fn random_invite_code() -> String { - nanoid::nanoid!(16) -} - -fn random_email_confirmation_code() -> String { - nanoid::nanoid!(64) -} - #[derive(Clone)] pub struct JoinRoom { pub room: proto::Room, @@ -4277,8 +542,3 @@ pub struct WorktreeSettingsFile { pub path: String, pub content: String, } - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -enum QueryUserIds { - UserId, -} diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs new file mode 100644 index 0000000000..f67bde30b8 --- /dev/null +++ b/crates/collab/src/db/queries.rs @@ -0,0 +1,10 @@ +use super::*; + +pub mod access_tokens; +pub mod channels; +pub mod contacts; +pub mod projects; +pub mod rooms; +pub mod servers; +pub mod signups; +pub mod users; diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs new file mode 100644 index 0000000000..def9428a2b --- /dev/null +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -0,0 +1,53 @@ +use super::*; + +impl Database { + pub async fn create_access_token( + &self, + user_id: UserId, + access_token_hash: &str, + max_access_token_count: usize, + ) -> Result { + self.transaction(|tx| async { + let tx = tx; + + let token = access_token::ActiveModel { + user_id: ActiveValue::set(user_id), + hash: ActiveValue::set(access_token_hash.into()), + ..Default::default() + } + .insert(&*tx) + .await?; + + access_token::Entity::delete_many() + .filter( + access_token::Column::Id.in_subquery( + Query::select() + .column(access_token::Column::Id) + .from(access_token::Entity) + .and_where(access_token::Column::UserId.eq(user_id)) + .order_by(access_token::Column::Id, sea_orm::Order::Desc) + .limit(10000) + .offset(max_access_token_count as u64) + .to_owned(), + ), + ) + .exec(&*tx) + .await?; + Ok(token.id) + }) + .await + } + + pub async fn get_access_token( + &self, + access_token_id: AccessTokenId, + ) -> Result { + self.transaction(|tx| async move { + Ok(access_token::Entity::find_by_id(access_token_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such access token"))?) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs new file mode 100644 index 0000000000..e3d3643a61 --- /dev/null +++ b/crates/collab/src/db/queries/channels.rs @@ -0,0 +1,697 @@ +use super::*; + +impl Database { + pub async fn create_root_channel( + &self, + name: &str, + live_kit_room: &str, + creator_id: UserId, + ) -> Result { + self.create_channel(name, None, live_kit_room, creator_id) + .await + } + + pub async fn create_channel( + &self, + name: &str, + parent: Option, + live_kit_room: &str, + creator_id: UserId, + ) -> Result { + let name = Self::sanitize_channel_name(name)?; + self.transaction(move |tx| async move { + if let Some(parent) = parent { + self.check_user_is_channel_admin(parent, creator_id, &*tx) + .await?; + } + + let channel = channel::ActiveModel { + name: ActiveValue::Set(name.to_string()), + ..Default::default() + } + .insert(&*tx) + .await?; + + let channel_paths_stmt; + if let Some(parent) = parent { + let sql = r#" + INSERT INTO channel_paths + (id_path, channel_id) + SELECT + id_path || $1 || '/', $2 + FROM + channel_paths + WHERE + channel_id = $3 + "#; + channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [ + channel.id.to_proto().into(), + channel.id.to_proto().into(), + parent.to_proto().into(), + ], + ); + tx.execute(channel_paths_stmt).await?; + } else { + channel_path::Entity::insert(channel_path::ActiveModel { + channel_id: ActiveValue::Set(channel.id), + id_path: ActiveValue::Set(format!("/{}/", channel.id)), + }) + .exec(&*tx) + .await?; + } + + channel_member::ActiveModel { + channel_id: ActiveValue::Set(channel.id), + user_id: ActiveValue::Set(creator_id), + accepted: ActiveValue::Set(true), + admin: ActiveValue::Set(true), + ..Default::default() + } + .insert(&*tx) + .await?; + + room::ActiveModel { + channel_id: ActiveValue::Set(Some(channel.id)), + live_kit_room: ActiveValue::Set(live_kit_room.to_string()), + ..Default::default() + } + .insert(&*tx) + .await?; + + Ok(channel.id) + }) + .await + } + + pub async fn remove_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result<(Vec, Vec)> { + self.transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .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_keep = channel_path::Entity::find() + .filter( + channel_path::Column::ChannelId + .is_in( + channels_to_remove + .keys() + .copied() + .filter(|&id| id != channel_id), + ) + .and( + channel_path::Column::IdPath + .not_like(&format!("%/{}/%", channel_id)), + ), + ) + .stream(&*tx) + .await?; + while let Some(row) = channels_to_keep.next().await { + let row = row?; + channels_to_remove.remove(&row.channel_id); + } + } + + let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?; + let members_to_notify: Vec = channel_member::Entity::find() + .filter(channel_member::Column::ChannelId.is_in(channel_ancestors)) + .select_only() + .column(channel_member::Column::UserId) + .distinct() + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + channel::Entity::delete_many() + .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied())) + .exec(&*tx) + .await?; + + Ok((channels_to_remove.into_keys().collect(), members_to_notify)) + }) + .await + } + + pub async fn invite_channel_member( + &self, + channel_id: ChannelId, + invitee_id: UserId, + inviter_id: UserId, + is_admin: bool, + ) -> Result<()> { + self.transaction(move |tx| async move { + self.check_user_is_channel_admin(channel_id, inviter_id, &*tx) + .await?; + + channel_member::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + user_id: ActiveValue::Set(invitee_id), + accepted: ActiveValue::Set(false), + admin: ActiveValue::Set(is_admin), + ..Default::default() + } + .insert(&*tx) + .await?; + + Ok(()) + }) + .await + } + + fn sanitize_channel_name(name: &str) -> Result<&str> { + let new_name = name.trim().trim_start_matches('#'); + if new_name == "" { + Err(anyhow!("channel name can't be blank"))?; + } + Ok(new_name) + } + + pub async fn rename_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + new_name: &str, + ) -> 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 { + id: ActiveValue::Unchanged(channel_id), + name: ActiveValue::Set(new_name.clone()), + ..Default::default() + } + .update(&*tx) + .await?; + + Ok(new_name) + }) + .await + } + + pub async fn respond_to_channel_invite( + &self, + channel_id: ChannelId, + user_id: UserId, + accept: bool, + ) -> Result<()> { + self.transaction(move |tx| async move { + let rows_affected = if accept { + channel_member::Entity::update_many() + .set(channel_member::ActiveModel { + accepted: ActiveValue::Set(accept), + ..Default::default() + }) + .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 + } else { + channel_member::ActiveModel { + channel_id: ActiveValue::Unchanged(channel_id), + user_id: ActiveValue::Unchanged(user_id), + ..Default::default() + } + .delete(&*tx) + .await? + .rows_affected + }; + + if rows_affected == 0 { + Err(anyhow!("no such invitation"))?; + } + + Ok(()) + }) + .await + } + + pub async fn remove_channel_member( + &self, + channel_id: ChannelId, + member_id: UserId, + remover_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, remover_id, &*tx) + .await?; + + let result = channel_member::Entity::delete_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(member_id)), + ) + .exec(&*tx) + .await?; + + if result.rows_affected == 0 { + Err(anyhow!("no such member"))?; + } + + Ok(()) + }) + .await + } + + pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { + self.transaction(|tx| async move { + let channel_invites = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(false)), + ) + .all(&*tx) + .await?; + + let channels = channel::Entity::find() + .filter( + channel::Column::Id.is_in( + channel_invites + .into_iter() + .map(|channel_member| channel_member.channel_id), + ), + ) + .all(&*tx) + .await?; + + let channels = channels + .into_iter() + .map(|channel| Channel { + id: channel.id, + name: channel.name, + parent_id: None, + }) + .collect(); + + Ok(channels) + }) + .await + } + + pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { + self.transaction(|tx| async move { + let tx = tx; + + let channel_memberships = channel_member::Entity::find() + .filter( + channel_member::Column::UserId + .eq(user_id) + .and(channel_member::Column::Accepted.eq(true)), + ) + .all(&*tx) + .await?; + + let parents_by_child_id = self + .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx) + .await?; + + let channels_with_admin_privileges = channel_memberships + .iter() + .filter_map(|membership| membership.admin.then_some(membership.channel_id)) + .collect(); + + 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, + parent_id: parents_by_child_id.get(&row.id).copied().flatten(), + }); + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryUserIdsAndChannelIds { + ChannelId, + UserId, + } + + let mut channel_participants: HashMap> = HashMap::default(); + { + let mut rows = room_participant::Entity::find() + .inner_join(room::Entity) + .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id))) + .select_only() + .column(room::Column::ChannelId) + .column(room_participant::Column::UserId) + .into_values::<_, QueryUserIdsAndChannelIds>() + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row: (ChannelId, UserId) = row?; + channel_participants.entry(row.0).or_default().push(row.1) + } + } + + Ok(ChannelsForUser { + channels, + channel_participants, + channels_with_admin_privileges, + }) + }) + .await + } + + pub async fn get_channel_members(&self, id: ChannelId) -> Result> { + self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await }) + .await + } + + pub async fn set_channel_member_admin( + &self, + channel_id: ChannelId, + from: UserId, + for_user: UserId, + admin: bool, + ) -> Result<()> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, from, &*tx) + .await?; + + let result = channel_member::Entity::update_many() + .filter( + channel_member::Column::ChannelId + .eq(channel_id) + .and(channel_member::Column::UserId.eq(for_user)), + ) + .set(channel_member::ActiveModel { + admin: ActiveValue::set(admin), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected == 0 { + Err(anyhow!("no such member"))?; + } + + Ok(()) + }) + .await + } + + pub async fn get_channel_member_details( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + self.check_user_is_channel_admin(channel_id, user_id, &*tx) + .await?; + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryMemberDetails { + UserId, + Admin, + IsDirectMember, + Accepted, + } + + let tx = tx; + let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?; + let mut stream = channel_member::Entity::find() + .distinct() + .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied())) + .select_only() + .column(channel_member::Column::UserId) + .column(channel_member::Column::Admin) + .column_as( + channel_member::Column::ChannelId.eq(channel_id), + QueryMemberDetails::IsDirectMember, + ) + .column(channel_member::Column::Accepted) + .order_by_asc(channel_member::Column::UserId) + .into_values::<_, QueryMemberDetails>() + .stream(&*tx) + .await?; + + let mut rows = Vec::::new(); + while let Some(row) = stream.next().await { + let (user_id, is_admin, is_direct_member, is_invite_accepted): ( + UserId, + bool, + bool, + bool, + ) = row?; + let kind = match (is_direct_member, is_invite_accepted) { + (true, true) => proto::channel_member::Kind::Member, + (true, false) => proto::channel_member::Kind::Invitee, + (false, true) => proto::channel_member::Kind::AncestorMember, + (false, false) => continue, + }; + 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 = is_admin; + } + continue; + } + } + rows.push(proto::ChannelMember { + user_id, + kind, + admin: is_admin, + }); + } + + Ok(rows) + }) + .await + } + + pub async fn get_channel_members_internal( + &self, + id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let ancestor_ids = self.get_channel_ancestors(id, tx).await?; + let user_ids = channel_member::Entity::find() + .distinct() + .filter( + channel_member::Column::ChannelId + .is_in(ancestor_ids.iter().copied()) + .and(channel_member::Column::Accepted.eq(true)), + ) + .select_only() + .column(channel_member::Column::UserId) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + 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<()> { + 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)) + .and(channel_member::Column::Admin.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?; + Ok(()) + } + + pub async fn get_channel_ancestors( + &self, + channel_id: ChannelId, + tx: &DatabaseTransaction, + ) -> Result> { + let paths = channel_path::Entity::find() + .filter(channel_path::Column::ChannelId.eq(channel_id)) + .all(tx) + .await?; + let mut channel_ids = Vec::new(); + for path in paths { + for id in path.id_path.trim_matches('/').split('/') { + if let Ok(id) = id.parse() { + let id = ChannelId::from_proto(id); + if let Err(ix) = channel_ids.binary_search(&id) { + channel_ids.insert(ix, id); + } + } + } + } + Ok(channel_ids) + } + + 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 = 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); + } + } + parents_by_child_id.insert(path.channel_id, 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 + pub async fn get_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + let tx = tx; + + let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?; + + 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, + name: channel.name, + parent_id: None, + }, + is_accepted, + ))) + } else { + Ok(None) + } + }) + .await + } + + pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result { + self.transaction(|tx| async move { + let tx = tx; + let room = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(room::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid channel"))?; + Ok(room.id) + }) + .await + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +enum QueryUserIds { + UserId, +} diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs new file mode 100644 index 0000000000..a18958f035 --- /dev/null +++ b/crates/collab/src/db/queries/contacts.rs @@ -0,0 +1,298 @@ +use super::*; + +impl Database { + pub async fn get_contacts(&self, user_id: UserId) -> Result> { + #[derive(Debug, FromQueryResult)] + struct ContactWithUserBusyStatuses { + user_id_a: UserId, + user_id_b: UserId, + a_to_b: bool, + accepted: bool, + should_notify: bool, + user_a_busy: bool, + user_b_busy: bool, + } + + self.transaction(|tx| async move { + let user_a_participant = Alias::new("user_a_participant"); + let user_b_participant = Alias::new("user_b_participant"); + let mut db_contacts = contact::Entity::find() + .column_as( + Expr::tbl(user_a_participant.clone(), room_participant::Column::Id) + .is_not_null(), + "user_a_busy", + ) + .column_as( + Expr::tbl(user_b_participant.clone(), room_participant::Column::Id) + .is_not_null(), + "user_b_busy", + ) + .filter( + contact::Column::UserIdA + .eq(user_id) + .or(contact::Column::UserIdB.eq(user_id)), + ) + .join_as( + JoinType::LeftJoin, + contact::Relation::UserARoomParticipant.def(), + user_a_participant, + ) + .join_as( + JoinType::LeftJoin, + contact::Relation::UserBRoomParticipant.def(), + user_b_participant, + ) + .into_model::() + .stream(&*tx) + .await?; + + let mut contacts = Vec::new(); + while let Some(db_contact) = db_contacts.next().await { + let db_contact = db_contact?; + if db_contact.user_id_a == user_id { + 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 { + contacts.push(Contact::Outgoing { + user_id: db_contact.user_id_b, + }) + } 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 { + user_id: db_contact.user_id_a, + }); + } + } + + contacts.sort_unstable_by_key(|contact| contact.user_id()); + + Ok(contacts) + }) + .await + } + + pub async fn is_user_busy(&self, user_id: UserId) -> Result { + self.transaction(|tx| async move { + let participant = room_participant::Entity::find() + .filter(room_participant::Column::UserId.eq(user_id)) + .one(&*tx) + .await?; + Ok(participant.is_some()) + }) + .await + } + + pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { + self.transaction(|tx| async move { + let (id_a, id_b) = if user_id_1 < user_id_2 { + (user_id_1, user_id_2) + } else { + (user_id_2, user_id_1) + }; + + Ok(contact::Entity::find() + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and(contact::Column::Accepted.eq(true)), + ) + .one(&*tx) + .await? + .is_some()) + }) + .await + } + + 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) + } else { + (receiver_id, sender_id, false) + }; + + let rows_affected = contact::Entity::insert(contact::ActiveModel { + user_id_a: ActiveValue::set(id_a), + user_id_b: ActiveValue::set(id_b), + a_to_b: ActiveValue::set(a_to_b), + accepted: ActiveValue::set(false), + should_notify: ActiveValue::set(true), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB]) + .values([ + (contact::Column::Accepted, true.into()), + (contact::Column::ShouldNotify, false.into()), + ]) + .action_and_where( + contact::Column::Accepted.eq(false).and( + contact::Column::AToB + .eq(a_to_b) + .and(contact::Column::UserIdA.eq(id_b)) + .or(contact::Column::AToB + .ne(a_to_b) + .and(contact::Column::UserIdA.eq(id_a))), + ), + ) + .to_owned(), + ) + .exec_without_returning(&*tx) + .await?; + + if rows_affected == 1 { + Ok(()) + } else { + Err(anyhow!("contact already requested"))? + } + }) + .await + } + + /// Returns a bool indicating whether the removed contact had originally accepted or not + /// + /// Deletes the contact identified by the requester and responder ids, and then returns + /// whether the deleted contact had originally accepted or was a pending contact request. + /// + /// # Arguments + /// + /// * `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 { + self.transaction(|tx| async move { + let (id_a, id_b) = if responder_id < requester_id { + (responder_id, requester_id) + } else { + (requester_id, responder_id) + }; + + let contact = contact::Entity::find() + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such contact"))?; + + contact::Entity::delete_by_id(contact.id).exec(&*tx).await?; + Ok(contact.accepted) + }) + .await + } + + pub async fn dismiss_contact_notification( + &self, + user_id: UserId, + contact_user_id: UserId, + ) -> Result<()> { + self.transaction(|tx| async move { + let (id_a, id_b, a_to_b) = if user_id < contact_user_id { + (user_id, contact_user_id, true) + } else { + (contact_user_id, user_id, false) + }; + + let result = contact::Entity::update_many() + .set(contact::ActiveModel { + should_notify: ActiveValue::set(false), + ..Default::default() + }) + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and( + contact::Column::AToB + .eq(a_to_b) + .and(contact::Column::Accepted.eq(true)) + .or(contact::Column::AToB + .ne(a_to_b) + .and(contact::Column::Accepted.eq(false))), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("no such contact request"))? + } else { + Ok(()) + } + }) + .await + } + + pub async fn respond_to_contact_request( + &self, + responder_id: UserId, + requester_id: UserId, + accept: bool, + ) -> Result<()> { + self.transaction(|tx| async move { + let (id_a, id_b, a_to_b) = if responder_id < requester_id { + (responder_id, requester_id, false) + } else { + (requester_id, responder_id, true) + }; + let rows_affected = if accept { + let result = contact::Entity::update_many() + .set(contact::ActiveModel { + accepted: ActiveValue::set(true), + should_notify: ActiveValue::set(true), + ..Default::default() + }) + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and(contact::Column::AToB.eq(a_to_b)), + ) + .exec(&*tx) + .await?; + result.rows_affected + } else { + let result = contact::Entity::delete_many() + .filter( + contact::Column::UserIdA + .eq(id_a) + .and(contact::Column::UserIdB.eq(id_b)) + .and(contact::Column::AToB.eq(a_to_b)) + .and(contact::Column::Accepted.eq(false)), + ) + .exec(&*tx) + .await?; + + result.rows_affected + }; + + if rows_affected == 1 { + Ok(()) + } else { + Err(anyhow!("no such contact request"))? + } + }) + .await + } +} diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs new file mode 100644 index 0000000000..31c7cdae3e --- /dev/null +++ b/crates/collab/src/db/queries/projects.rs @@ -0,0 +1,926 @@ +use super::*; + +impl Database { + pub async fn project_count_excluding_admins(&self) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Count, + } + + self.transaction(|tx| async move { + Ok(project::Entity::find() + .select_only() + .column_as(project::Column::Id.count(), QueryAs::Count) + .inner_join(user::Entity) + .filter(user::Column::Admin.eq(false)) + .into_values::<_, QueryAs>() + .one(&*tx) + .await? + .unwrap_or(0i64) as usize) + }) + .await + } + + pub async fn share_project( + &self, + room_id: RoomId, + connection: ConnectionId, + worktrees: &[proto::WorktreeMetadata], + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("could not find participant"))?; + if participant.room_id != room_id { + return Err(anyhow!("shared project on unexpected room"))?; + } + + let project = project::ActiveModel { + room_id: ActiveValue::set(participant.room_id), + host_user_id: ActiveValue::set(participant.user_id), + host_connection_id: ActiveValue::set(Some(connection.id as i32)), + host_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + ..Default::default() + } + .insert(&*tx) + .await?; + + if !worktrees.is_empty() { + worktree::Entity::insert_many(worktrees.iter().map(|worktree| { + worktree::ActiveModel { + id: ActiveValue::set(worktree.id as i64), + project_id: ActiveValue::set(project.id), + abs_path: ActiveValue::set(worktree.abs_path.clone()), + root_name: ActiveValue::set(worktree.root_name.clone()), + visible: ActiveValue::set(worktree.visible), + scan_id: ActiveValue::set(0), + completed_scan_id: ActiveValue::set(0), + } + })) + .exec(&*tx) + .await?; + } + + project_collaborator::ActiveModel { + project_id: ActiveValue::set(project.id), + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::set(participant.user_id), + replica_id: ActiveValue::set(ReplicaId(0)), + is_host: ActiveValue::set(true), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + Ok((project.id, room)) + }) + .await + } + + pub async fn unshare_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result)>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("project not found"))?; + if project.host_connection()? == connection { + project::Entity::delete(project.into_active_model()) + .exec(&*tx) + .await?; + let room = self.get_room(room_id, &tx).await?; + Ok((room, guest_connection_ids)) + } else { + Err(anyhow!("cannot unshare a project hosted by another user"))? + } + }) + .await + } + + pub async fn update_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + worktrees: &[proto::WorktreeMetadata], + ) -> Result)>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let project = project::Entity::find_by_id(project_id) + .filter( + Condition::all() + .add(project::Column::HostConnectionId.eq(connection.id as i32)) + .add( + project::Column::HostConnectionServerId.eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + + self.update_project_worktrees(project.id, worktrees, &tx) + .await?; + + let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; + let room = self.get_room(project.room_id, &tx).await?; + Ok((room, guest_connection_ids)) + }) + .await + } + + pub(in crate::db) async fn update_project_worktrees( + &self, + project_id: ProjectId, + worktrees: &[proto::WorktreeMetadata], + tx: &DatabaseTransaction, + ) -> Result<()> { + if !worktrees.is_empty() { + worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel { + id: ActiveValue::set(worktree.id as i64), + project_id: ActiveValue::set(project_id), + abs_path: ActiveValue::set(worktree.abs_path.clone()), + root_name: ActiveValue::set(worktree.root_name.clone()), + visible: ActiveValue::set(worktree.visible), + scan_id: ActiveValue::set(0), + completed_scan_id: ActiveValue::set(0), + })) + .on_conflict( + OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id]) + .update_column(worktree::Column::RootName) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + worktree::Entity::delete_many() + .filter(worktree::Column::ProjectId.eq(project_id).and( + worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)), + )) + .exec(&*tx) + .await?; + + Ok(()) + } + + pub async fn update_worktree( + &self, + update: &proto::UpdateWorktree, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let worktree_id = update.worktree_id as i64; + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let _project = project::Entity::find_by_id(project_id) + .filter( + Condition::all() + .add(project::Column::HostConnectionId.eq(connection.id as i32)) + .add( + project::Column::HostConnectionServerId.eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + + // Update metadata. + worktree::Entity::update(worktree::ActiveModel { + id: ActiveValue::set(worktree_id), + project_id: ActiveValue::set(project_id), + root_name: ActiveValue::set(update.root_name.clone()), + scan_id: ActiveValue::set(update.scan_id as i64), + completed_scan_id: if update.is_last_update { + ActiveValue::set(update.scan_id as i64) + } else { + ActiveValue::default() + }, + abs_path: ActiveValue::set(update.abs_path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if !update.updated_entries.is_empty() { + worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| { + let mtime = entry.mtime.clone().unwrap_or_default(); + worktree_entry::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + id: ActiveValue::set(entry.id as i64), + is_dir: ActiveValue::set(entry.is_dir), + path: ActiveValue::set(entry.path.clone()), + inode: ActiveValue::set(entry.inode as i64), + mtime_seconds: ActiveValue::set(mtime.seconds as i64), + mtime_nanos: ActiveValue::set(mtime.nanos as i32), + is_symlink: ActiveValue::set(entry.is_symlink), + is_ignored: ActiveValue::set(entry.is_ignored), + is_external: ActiveValue::set(entry.is_external), + git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), + is_deleted: ActiveValue::set(false), + scan_id: ActiveValue::set(update.scan_id as i64), + } + })) + .on_conflict( + OnConflict::columns([ + worktree_entry::Column::ProjectId, + worktree_entry::Column::WorktreeId, + worktree_entry::Column::Id, + ]) + .update_columns([ + worktree_entry::Column::IsDir, + worktree_entry::Column::Path, + worktree_entry::Column::Inode, + worktree_entry::Column::MtimeSeconds, + worktree_entry::Column::MtimeNanos, + worktree_entry::Column::IsSymlink, + worktree_entry::Column::IsIgnored, + worktree_entry::Column::GitStatus, + worktree_entry::Column::ScanId, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !update.removed_entries.is_empty() { + worktree_entry::Entity::update_many() + .filter( + worktree_entry::Column::ProjectId + .eq(project_id) + .and(worktree_entry::Column::WorktreeId.eq(worktree_id)) + .and( + worktree_entry::Column::Id + .is_in(update.removed_entries.iter().map(|id| *id as i64)), + ), + ) + .set(worktree_entry::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + if !update.updated_repositories.is_empty() { + worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( + |repository| worktree_repository::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + work_directory_id: ActiveValue::set(repository.work_directory_id as i64), + scan_id: ActiveValue::set(update.scan_id as i64), + branch: ActiveValue::set(repository.branch.clone()), + is_deleted: ActiveValue::set(false), + }, + )) + .on_conflict( + OnConflict::columns([ + worktree_repository::Column::ProjectId, + worktree_repository::Column::WorktreeId, + worktree_repository::Column::WorkDirectoryId, + ]) + .update_columns([ + worktree_repository::Column::ScanId, + worktree_repository::Column::Branch, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + } + + if !update.removed_repositories.is_empty() { + worktree_repository::Entity::update_many() + .filter( + worktree_repository::Column::ProjectId + .eq(project_id) + .and(worktree_repository::Column::WorktreeId.eq(worktree_id)) + .and( + worktree_repository::Column::WorkDirectoryId + .is_in(update.removed_repositories.iter().map(|id| *id as i64)), + ), + ) + .set(worktree_repository::ActiveModel { + is_deleted: ActiveValue::Set(true), + scan_id: ActiveValue::Set(update.scan_id as i64), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn update_diagnostic_summary( + &self, + update: &proto::UpdateDiagnosticSummary, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let worktree_id = update.worktree_id as i64; + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let summary = update + .summary + .as_ref() + .ok_or_else(|| anyhow!("invalid summary"))?; + + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + // Update summary. + worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + path: ActiveValue::set(summary.path.clone()), + language_server_id: ActiveValue::set(summary.language_server_id as i64), + error_count: ActiveValue::set(summary.error_count as i32), + warning_count: ActiveValue::set(summary.warning_count as i32), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([ + worktree_diagnostic_summary::Column::ProjectId, + worktree_diagnostic_summary::Column::WorktreeId, + worktree_diagnostic_summary::Column::Path, + ]) + .update_columns([ + worktree_diagnostic_summary::Column::LanguageServerId, + worktree_diagnostic_summary::Column::ErrorCount, + worktree_diagnostic_summary::Column::WarningCount, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn start_language_server( + &self, + update: &proto::StartLanguageServer, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let server = update + .server + .as_ref() + .ok_or_else(|| anyhow!("invalid language server"))?; + + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + // Add the newly-started language server. + language_server::Entity::insert(language_server::ActiveModel { + project_id: ActiveValue::set(project_id), + id: ActiveValue::set(server.id as i64), + name: ActiveValue::set(server.name.clone()), + ..Default::default() + }) + .on_conflict( + OnConflict::columns([ + language_server::Column::ProjectId, + language_server::Column::Id, + ]) + .update_column(language_server::Column::Name) + .to_owned(), + ) + .exec(&*tx) + .await?; + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn update_worktree_settings( + &self, + update: &proto::UpdateWorktreeSettings, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + if let Some(content) = &update.content { + worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + content: ActiveValue::Set(content.clone()), + }) + .on_conflict( + OnConflict::columns([ + worktree_settings_file::Column::ProjectId, + worktree_settings_file::Column::WorktreeId, + worktree_settings_file::Column::Path, + ]) + .update_column(worktree_settings_file::Column::Content) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + + pub async fn join_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("must join a room first"))?; + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.room_id != participant.room_id { + return Err(anyhow!("no such project"))?; + } + + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(1); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + let new_collaborator = project_collaborator::ActiveModel { + project_id: ActiveValue::set(project_id), + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::set(participant.user_id), + replica_id: ActiveValue::set(replica_id), + is_host: ActiveValue::set(false), + ..Default::default() + } + .insert(&*tx) + .await?; + collaborators.push(new_collaborator); + + let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; + let mut worktrees = db_worktrees + .into_iter() + .map(|db_worktree| { + ( + db_worktree.id as u64, + Worktree { + id: db_worktree.id as u64, + abs_path: db_worktree.abs_path, + root_name: db_worktree.root_name, + visible: db_worktree.visible, + entries: Default::default(), + repository_entries: Default::default(), + diagnostic_summaries: Default::default(), + settings_files: Default::default(), + scan_id: db_worktree.scan_id as u64, + completed_scan_id: db_worktree.completed_scan_id as u64, + }, + ) + }) + .collect::>(); + + // Populate worktree entries. + { + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project_id)) + .add(worktree_entry::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { + worktree.entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, + git_status: db_entry.git_status.map(|status| status as i32), + }); + } + } + } + + // Populate repository entries. + { + let mut db_repository_entries = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project_id)) + .add(worktree_repository::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_repository_entry) = db_repository_entries.next().await { + let db_repository_entry = db_repository_entry?; + if let Some(worktree) = + worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) + { + worktree.repository_entries.insert( + db_repository_entry.work_directory_id as u64, + proto::RepositoryEntry { + work_directory_id: db_repository_entry.work_directory_id as u64, + branch: db_repository_entry.branch, + }, + ); + } + } + } + + // Populate worktree diagnostic summaries. + { + let mut db_summaries = worktree_diagnostic_summary::Entity::find() + .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_summary) = db_summaries.next().await { + let db_summary = db_summary?; + if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { + worktree + .diagnostic_summaries + .push(proto::DiagnosticSummary { + path: db_summary.path, + language_server_id: db_summary.language_server_id as u64, + error_count: db_summary.error_count as u32, + warning_count: db_summary.warning_count as u32, + }); + } + } + } + + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = + worktrees.get_mut(&(db_settings_file.worktree_id as u64)) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + + // Populate language servers. + let language_servers = project + .find_related(language_server::Entity) + .all(&*tx) + .await?; + + let project = Project { + collaborators: collaborators + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect(), + worktrees, + language_servers: language_servers + .into_iter() + .map(|language_server| proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + }) + .collect(), + }; + Ok((project, replica_id as ReplicaId)) + }) + .await + } + + pub async fn leave_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let result = project_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(project_collaborator::Column::ProjectId.eq(project_id)) + .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + let collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let connection_ids = collaborators + .into_iter() + .map(|collaborator| collaborator.connection()) + .collect(); + + follower::Entity::delete_many() + .filter( + Condition::any() + .add( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::LeaderConnectionServerId + .eq(connection.owner_id), + ) + .add(follower::Column::LeaderConnectionId.eq(connection.id)), + ) + .add( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::FollowerConnectionServerId + .eq(connection.owner_id), + ) + .add(follower::Column::FollowerConnectionId.eq(connection.id)), + ), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(project.room_id, &tx).await?; + let left_project = LeftProject { + id: project_id, + host_user_id: project.host_user_id, + host_connection_id: project.host_connection()?, + connection_ids, + }; + Ok((room, left_project)) + }) + .await + } + + pub async fn project_collaborators( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.eq(project_id)) + .all(&*tx) + .await? + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect::>(); + + if collaborators + .iter() + .any(|collaborator| collaborator.connection_id == connection_id) + { + Ok(collaborators) + } else { + Err(anyhow!("no such project"))? + } + }) + .await + } + + pub async fn project_connection_ids( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result>> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let mut collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + + let mut connection_ids = HashSet::default(); + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + connection_ids.insert(collaborator.connection()); + } + + if connection_ids.contains(&connection_id) { + Ok(connection_ids) + } else { + Err(anyhow!("no such project"))? + } + }) + .await + } + + async fn project_guest_connection_ids( + &self, + project_id: ProjectId, + tx: &DatabaseTransaction, + ) -> Result> { + let mut collaborators = project_collaborator::Entity::find() + .filter( + project_collaborator::Column::ProjectId + .eq(project_id) + .and(project_collaborator::Column::IsHost.eq(false)), + ) + .stream(tx) + .await?; + + let mut guest_connection_ids = Vec::new(); + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + guest_connection_ids.push(collaborator.connection()); + } + Ok(guest_connection_ids) + } + + pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result { + self.transaction(|tx| async move { + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("project {} not found", project_id))?; + Ok(project.room_id) + }) + .await + } + + pub async fn follow( + &self, + project_id: ProjectId, + leader_connection: ConnectionId, + follower_connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + follower::ActiveModel { + room_id: ActiveValue::set(room_id), + project_id: ActiveValue::set(project_id), + leader_connection_server_id: ActiveValue::set(ServerId( + leader_connection.owner_id as i32, + )), + leader_connection_id: ActiveValue::set(leader_connection.id as i32), + follower_connection_server_id: ActiveValue::set(ServerId( + follower_connection.owner_id as i32, + )), + follower_connection_id: ActiveValue::set(follower_connection.id as i32), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room_id, &*tx).await?; + Ok(room) + }) + .await + } + + pub async fn unfollow( + &self, + project_id: ProjectId, + leader_connection: ConnectionId, + follower_connection: ConnectionId, + ) -> Result> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + follower::Entity::delete_many() + .filter( + Condition::all() + .add(follower::Column::ProjectId.eq(project_id)) + .add( + follower::Column::LeaderConnectionServerId + .eq(leader_connection.owner_id), + ) + .add(follower::Column::LeaderConnectionId.eq(leader_connection.id)) + .add( + follower::Column::FollowerConnectionServerId + .eq(follower_connection.owner_id), + ) + .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)), + ) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &*tx).await?; + Ok(room) + }) + .await + } +} diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs new file mode 100644 index 0000000000..ee79f2cb4f --- /dev/null +++ b/crates/collab/src/db/queries/rooms.rs @@ -0,0 +1,1073 @@ +use super::*; + +impl Database { + pub async fn refresh_room( + &self, + room_id: RoomId, + new_server_id: ServerId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + let stale_participant_filter = Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::AnsweringConnectionId.is_not_null()) + .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id)); + + let stale_participant_user_ids = room_participant::Entity::find() + .filter(stale_participant_filter.clone()) + .all(&*tx) + .await? + .into_iter() + .map(|participant| participant.user_id) + .collect::>(); + + // Delete participants who failed to reconnect and cancel their calls. + let mut canceled_calls_to_user_ids = Vec::new(); + room_participant::Entity::delete_many() + .filter(stale_participant_filter) + .exec(&*tx) + .await?; + let called_participants = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::CallingUserId + .is_in(stale_participant_user_ids.iter().copied()), + ) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .all(&*tx) + .await?; + room_participant::Entity::delete_many() + .filter( + room_participant::Column::Id + .is_in(called_participants.iter().map(|participant| participant.id)), + ) + .exec(&*tx) + .await?; + canceled_calls_to_user_ids.extend( + called_participants + .into_iter() + .map(|participant| participant.user_id), + ); + + 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?; + } else { + channel_members = Vec::new(); + + // Delete the room if it becomes empty. + if room.participants.is_empty() { + project::Entity::delete_many() + .filter(project::Column::RoomId.eq(room_id)) + .exec(&*tx) + .await?; + room::Entity::delete_by_id(room_id).exec(&*tx).await?; + } + }; + + Ok(RefreshedRoom { + room, + channel_id, + channel_members, + stale_participant_user_ids, + canceled_calls_to_user_ids, + }) + }) + .await + } + + pub async fn incoming_call_for_user( + &self, + user_id: UserId, + ) -> Result> { + self.transaction(|tx| async move { + let pending_participant = room_participant::Entity::find() + .filter( + room_participant::Column::UserId + .eq(user_id) + .and(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .one(&*tx) + .await?; + + if let Some(pending_participant) = pending_participant { + let room = self.get_room(pending_participant.room_id, &tx).await?; + Ok(Self::build_incoming_call(&room, user_id)) + } else { + Ok(None) + } + }) + .await + } + + pub async fn create_room( + &self, + user_id: UserId, + connection: ConnectionId, + live_kit_room: &str, + ) -> Result { + self.transaction(|tx| async move { + let room = room::ActiveModel { + live_kit_room: ActiveValue::set(live_kit_room.into()), + ..Default::default() + } + .insert(&*tx) + .await?; + 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, + ))), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room.id, &tx).await?; + Ok(room) + }) + .await + } + + pub async fn call( + &self, + room_id: RoomId, + calling_user_id: UserId, + calling_connection: ConnectionId, + called_user_id: UserId, + initial_project_id: Option, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + room_participant::ActiveModel { + room_id: ActiveValue::set(room_id), + user_id: ActiveValue::set(called_user_id), + answering_connection_lost: ActiveValue::set(false), + calling_user_id: ActiveValue::set(calling_user_id), + calling_connection_id: ActiveValue::set(calling_connection.id as i32), + calling_connection_server_id: ActiveValue::set(Some(ServerId( + calling_connection.owner_id as i32, + ))), + initial_project_id: ActiveValue::set(initial_project_id), + ..Default::default() + } + .insert(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + let incoming_call = Self::build_incoming_call(&room, called_user_id) + .ok_or_else(|| anyhow!("failed to build incoming call"))?; + Ok((room, incoming_call)) + }) + .await + } + + pub async fn call_failed( + &self, + room_id: RoomId, + called_user_id: UserId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + room_participant::Entity::delete_many() + .filter( + room_participant::Column::RoomId + .eq(room_id) + .and(room_participant::Column::UserId.eq(called_user_id)), + ) + .exec(&*tx) + .await?; + let room = self.get_room(room_id, &tx).await?; + Ok(room) + }) + .await + } + + pub async fn decline_call( + &self, + expected_room_id: Option, + user_id: UserId, + ) -> Result>> { + self.optional_room_transaction(|tx| async move { + let mut filter = Condition::all() + .add(room_participant::Column::UserId.eq(user_id)) + .add(room_participant::Column::AnsweringConnectionId.is_null()); + if let Some(room_id) = expected_room_id { + filter = filter.add(room_participant::Column::RoomId.eq(room_id)); + } + let participant = room_participant::Entity::find() + .filter(filter) + .one(&*tx) + .await?; + + let participant = if let Some(participant) = participant { + participant + } else if expected_room_id.is_some() { + return Err(anyhow!("could not find call to decline"))?; + } else { + return Ok(None); + }; + + let room_id = participant.room_id; + room_participant::Entity::delete(participant.into_active_model()) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + Ok(Some((room_id, room))) + }) + .await + } + + pub async fn cancel_call( + &self, + room_id: RoomId, + calling_connection: ConnectionId, + called_user_id: UserId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add(room_participant::Column::UserId.eq(called_user_id)) + .add(room_participant::Column::RoomId.eq(room_id)) + .add( + room_participant::Column::CallingConnectionId + .eq(calling_connection.id as i32), + ) + .add( + room_participant::Column::CallingConnectionServerId + .eq(calling_connection.owner_id as i32), + ) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no call to cancel"))?; + + room_participant::Entity::delete(participant.into_active_model()) + .exec(&*tx) + .await?; + + let room = self.get_room(room_id, &tx).await?; + Ok(room) + }) + .await + } + + pub async fn join_room( + &self, + room_id: RoomId, + user_id: UserId, + connection: ConnectionId, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryChannelId { + ChannelId, + } + let channel_id: Option = room::Entity::find() + .select_only() + .column(room::Column::ChannelId) + .filter(room::Column::Id.eq(room_id)) + .into_values::<_, QueryChannelId>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + 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), + 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, + ))), + ..Default::default() + }]) + .on_conflict( + OnConflict::columns([room_participant::Column::UserId]) + .update_columns([ + room_participant::Column::AnsweringConnectionId, + room_participant::Column::AnsweringConnectionServerId, + room_participant::Column::AnsweringConnectionLost, + ]) + .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 { + 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"))?; + } + } + + 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? + } else { + Vec::new() + }; + Ok(JoinRoom { + room, + channel_id, + channel_members, + }) + }) + .await + } + + pub async fn rejoin_room( + &self, + rejoin_room: proto::RejoinRoom, + user_id: UserId, + connection: ConnectionId, + ) -> Result> { + let room_id = RoomId::from_proto(rejoin_room.id); + self.room_transaction(room_id, |tx| async { + let tx = tx; + let participant_update = 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_not_null()) + .add( + Condition::any() + .add(room_participant::Column::AnsweringConnectionLost.eq(true)) + .add( + room_participant::Column::AnsweringConnectionServerId + .ne(connection.owner_id as i32), + ), + ), + ) + .set(room_participant::ActiveModel { + 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 participant_update.rows_affected == 0 { + return Err(anyhow!("room does not exist or was already joined"))?; + } + + let mut reshared_projects = Vec::new(); + for reshared_project in &rejoin_room.reshared_projects { + let project_id = ProjectId::from_proto(reshared_project.project_id); + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("project does not exist"))?; + if project.host_user_id != user_id { + return Err(anyhow!("no such project"))?; + } + + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let host_ix = collaborators + .iter() + .position(|collaborator| { + collaborator.user_id == user_id && collaborator.is_host + }) + .ok_or_else(|| anyhow!("host not found among collaborators"))?; + let host = collaborators.swap_remove(host_ix); + let old_connection_id = host.connection(); + + project::Entity::update(project::ActiveModel { + host_connection_id: ActiveValue::set(Some(connection.id as i32)), + host_connection_server_id: ActiveValue::set(Some(ServerId( + connection.owner_id as i32, + ))), + ..project.into_active_model() + }) + .exec(&*tx) + .await?; + project_collaborator::Entity::update(project_collaborator::ActiveModel { + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + ..host.into_active_model() + }) + .exec(&*tx) + .await?; + + self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx) + .await?; + + reshared_projects.push(ResharedProject { + id: project_id, + old_connection_id, + collaborators: collaborators + .iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect(), + worktrees: reshared_project.worktrees.clone(), + }); + } + + project::Entity::delete_many() + .filter( + Condition::all() + .add(project::Column::RoomId.eq(room_id)) + .add(project::Column::HostUserId.eq(user_id)) + .add( + project::Column::Id + .is_not_in(reshared_projects.iter().map(|project| project.id)), + ), + ) + .exec(&*tx) + .await?; + + let mut rejoined_projects = Vec::new(); + for rejoined_project in &rejoin_room.rejoined_projects { + let project_id = ProjectId::from_proto(rejoined_project.id); + let Some(project) = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? else { continue }; + + let mut worktrees = Vec::new(); + let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; + for db_worktree in db_worktrees { + let mut worktree = RejoinedWorktree { + id: db_worktree.id as u64, + abs_path: db_worktree.abs_path, + root_name: db_worktree.root_name, + visible: db_worktree.visible, + updated_entries: Default::default(), + removed_entries: Default::default(), + updated_repositories: Default::default(), + removed_repositories: Default::default(), + diagnostic_summaries: Default::default(), + settings_files: Default::default(), + scan_id: db_worktree.scan_id as u64, + completed_scan_id: db_worktree.completed_scan_id as u64, + }; + + let rejoined_worktree = rejoined_project + .worktrees + .iter() + .find(|worktree| worktree.id == db_worktree.id as u64); + + // File entries + { + let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree { + worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id) + } else { + worktree_entry::Column::IsDeleted.eq(false) + }; + + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project.id)) + .add(worktree_entry::Column::WorktreeId.eq(worktree.id)) + .add(entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if db_entry.is_deleted { + worktree.removed_entries.push(db_entry.id as u64); + } else { + worktree.updated_entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, + git_status: db_entry.git_status.map(|status| status as i32), + }); + } + } + } + + // Repository Entries + { + let repository_entry_filter = + if let Some(rejoined_worktree) = rejoined_worktree { + worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id) + } else { + worktree_repository::Column::IsDeleted.eq(false) + }; + + let mut db_repositories = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project.id)) + .add(worktree_repository::Column::WorktreeId.eq(worktree.id)) + .add(repository_entry_filter), + ) + .stream(&*tx) + .await?; + + while let Some(db_repository) = db_repositories.next().await { + let db_repository = db_repository?; + if db_repository.is_deleted { + worktree + .removed_repositories + .push(db_repository.work_directory_id as u64); + } else { + worktree.updated_repositories.push(proto::RepositoryEntry { + work_directory_id: db_repository.work_directory_id as u64, + branch: db_repository.branch, + }); + } + } + } + + worktrees.push(worktree); + } + + let language_servers = project + .find_related(language_server::Entity) + .all(&*tx) + .await? + .into_iter() + .map(|language_server| proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + }) + .collect::>(); + + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees + .iter_mut() + .find(|w| w.id == db_settings_file.worktree_id as u64) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let self_collaborator = if let Some(self_collaborator_ix) = collaborators + .iter() + .position(|collaborator| collaborator.user_id == user_id) + { + collaborators.swap_remove(self_collaborator_ix) + } else { + continue; + }; + let old_connection_id = self_collaborator.connection(); + project_collaborator::Entity::update(project_collaborator::ActiveModel { + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + ..self_collaborator.into_active_model() + }) + .exec(&*tx) + .await?; + + let collaborators = collaborators + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect::>(); + + rejoined_projects.push(RejoinedProject { + id: project_id, + old_connection_id, + collaborators, + worktrees, + language_servers, + }); + } + + 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? + } else { + Vec::new() + }; + + Ok(RejoinedRoom { + room, + channel_id, + channel_members, + rejoined_projects, + reshared_projects, + }) + }) + .await + } + + pub async fn leave_room( + &self, + connection: ConnectionId, + ) -> Result>> { + self.optional_room_transaction(|tx| async move { + let leaving_participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await?; + + if let Some(leaving_participant) = leaving_participant { + // Leave room. + let room_id = leaving_participant.room_id; + room_participant::Entity::delete_by_id(leaving_participant.id) + .exec(&*tx) + .await?; + + // Cancel pending calls initiated by the leaving user. + let called_participants = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::CallingUserId + .eq(leaving_participant.user_id), + ) + .add(room_participant::Column::AnsweringConnectionId.is_null()), + ) + .all(&*tx) + .await?; + room_participant::Entity::delete_many() + .filter( + room_participant::Column::Id + .is_in(called_participants.iter().map(|participant| participant.id)), + ) + .exec(&*tx) + .await?; + let canceled_calls_to_user_ids = called_participants + .into_iter() + .map(|participant| participant.user_id) + .collect(); + + // Detect left projects. + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryProjectIds { + ProjectId, + } + let project_ids: Vec = project_collaborator::Entity::find() + .select_only() + .column_as( + project_collaborator::Column::ProjectId, + QueryProjectIds::ProjectId, + ) + .filter( + Condition::all() + .add( + project_collaborator::Column::ConnectionId.eq(connection.id as i32), + ) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .into_values::<_, QueryProjectIds>() + .all(&*tx) + .await?; + let mut left_projects = HashMap::default(); + let mut collaborators = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.is_in(project_ids)) + .stream(&*tx) + .await?; + while let Some(collaborator) = collaborators.next().await { + let collaborator = collaborator?; + let left_project = + left_projects + .entry(collaborator.project_id) + .or_insert(LeftProject { + id: collaborator.project_id, + host_user_id: Default::default(), + connection_ids: Default::default(), + host_connection_id: Default::default(), + }); + + let collaborator_connection_id = collaborator.connection(); + if collaborator_connection_id != connection { + left_project.connection_ids.push(collaborator_connection_id); + } + + if collaborator.is_host { + left_project.host_user_id = collaborator.user_id; + left_project.host_connection_id = collaborator_connection_id; + } + } + drop(collaborators); + + // Leave projects. + project_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add( + project_collaborator::Column::ConnectionId.eq(connection.id as i32), + ) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + + // Unshare projects. + project::Entity::delete_many() + .filter( + Condition::all() + .add(project::Column::RoomId.eq(room_id)) + .add(project::Column::HostConnectionId.eq(connection.id as i32)) + .add( + project::Column::HostConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + + let (channel_id, room) = self.get_channel_room(room_id, &tx).await?; + let deleted = if room.participants.is_empty() { + let result = room::Entity::delete_by_id(room_id) + .filter(room::Column::ChannelId.is_null()) + .exec(&*tx) + .await?; + result.rows_affected > 0 + } else { + false + }; + + let channel_members = if let Some(channel_id) = channel_id { + self.get_channel_members_internal(channel_id, &tx).await? + } else { + Vec::new() + }; + let left_room = LeftRoom { + room, + channel_id, + channel_members, + left_projects, + canceled_calls_to_user_ids, + deleted, + }; + + if left_room.room.participants.is_empty() { + self.rooms.remove(&room_id); + } + + Ok(Some((room_id, left_room))) + } else { + Ok(None) + } + }) + .await + } + + pub async fn update_room_participant_location( + &self, + room_id: RoomId, + connection: ConnectionId, + location: proto::ParticipantLocation, + ) -> Result> { + self.room_transaction(room_id, |tx| async { + let tx = tx; + let location_kind; + let location_project_id; + match location + .variant + .as_ref() + .ok_or_else(|| anyhow!("invalid location"))? + { + proto::participant_location::Variant::SharedProject(project) => { + location_kind = 0; + location_project_id = Some(ProjectId::from_proto(project.id)); + } + proto::participant_location::Variant::UnsharedProject(_) => { + location_kind = 1; + location_project_id = None; + } + proto::participant_location::Variant::External(_) => { + location_kind = 2; + location_project_id = None; + } + } + + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(room_participant::ActiveModel { + location_kind: ActiveValue::set(Some(location_kind)), + location_project_id: ActiveValue::set(location_project_id), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected == 1 { + let room = self.get_room(room_id, &tx).await?; + Ok(room) + } else { + Err(anyhow!("could not update room participant location"))? + } + }) + .await + } + + pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { + self.transaction(|tx| async move { + let participant = room_participant::Entity::find() + .filter( + Condition::all() + .add( + room_participant::Column::AnsweringConnectionId + .eq(connection.id as i32), + ) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("not a participant in any room"))?; + + room_participant::Entity::update(room_participant::ActiveModel { + answering_connection_lost: ActiveValue::set(true), + ..participant.into_active_model() + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + fn build_incoming_call( + room: &proto::Room, + called_user_id: UserId, + ) -> Option { + let pending_participant = room + .pending_participants + .iter() + .find(|participant| participant.user_id == called_user_id.to_proto())?; + + Some(proto::IncomingCall { + room_id: room.id, + calling_user_id: pending_participant.calling_user_id, + participant_user_ids: room + .participants + .iter() + .map(|participant| participant.user_id) + .collect(), + initial_project: room.participants.iter().find_map(|participant| { + let initial_project_id = pending_participant.initial_project_id?; + participant + .projects + .iter() + .find(|project| project.id == initial_project_id) + .cloned() + }), + }) + } + + pub async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result { + let (_, room) = self.get_channel_room(room_id, tx).await?; + Ok(room) + } + + async fn get_channel_room( + &self, + room_id: RoomId, + tx: &DatabaseTransaction, + ) -> Result<(Option, proto::Room)> { + let db_room = room::Entity::find_by_id(room_id) + .one(tx) + .await? + .ok_or_else(|| anyhow!("could not find room"))?; + + let mut db_participants = db_room + .find_related(room_participant::Entity) + .stream(tx) + .await?; + let mut participants = HashMap::default(); + let mut pending_participants = Vec::new(); + while let Some(db_participant) = db_participants.next().await { + let db_participant = db_participant?; + if let Some((answering_connection_id, answering_connection_server_id)) = db_participant + .answering_connection_id + .zip(db_participant.answering_connection_server_id) + { + let location = match ( + db_participant.location_kind, + db_participant.location_project_id, + ) { + (Some(0), Some(project_id)) => { + Some(proto::participant_location::Variant::SharedProject( + proto::participant_location::SharedProject { + id: project_id.to_proto(), + }, + )) + } + (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject( + Default::default(), + )), + _ => Some(proto::participant_location::Variant::External( + Default::default(), + )), + }; + + let answering_connection = ConnectionId { + owner_id: answering_connection_server_id.0 as u32, + id: answering_connection_id as u32, + }; + participants.insert( + answering_connection, + proto::Participant { + user_id: db_participant.user_id.to_proto(), + peer_id: Some(answering_connection.into()), + projects: Default::default(), + location: Some(proto::ParticipantLocation { variant: location }), + }, + ); + } else { + pending_participants.push(proto::PendingParticipant { + user_id: db_participant.user_id.to_proto(), + calling_user_id: db_participant.calling_user_id.to_proto(), + initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()), + }); + } + } + drop(db_participants); + + let mut db_projects = db_room + .find_related(project::Entity) + .find_with_related(worktree::Entity) + .stream(tx) + .await?; + + while let Some(row) = db_projects.next().await { + let (db_project, db_worktree) = row?; + let host_connection = db_project.host_connection()?; + if let Some(participant) = participants.get_mut(&host_connection) { + let project = if let Some(project) = participant + .projects + .iter_mut() + .find(|project| project.id == db_project.id.to_proto()) + { + project + } else { + participant.projects.push(proto::ParticipantProject { + id: db_project.id.to_proto(), + worktree_root_names: Default::default(), + }); + participant.projects.last_mut().unwrap() + }; + + if let Some(db_worktree) = db_worktree { + if db_worktree.visible { + project.worktree_root_names.push(db_worktree.root_name); + } + } + } + } + drop(db_projects); + + let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; + let mut followers = Vec::new(); + while let Some(db_follower) = db_followers.next().await { + let db_follower = db_follower?; + followers.push(proto::Follower { + leader_id: Some(db_follower.leader_connection().into()), + follower_id: Some(db_follower.follower_connection().into()), + project_id: db_follower.project_id.to_proto(), + }); + } + + Ok(( + db_room.channel_id, + proto::Room { + id: db_room.id.to_proto(), + live_kit_room: db_room.live_kit_room, + participants: participants.into_values().collect(), + pending_participants, + followers, + }, + )) + } +} diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs new file mode 100644 index 0000000000..08a2bda16a --- /dev/null +++ b/crates/collab/src/db/queries/servers.rs @@ -0,0 +1,81 @@ +use super::*; + +impl Database { + pub async fn create_server(&self, environment: &str) -> Result { + self.transaction(|tx| async move { + let server = server::ActiveModel { + environment: ActiveValue::set(environment.into()), + ..Default::default() + } + .insert(&*tx) + .await?; + Ok(server.id) + }) + .await + } + + pub async fn stale_room_ids( + &self, + environment: &str, + new_server_id: ServerId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + RoomId, + } + + let stale_server_epochs = self + .stale_server_ids(environment, new_server_id, &tx) + .await?; + Ok(room_participant::Entity::find() + .select_only() + .column(room_participant::Column::RoomId) + .distinct() + .filter( + room_participant::Column::AnsweringConnectionServerId + .is_in(stale_server_epochs), + ) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?) + }) + .await + } + + pub async fn delete_stale_servers( + &self, + environment: &str, + new_server_id: ServerId, + ) -> Result<()> { + self.transaction(|tx| async move { + server::Entity::delete_many() + .filter( + Condition::all() + .add(server::Column::Environment.eq(environment)) + .add(server::Column::Id.ne(new_server_id)), + ) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + async fn stale_server_ids( + &self, + environment: &str, + new_server_id: ServerId, + tx: &DatabaseTransaction, + ) -> Result> { + let stale_servers = server::Entity::find() + .filter( + Condition::all() + .add(server::Column::Environment.eq(environment)) + .add(server::Column::Id.ne(new_server_id)), + ) + .all(&*tx) + .await?; + Ok(stale_servers.into_iter().map(|server| server.id).collect()) + } +} diff --git a/crates/collab/src/db/queries/signups.rs b/crates/collab/src/db/queries/signups.rs new file mode 100644 index 0000000000..8cb8d866fb --- /dev/null +++ b/crates/collab/src/db/queries/signups.rs @@ -0,0 +1,349 @@ +use super::*; +use hyper::StatusCode; + +impl Database { + pub async fn create_invite_from_code( + &self, + code: &str, + email_address: &str, + device_id: Option<&str>, + added_to_mailing_list: bool, + ) -> Result { + self.transaction(|tx| async move { + let existing_user = user::Entity::find() + .filter(user::Column::EmailAddress.eq(email_address)) + .one(&*tx) + .await?; + + if existing_user.is_some() { + Err(anyhow!("email address is already in use"))?; + } + + let inviting_user_with_invites = match user::Entity::find() + .filter( + user::Column::InviteCode + .eq(code) + .and(user::Column::InviteCount.gt(0)), + ) + .one(&*tx) + .await? + { + Some(inviting_user) => inviting_user, + None => { + return Err(Error::Http( + StatusCode::UNAUTHORIZED, + "unable to find an invite code with invites remaining".to_string(), + ))? + } + }; + user::Entity::update_many() + .filter( + user::Column::Id + .eq(inviting_user_with_invites.id) + .and(user::Column::InviteCount.gt(0)), + ) + .col_expr( + user::Column::InviteCount, + Expr::col(user::Column::InviteCount).sub(1), + ) + .exec(&*tx) + .await?; + + let signup = signup::Entity::insert(signup::ActiveModel { + email_address: ActiveValue::set(email_address.into()), + email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), + email_confirmation_sent: ActiveValue::set(false), + inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)), + platform_linux: ActiveValue::set(false), + platform_mac: ActiveValue::set(false), + platform_windows: ActiveValue::set(false), + platform_unknown: ActiveValue::set(true), + device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())), + added_to_mailing_list: ActiveValue::set(added_to_mailing_list), + ..Default::default() + }) + .on_conflict( + OnConflict::column(signup::Column::EmailAddress) + .update_column(signup::Column::InvitingUserId) + .to_owned(), + ) + .exec_with_returning(&*tx) + .await?; + + Ok(Invite { + email_address: signup.email_address, + email_confirmation_code: signup.email_confirmation_code, + }) + }) + .await + } + + pub async fn create_user_from_invite( + &self, + invite: &Invite, + user: NewUserParams, + ) -> Result> { + self.transaction(|tx| async { + let tx = tx; + let signup = signup::Entity::find() + .filter( + signup::Column::EmailAddress + .eq(invite.email_address.as_str()) + .and( + signup::Column::EmailConfirmationCode + .eq(invite.email_confirmation_code.as_str()), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?; + + if signup.user_id.is_some() { + return Ok(None); + } + + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(Some(invite.email_address.clone())), + github_login: ActiveValue::set(user.github_login.clone()), + github_user_id: ActiveValue::set(Some(user.github_user_id)), + admin: ActiveValue::set(false), + invite_count: ActiveValue::set(user.invite_count), + invite_code: ActiveValue::set(Some(random_invite_code())), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .on_conflict( + OnConflict::column(user::Column::GithubLogin) + .update_columns([ + user::Column::EmailAddress, + user::Column::GithubUserId, + user::Column::Admin, + ]) + .to_owned(), + ) + .exec_with_returning(&*tx) + .await?; + + let mut signup = signup.into_active_model(); + signup.user_id = ActiveValue::set(Some(user.id)); + let signup = signup.update(&*tx).await?; + + if let Some(inviting_user_id) = signup.inviting_user_id { + let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id { + (inviting_user_id, user.id, true) + } else { + (user.id, inviting_user_id, false) + }; + + contact::Entity::insert(contact::ActiveModel { + user_id_a: ActiveValue::set(user_id_a), + user_id_b: ActiveValue::set(user_id_b), + a_to_b: ActiveValue::set(a_to_b), + should_notify: ActiveValue::set(true), + accepted: ActiveValue::set(true), + ..Default::default() + }) + .on_conflict(OnConflict::new().do_nothing().to_owned()) + .exec_without_returning(&*tx) + .await?; + } + + Ok(Some(NewUserResult { + user_id: user.id, + metrics_id: user.metrics_id.to_string(), + inviting_user_id: signup.inviting_user_id, + signup_device_id: signup.device_id, + })) + }) + .await + } + + pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> { + self.transaction(|tx| async move { + if count > 0 { + user::Entity::update_many() + .filter( + user::Column::Id + .eq(id) + .and(user::Column::InviteCode.is_null()), + ) + .set(user::ActiveModel { + invite_code: ActiveValue::set(Some(random_invite_code())), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + user::Entity::update_many() + .filter(user::Column::Id.eq(id)) + .set(user::ActiveModel { + invite_count: ActiveValue::set(count), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_invite_code_for_user(&self, id: UserId) -> Result> { + self.transaction(|tx| async move { + match user::Entity::find_by_id(id).one(&*tx).await? { + Some(user) if user.invite_code.is_some() => { + Ok(Some((user.invite_code.unwrap(), user.invite_count))) + } + _ => Ok(None), + } + }) + .await + } + + pub async fn get_user_for_invite_code(&self, code: &str) -> Result { + self.transaction(|tx| async move { + user::Entity::find() + .filter(user::Column::InviteCode.eq(code)) + .one(&*tx) + .await? + .ok_or_else(|| { + Error::Http( + StatusCode::NOT_FOUND, + "that invite code does not exist".to_string(), + ) + }) + }) + .await + } + + pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> { + self.transaction(|tx| async move { + signup::Entity::insert(signup::ActiveModel { + email_address: ActiveValue::set(signup.email_address.clone()), + email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), + email_confirmation_sent: ActiveValue::set(false), + platform_mac: ActiveValue::set(signup.platform_mac), + platform_windows: ActiveValue::set(signup.platform_windows), + platform_linux: ActiveValue::set(signup.platform_linux), + platform_unknown: ActiveValue::set(false), + editor_features: ActiveValue::set(Some(signup.editor_features.clone())), + programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), + device_id: ActiveValue::set(signup.device_id.clone()), + added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list), + ..Default::default() + }) + .on_conflict( + OnConflict::column(signup::Column::EmailAddress) + .update_columns([ + signup::Column::PlatformMac, + signup::Column::PlatformWindows, + signup::Column::PlatformLinux, + signup::Column::EditorFeatures, + signup::Column::ProgrammingLanguages, + signup::Column::DeviceId, + signup::Column::AddedToMailingList, + ]) + .to_owned(), + ) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_signup(&self, email_address: &str) -> Result { + self.transaction(|tx| async move { + let signup = signup::Entity::find() + .filter(signup::Column::EmailAddress.eq(email_address)) + .one(&*tx) + .await? + .ok_or_else(|| { + anyhow!("signup with email address {} doesn't exist", email_address) + })?; + + Ok(signup) + }) + .await + } + + pub async fn get_waitlist_summary(&self) -> Result { + self.transaction(|tx| async move { + let query = " + SELECT + COUNT(*) as count, + COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count, + COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count, + COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count, + COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count + FROM ( + SELECT * + FROM signups + WHERE + NOT email_confirmation_sent + ) AS unsent + "; + Ok( + WaitlistSummary::find_by_statement(Statement::from_sql_and_values( + self.pool.get_database_backend(), + query.into(), + vec![], + )) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid result"))?, + ) + }) + .await + } + + pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> { + let emails = invites + .iter() + .map(|s| s.email_address.as_str()) + .collect::>(); + self.transaction(|tx| async { + let tx = tx; + signup::Entity::update_many() + .filter(signup::Column::EmailAddress.is_in(emails.iter().copied())) + .set(signup::ActiveModel { + email_confirmation_sent: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn get_unsent_invites(&self, count: usize) -> Result> { + self.transaction(|tx| async move { + Ok(signup::Entity::find() + .select_only() + .column(signup::Column::EmailAddress) + .column(signup::Column::EmailConfirmationCode) + .filter( + signup::Column::EmailConfirmationSent.eq(false).and( + signup::Column::PlatformMac + .eq(true) + .or(signup::Column::PlatformUnknown.eq(true)), + ), + ) + .order_by_asc(signup::Column::CreatedAt) + .limit(count as u64) + .into_model() + .all(&*tx) + .await?) + }) + .await + } +} + +fn random_invite_code() -> String { + nanoid::nanoid!(16) +} + +fn random_email_confirmation_code() -> String { + nanoid::nanoid!(64) +} diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs new file mode 100644 index 0000000000..bac0f14f83 --- /dev/null +++ b/crates/collab/src/db/queries/users.rs @@ -0,0 +1,243 @@ +use super::*; + +impl Database { + pub async fn create_user( + &self, + email_address: &str, + admin: bool, + params: NewUserParams, + ) -> Result { + self.transaction(|tx| async { + let tx = tx; + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(Some(email_address.into())), + github_login: ActiveValue::set(params.github_login.clone()), + github_user_id: ActiveValue::set(Some(params.github_user_id)), + admin: ActiveValue::set(admin), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .on_conflict( + OnConflict::column(user::Column::GithubLogin) + .update_column(user::Column::GithubLogin) + .to_owned(), + ) + .exec_with_returning(&*tx) + .await?; + + Ok(NewUserResult { + user_id: user.id, + metrics_id: user.metrics_id.to_string(), + signup_device_id: None, + inviting_user_id: None, + }) + }) + .await + } + + pub async fn get_user_by_id(&self, id: UserId) -> Result> { + self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) + .await + } + + pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { + self.transaction(|tx| async { + let tx = tx; + Ok(user::Entity::find() + .filter(user::Column::Id.is_in(ids.iter().copied())) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + self.transaction(|tx| async move { + Ok(user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(&*tx) + .await?) + }) + .await + } + + pub async fn get_or_create_user_by_github_account( + &self, + github_login: &str, + github_user_id: Option, + github_email: Option<&str>, + ) -> Result> { + self.transaction(|tx| async move { + let tx = &*tx; + if let Some(github_user_id) = github_user_id { + if let Some(user_by_github_user_id) = user::Entity::find() + .filter(user::Column::GithubUserId.eq(github_user_id)) + .one(tx) + .await? + { + let mut user_by_github_user_id = user_by_github_user_id.into_active_model(); + user_by_github_user_id.github_login = ActiveValue::set(github_login.into()); + Ok(Some(user_by_github_user_id.update(tx).await?)) + } else if let Some(user_by_github_login) = user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(tx) + .await? + { + let mut user_by_github_login = user_by_github_login.into_active_model(); + user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id)); + Ok(Some(user_by_github_login.update(tx).await?)) + } else { + let user = user::Entity::insert(user::ActiveModel { + email_address: ActiveValue::set(github_email.map(|email| email.into())), + github_login: ActiveValue::set(github_login.into()), + github_user_id: ActiveValue::set(Some(github_user_id)), + admin: ActiveValue::set(false), + invite_count: ActiveValue::set(0), + invite_code: ActiveValue::set(None), + metrics_id: ActiveValue::set(Uuid::new_v4()), + ..Default::default() + }) + .exec_with_returning(&*tx) + .await?; + Ok(Some(user)) + } + } else { + Ok(user::Entity::find() + .filter(user::Column::GithubLogin.eq(github_login)) + .one(tx) + .await?) + } + }) + .await + } + + pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { + self.transaction(|tx| async move { + Ok(user::Entity::find() + .order_by_asc(user::Column::GithubLogin) + .limit(limit as u64) + .offset(page as u64 * limit as u64) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn get_users_with_no_invites( + &self, + invited_by_another_user: bool, + ) -> Result> { + self.transaction(|tx| async move { + Ok(user::Entity::find() + .filter( + user::Column::InviteCount + .eq(0) + .and(if invited_by_another_user { + user::Column::InviterId.is_not_null() + } else { + user::Column::InviterId.is_null() + }), + ) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn get_user_metrics_id(&self, id: UserId) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + MetricsId, + } + + self.transaction(|tx| async move { + let metrics_id: Uuid = user::Entity::find_by_id(id) + .select_only() + .column(user::Column::MetricsId) + .into_values::<_, QueryAs>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("could not find user"))?; + Ok(metrics_id.to_string()) + }) + .await + } + + pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { + self.transaction(|tx| async move { + user::Entity::update_many() + .filter(user::Column::Id.eq(id)) + .set(user::ActiveModel { + admin: ActiveValue::set(is_admin), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { + self.transaction(|tx| async move { + user::Entity::update_many() + .filter(user::Column::Id.eq(id)) + .set(user::ActiveModel { + connected_once: ActiveValue::set(connected_once), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(()) + }) + .await + } + + pub async fn destroy_user(&self, id: UserId) -> Result<()> { + self.transaction(|tx| async move { + access_token::Entity::delete_many() + .filter(access_token::Column::UserId.eq(id)) + .exec(&*tx) + .await?; + user::Entity::delete_by_id(id).exec(&*tx).await?; + Ok(()) + }) + .await + } + + pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { + self.transaction(|tx| async { + let tx = tx; + let like_string = Self::fuzzy_like_string(name_query); + let query = " + SELECT users.* + FROM users + WHERE github_login ILIKE $1 + ORDER BY github_login <-> $2 + LIMIT $3 + "; + + Ok(user::Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + self.pool.get_database_backend(), + query.into(), + vec![like_string.into(), name_query.into(), limit.into()], + )) + .all(&*tx) + .await?) + }) + .await + } + + pub fn fuzzy_like_string(string: &str) -> String { + let mut result = String::with_capacity(string.len() * 2 + 1); + for c in string.chars() { + if c.is_alphanumeric() { + result.push('%'); + result.push(c); + } + } + result.push('%'); + result + } +} From 5a356a47103fa5912fbe43853147583e28b6e44e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 17:38:18 -0700 Subject: [PATCH 312/326] Remove potential osascript hijacking --- crates/install_cli/src/install_cli.rs | 2 +- crates/zed/resources/zed.entitlements | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index adf50586d7..5d16054182 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -29,7 +29,7 @@ pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { // The symlink could not be created, so use osascript with admin privileges // to create it. - let status = smol::process::Command::new("osascript") + let status = smol::process::Command::new("/usr/bin/osascript") .args([ "-e", &format!( diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements index 07af3124cd..f40a8a253a 100644 --- a/crates/zed/resources/zed.entitlements +++ b/crates/zed/resources/zed.entitlements @@ -18,11 +18,7 @@ com.apple.security.personal-information.photos-library - com.apple.security.cs.allow-dyld-environment-variables - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-library-validation - + From d22caf5a196ced489fa1454856a9e3c8578bb524 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 17:59:06 -0700 Subject: [PATCH 313/326] Fix erorr in revert --- crates/gpui/src/elements/component.rs | 39 ++++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index c2158356e9..e2770c0148 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -105,22 +105,24 @@ impl Component for ElementAdapter { } // Component -> Element -pub struct ComponentAdapter { +pub struct ComponentAdapter { component: Option, + element: Option>, phantom: PhantomData, } -impl ComponentAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), + element: None, phantom: PhantomData, } } } impl + 'static> Element for ComponentAdapter { - type LayoutState = AnyElement; + type LayoutState = (); type PaintState = (); @@ -130,10 +132,16 @@ impl + 'static> Element for ComponentAdapter { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let component = self.component.take().unwrap(); - let mut element = component.render(view, cx.view_context()); - let constraint = element.layout(constraint, view, cx); - (constraint, element) + if self.element.is_none() { + let element = self + .component + .take() + .expect("Component can only be rendered once") + .render(view, cx.view_context()); + self.element = Some(element); + } + let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx); + (constraint, ()) } fn paint( @@ -141,11 +149,14 @@ impl + 'static> Element for ComponentAdapter { scene: &mut SceneBuilder, bounds: RectF, visible_bounds: RectF, - layout: &mut Self::LayoutState, + _: &mut Self::LayoutState, view: &mut V, cx: &mut PaintContext, ) -> Self::PaintState { - layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + self.element + .as_mut() + .expect("Layout should always be called before paint") + .paint(scene, bounds.origin(), visible_bounds, view, cx) } fn rect_for_text_range( @@ -153,25 +164,27 @@ impl + 'static> Element for ComponentAdapter { range_utf16: std::ops::Range, _: RectF, _: RectF, - element: &Self::LayoutState, + _: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> Option { - element.rect_for_text_range(range_utf16, view, cx) + self.element + .as_ref() + .and_then(|el| el.rect_for_text_range(range_utf16, view, cx)) } fn debug( &self, _: RectF, - element: &Self::LayoutState, + _: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> serde_json::Value { serde_json::json!({ "type": "ComponentAdapter", - "child": element.debug(view, cx), + "child": self.element.as_ref().map(|el| el.debug(view, cx)), }) } } From b7e03507c293c49c79a91c5ff78a061cc2408c6d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 19 Aug 2023 04:30:49 -0700 Subject: [PATCH 314/326] Update components.rs --- crates/theme/src/components.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fce7ad825c..8a0d8527a4 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -175,8 +175,13 @@ pub mod action_button { .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { - cx.window() - .dispatch_action(cx.view_id(), action.as_ref(), cx); + let window = cx.window(); + let view = cx.view_id(); + let action = action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action(view, action.as_ref(), &mut cx) + }) + .detach(); } }) .with_cursor_style(CursorStyle::PointingHand) From 9ea0ed3e95f81589e535de626cf013568759c232 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:07:31 +0200 Subject: [PATCH 315/326] chore: remove unused features from rodio crate. (#2869) This reduces our dep count by 1% at the expense of not supporting playback of .flac, .mp3 and .vorbis formats. We only use .wav anyways. Release Notes: - N/A --- crates/audio/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 182e421eb8..36135a1e76 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -13,7 +13,7 @@ gpui = { path = "../gpui" } collections = { path = "../collections" } util = { path = "../util" } -rodio = "0.17.1" +rodio ={version = "0.17.1", default-features=false, features = ["wav"]} log.workspace = true From 1cae4758ccfdc2486da79082943e283fed4ec131 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 21 Aug 2023 11:29:45 +0200 Subject: [PATCH 316/326] manage for edge case in which file documents are larger than the allowable limit --- crates/semantic_index/src/db.rs | 38 +++++++++-------- crates/semantic_index/src/semantic_index.rs | 47 +++++++++++++++++++-- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index e8c929c995..e57a5d733f 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -156,25 +156,27 @@ impl VectorDatabase { mtime: SystemTime, documents: Vec, ) -> Result<()> { - // Write to files table, and return generated id. - self.db.execute( - " - DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2; - ", - params![worktree_id, path.to_str()], - )?; + // Return the existing ID, if both the file and mtime match let mtime = Timestamp::from(mtime); - self.db.execute( - " - INSERT INTO files - (worktree_id, relative_path, mtime_seconds, mtime_nanos) - VALUES - (?1, ?2, $3, $4); - ", - params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos], - )?; - - let file_id = self.db.last_insert_rowid(); + let mut existing_id_query = self.db.prepare("SELECT id FROM files WHERE worktree_id = ?1 AND relative_path = ?2 AND mtime_seconds = ?3 AND mtime_nanos = ?4")?; + let existing_id = existing_id_query + .query_row( + params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos], + |row| Ok(row.get::<_, i64>(0)?), + ) + .map_err(|err| anyhow!(err)); + let file_id = if existing_id.is_ok() { + // If already exists, just return the existing id + existing_id.unwrap() + } else { + // Delete Existing Row + self.db.execute( + "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;", + params![worktree_id, path.to_str()], + )?; + self.db.execute("INSERT INTO files (worktree_id, relative_path, mtime_seconds, mtime_nanos) VALUES (?1, ?2, ?3, ?4);", params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos])?; + self.db.last_insert_rowid() + }; // Currently inserting at approximately 3400 documents a second // I imagine we can speed this up with a bulk insert of some kind. diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 8c9877b9d3..dd53215203 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -96,6 +96,7 @@ struct ProjectState { _outstanding_job_count_tx: Arc>>, } +#[derive(Clone)] struct JobHandle { tx: Weak>>, } @@ -389,6 +390,7 @@ impl SemanticIndex { embeddings_queue: &mut Vec<(i64, Vec, PathBuf, SystemTime, JobHandle)>, embed_batch_tx: &channel::Sender, PathBuf, SystemTime, JobHandle)>>, ) { + // Handle edge case where individual file has more documents than max batch size let should_flush = match job { EmbeddingJob::Enqueue { documents, @@ -397,9 +399,43 @@ impl SemanticIndex { mtime, job_handle, } => { - *queue_len += &documents.len(); - embeddings_queue.push((worktree_id, documents, path, mtime, job_handle)); - *queue_len >= EMBEDDINGS_BATCH_SIZE + // If documents is greater than embeddings batch size, recursively batch existing rows. + if &documents.len() > &EMBEDDINGS_BATCH_SIZE { + let first_job = EmbeddingJob::Enqueue { + documents: documents[..EMBEDDINGS_BATCH_SIZE].to_vec(), + worktree_id, + path: path.clone(), + mtime, + job_handle: job_handle.clone(), + }; + + Self::enqueue_documents_to_embed( + first_job, + queue_len, + embeddings_queue, + embed_batch_tx, + ); + + let second_job = EmbeddingJob::Enqueue { + documents: documents[EMBEDDINGS_BATCH_SIZE..].to_vec(), + worktree_id, + path: path.clone(), + mtime, + job_handle: job_handle.clone(), + }; + + Self::enqueue_documents_to_embed( + second_job, + queue_len, + embeddings_queue, + embed_batch_tx, + ); + return; + } else { + *queue_len += &documents.len(); + embeddings_queue.push((worktree_id, documents, path, mtime, job_handle)); + *queue_len >= EMBEDDINGS_BATCH_SIZE + } } EmbeddingJob::Flush => true, }; @@ -796,7 +832,10 @@ impl Drop for JobHandle { fn drop(&mut self) { if let Some(tx) = self.tx.upgrade() { let mut tx = tx.lock(); - *tx.borrow_mut() -= 1; + // Manage for overflow, cause we are cloning the Job Handle + if *tx.borrow() > 0 { + *tx.borrow_mut() -= 1; + }; } } } From 267c0b9a3ff4ad606ae0a8a3a9edaa1f9cff5463 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:33:06 +0200 Subject: [PATCH 317/326] chore: Update Cargo.lock following rodio PR (#2870) That's merely a follow-up to a previous PR. Release Notes: - N/A --- Cargo.lock | 79 ------------------------------------------------------ 1 file changed, 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aab5504a86..3edf9acab3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,12 +1312,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - [[package]] name = "cli" version = "0.1.0" @@ -3890,17 +3884,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libc" version = "0.2.147" @@ -4754,15 +4737,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.18.0" @@ -6008,11 +5982,8 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" dependencies = [ - "claxon", "cpal", "hound", - "lewton", - "symphonia", ] [[package]] @@ -7328,56 +7299,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "symphonia" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" -dependencies = [ - "lazy_static", - "symphonia-bundle-mp3", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" -dependencies = [ - "bitflags 1.3.2", - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-core" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" -dependencies = [ - "arrayvec 0.7.4", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - [[package]] name = "syn" version = "1.0.109" From def215af9f4cebbabf2eba1663844b4a201ea2ae Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 21 Aug 2023 12:47:43 +0200 Subject: [PATCH 318/326] update job handle to ensure file count is consistent Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 45 +++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index dd53215203..7aea0f7cfe 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -98,9 +98,16 @@ struct ProjectState { #[derive(Clone)] struct JobHandle { - tx: Weak>>, + tx: Arc>>>, } +impl JobHandle { + fn new(tx: &Arc>>) -> Self { + Self { + tx: Arc::new(Arc::downgrade(&tx)), + } + } +} impl ProjectState { fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { self.worktree_db_ids @@ -651,7 +658,7 @@ impl SemanticIndex { count += 1; *job_count_tx.lock().borrow_mut() += 1; let job_handle = JobHandle { - tx: Arc::downgrade(&job_count_tx), + tx: Arc::new(Arc::downgrade(&job_count_tx)), }; parsing_files_tx .try_send(PendingFile { @@ -726,6 +733,7 @@ impl SemanticIndex { let database_url = self.database_url.clone(); let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { + let t0 = Instant::now(); let database = VectorDatabase::new(fs.clone(), database_url.clone()).await?; let phrase_embedding = embedding_provider @@ -735,6 +743,11 @@ impl SemanticIndex { .next() .unwrap(); + log::trace!( + "Embedding search phrase took: {:?} milliseconds", + t0.elapsed().as_millis() + ); + let file_ids = database.retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)?; @@ -809,6 +822,11 @@ impl SemanticIndex { let buffers = futures::future::join_all(tasks).await; + log::trace!( + "Semantic Searching took: {:?} milliseconds in total", + t0.elapsed().as_millis() + ); + Ok(buffers .into_iter() .zip(ranges) @@ -830,12 +848,25 @@ impl Entity for SemanticIndex { impl Drop for JobHandle { fn drop(&mut self) { - if let Some(tx) = self.tx.upgrade() { - let mut tx = tx.lock(); - // Manage for overflow, cause we are cloning the Job Handle - if *tx.borrow() > 0 { + if let Some(inner) = Arc::get_mut(&mut self.tx) { + if let Some(tx) = inner.upgrade() { + let mut tx = tx.lock(); *tx.borrow_mut() -= 1; - }; + } } } } + +#[cfg(test)] +mod tests { + + use super::*; + #[test] + fn test_job_handle() { + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let tx = Arc::new(Mutex::new(job_count_tx)); + let job_handle = JobHandle::new(tx); + + assert_eq!(1, *job_count_rx.borrow_mut()); + } +} From 1a88444f2f52876eee4355a71cd7e6901850623b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:00:56 +0200 Subject: [PATCH 319/326] Increment job counter on JobClient::new Co-authored-by: Kyle --- crates/semantic_index/src/semantic_index.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 7aea0f7cfe..4457f55b7c 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -103,6 +103,7 @@ struct JobHandle { impl JobHandle { fn new(tx: &Arc>>) -> Self { + *tx.lock().borrow_mut() += 1; Self { tx: Arc::new(Arc::downgrade(&tx)), } @@ -656,10 +657,8 @@ impl SemanticIndex { if !already_stored { count += 1; - *job_count_tx.lock().borrow_mut() += 1; - let job_handle = JobHandle { - tx: Arc::new(Arc::downgrade(&job_count_tx)), - }; + + let job_handle = JobHandle::new(&job_count_tx); parsing_files_tx .try_send(PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], @@ -865,8 +864,14 @@ mod tests { fn test_job_handle() { let (job_count_tx, job_count_rx) = watch::channel_with(0); let tx = Arc::new(Mutex::new(job_count_tx)); - let job_handle = JobHandle::new(tx); + let job_handle = JobHandle::new(&tx); - assert_eq!(1, *job_count_rx.borrow_mut()); + assert_eq!(1, *job_count_rx.borrow()); + let new_job_handle = job_handle.clone(); + assert_eq!(1, *job_count_rx.borrow()); + drop(job_handle); + assert_eq!(1, *job_count_rx.borrow()); + drop(new_job_handle); + assert_eq!(0, *job_count_rx.borrow()); } } From 61041b0cd1ed354f4a04c72d077adcf0ce68e567 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:23:11 +0200 Subject: [PATCH 320/326] Do not attempt to reindex a file if previous attempts have failed. Add doc comment to JobHandle Co-authored-by: Kyle --- crates/semantic_index/src/semantic_index.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 4457f55b7c..2534988196 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -98,6 +98,8 @@ struct ProjectState { #[derive(Clone)] struct JobHandle { + /// The outer Arc is here to count the clones of a JobHandle instance; + /// when the last handle to a given job is dropped, we decrement a counter (just once). tx: Arc>>>, } @@ -389,6 +391,20 @@ impl SemanticIndex { .await .unwrap(); } + } else { + // Insert the file in spite of failure so that future attempts to index it do not take place (unless the file is changed). + for (worktree_id, documents, path, mtime, job_handle) in embeddings_queue.into_iter() { + db_update_tx + .send(DbOperation::InsertFile { + worktree_id, + documents: vec![], + path, + mtime, + job_handle, + }) + .await + .unwrap(); + } } } @@ -848,6 +864,7 @@ impl Entity for SemanticIndex { impl Drop for JobHandle { fn drop(&mut self) { if let Some(inner) = Arc::get_mut(&mut self.tx) { + // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not) if let Some(tx) = inner.upgrade() { let mut tx = tx.lock(); *tx.borrow_mut() -= 1; From 67a48ec1068e2ca1532180913178ae985a8b31f6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:30:32 +0200 Subject: [PATCH 321/326] project_search: use search history's current entry as a tab name. Previously the tab name for Semantic Search was not updated, as we didn't have an active query to go off of Co-authored-by: Kyle --- crates/search/src/project_search.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index dada928d6e..196d5589f4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -499,10 +499,14 @@ impl Item for ProjectSearchView { .with_margin_right(tab_theme.spacing), ) .with_child({ - let tab_name: Option> = - self.model.read(cx).active_query.as_ref().map(|query| { - let query_text = - util::truncate_and_trailoff(query.as_str(), MAX_TAB_TITLE_LEN); + let tab_name: Option> = self + .model + .read(cx) + .search_history + .current() + .as_ref() + .map(|query| { + let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); query_text.into() }); Label::new( From c68b518aecc119b36886096d49bfe04b96ced74a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Aug 2023 13:31:45 +0200 Subject: [PATCH 322/326] chore: fix compiler warning Co-authored-by: Kyle --- crates/semantic_index/src/semantic_index.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 2534988196..5aaecac733 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -393,7 +393,7 @@ impl SemanticIndex { } } else { // Insert the file in spite of failure so that future attempts to index it do not take place (unless the file is changed). - for (worktree_id, documents, path, mtime, job_handle) in embeddings_queue.into_iter() { + for (worktree_id, _, path, mtime, job_handle) in embeddings_queue.into_iter() { db_update_tx .send(DbOperation::InsertFile { worktree_id, From f66e6863fa24d28ea012f47fef6138c96ddee4db Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 21 Aug 2023 13:50:04 -0400 Subject: [PATCH 323/326] Add channel id to call events Co-Authored-By: Max Brunsfeld --- crates/call/src/call.rs | 18 +++++++++++++++--- crates/client/src/telemetry.rs | 1 + crates/collab_ui/src/collab_ui.rs | 11 ++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 3ac29bfc85..5fef53fa81 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -274,7 +274,7 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room("decline incoming", call.room_id, &self.client, cx); + Self::report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -406,19 +406,31 @@ impl ActiveCall { fn report_call_event(&self, operation: &'static str, cx: &AppContext) { if let Some(room) = self.room() { - Self::report_call_event_for_room(operation, room.read(cx).id(), &self.client, cx) + let room = room.read(cx); + Self::report_call_event_for_room( + operation, + room.id(), + room.channel_id(), + &self.client, + cx, + ) } } pub fn report_call_event_for_room( operation: &'static str, room_id: u64, + channel_id: Option, client: &Arc, cx: &AppContext, ) { let telemetry = client.telemetry(); let telemetry_settings = *settings::get::(cx); - let event = ClickhouseEvent::Call { operation, room_id }; + let event = ClickhouseEvent::Call { + operation, + room_id, + channel_id, + }; telemetry.report_clickhouse_event(event, telemetry_settings); } } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index dc5154d96f..48886377ba 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -74,6 +74,7 @@ pub enum ClickhouseEvent { Call { operation: &'static str, room_id: u64, + channel_id: Option, }, } diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 0fed2e0ef9..5420dd1db5 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -49,6 +49,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { ActiveCall::report_call_event_for_room( "disable screen share", room.id(), + room.channel_id(), &client, cx, ); @@ -57,6 +58,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { ActiveCall::report_call_event_for_room( "enable screen share", room.id(), + room.channel_id(), &client, cx, ); @@ -73,11 +75,18 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { let client = call.client(); room.update(cx, |room, cx| { if room.is_muted(cx) { - ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx); + ActiveCall::report_call_event_for_room( + "enable microphone", + room.id(), + room.channel_id(), + &client, + cx, + ); } else { ActiveCall::report_call_event_for_room( "disable microphone", room.id(), + room.channel_id(), &client, cx, ); From a836f9c23d6566ad21e9aacf6e92612419196a3a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 22 Aug 2023 02:55:27 -0400 Subject: [PATCH 324/326] Add a `default_open_ai_model` setting for the assistant (#2876) [This PR has been sitting around for a bit](https://github.com/zed-industries/zed/pull/2845). I received a bit of mixed opinions from the team on how this setting should work, if it should use the full model names or some simpler form of it, etc. I went ahead and made the decision to do the following: - Use the full model names in settings - ex: `gpt-4-0613` - Default to `gpt-4-0613` when no setting is present - Save the full model names in the conversation history files (this is how it was prior) - ex: `gpt-4-0613` - Display the shortened model names in the assistant - ex: `gpt-4` - Not worry about adding an option to add custom models (can add in a follow-up PR) - Not query what models are available to the user via their api key (can add in a follow-up PR) Release Notes: - Added a `default_open_ai_model` setting for the assistant (defaults to `gpt-4-0613`). --------- Co-authored-by: Mikayla --- assets/settings/default.json | 8 ++++- crates/ai/src/ai.rs | 3 +- crates/ai/src/assistant.rs | 45 +++++++++++++++++---------- crates/ai/src/assistant_settings.rs | 33 ++++++++++++++++++++ crates/settings/src/settings_store.rs | 3 +- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 08faedbed6..24412b883b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -138,7 +138,13 @@ // Default width when the assistant is docked to the left or right. "default_width": 640, // Default height when the assistant is docked to the bottom. - "default_height": 320 + "default_height": 320, + // The default OpenAI model to use when starting new conversations. This + // setting can take two values: + // + // 1. "gpt-3.5-turbo-0613"" + // 2. "gpt-4-0613"" + "default_open_ai_model": "gpt-4-0613" }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 7cc5f08f7c..d2be651bd5 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -3,6 +3,7 @@ mod assistant_settings; use anyhow::Result; pub use assistant::AssistantPanel; +use assistant_settings::OpenAIModel; use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; @@ -60,7 +61,7 @@ struct SavedConversation { messages: Vec, message_metadata: HashMap, summary: String, - model: String, + model: OpenAIModel, } impl SavedConversation { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e5026182ed..81299bbdc2 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,5 +1,5 @@ use crate::{ - assistant_settings::{AssistantDockPosition, AssistantSettings}, + assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; @@ -833,7 +833,7 @@ struct Conversation { pending_summary: Task>, completion_count: usize, pending_completions: Vec, - model: String, + model: OpenAIModel, token_count: Option, max_token_count: usize, pending_token_count: Task>, @@ -853,7 +853,6 @@ impl Conversation { language_registry: Arc, cx: &mut ModelContext, ) -> Self { - let model = "gpt-3.5-turbo-0613"; let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); @@ -872,6 +871,9 @@ impl Conversation { buffer }); + let settings = settings::get::(cx); + let model = settings.default_open_ai_model.clone(); + let mut this = Self { message_anchors: Default::default(), messages_metadata: Default::default(), @@ -881,9 +883,9 @@ impl Conversation { completion_count: Default::default(), pending_completions: Default::default(), token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(model), + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), pending_token_count: Task::ready(None), - model: model.into(), + model: model.clone(), _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], pending_save: Task::ready(Ok(())), path: None, @@ -977,7 +979,7 @@ impl Conversation { completion_count: Default::default(), pending_completions: Default::default(), token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(&model), + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), pending_token_count: Task::ready(None), model, _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], @@ -1031,13 +1033,16 @@ impl Conversation { cx.background().timer(Duration::from_millis(200)).await; let token_count = cx .background() - .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) }) + .spawn(async move { + tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) + }) .await?; this.upgrade(&cx) .ok_or_else(|| anyhow!("conversation was dropped"))? .update(&mut cx, |this, cx| { - this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); this.token_count = Some(token_count); cx.notify() }); @@ -1051,7 +1056,7 @@ impl Conversation { Some(self.max_token_count as isize - self.token_count? as isize) } - fn set_model(&mut self, model: String, cx: &mut ModelContext) { + fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext) { self.model = model; self.count_remaining_tokens(cx); cx.notify(); @@ -1093,7 +1098,7 @@ impl Conversation { } } else { let request = OpenAIRequest { - model: self.model.clone(), + model: self.model.full_name().to_string(), messages: self .messages(cx) .filter(|message| matches!(message.status, MessageStatus::Done)) @@ -1419,7 +1424,7 @@ impl Conversation { .into(), })); let request = OpenAIRequest { - model: self.model.clone(), + model: self.model.full_name().to_string(), messages: messages.collect(), stream: true, }; @@ -2023,11 +2028,8 @@ impl ConversationEditor { fn cycle_model(&mut self, cx: &mut ViewContext) { self.conversation.update(cx, |conversation, cx| { - let new_model = match conversation.model.as_str() { - "gpt-4-0613" => "gpt-3.5-turbo-0613", - _ => "gpt-4-0613", - }; - conversation.set_model(new_model.into(), cx); + let new_model = conversation.model.cycle(); + conversation.set_model(new_model, cx); }); } @@ -2049,7 +2051,8 @@ impl ConversationEditor { MouseEventHandler::new::(0, cx, |state, cx| { let style = style.model.style_for(state); - Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) + let model_display_name = self.conversation.read(cx).model.short_name(); + Label::new(model_display_name, style.text.clone()) .contained() .with_style(style.container) }) @@ -2238,6 +2241,8 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = conversation.read(cx).buffer.clone(); @@ -2364,6 +2369,8 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = conversation.read(cx).buffer.clone(); @@ -2458,6 +2465,8 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = conversation.read(cx).buffer.clone(); @@ -2538,6 +2547,8 @@ mod tests { #[gpui::test] fn test_serialization(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); let registry = Arc::new(LanguageRegistry::test()); let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry.clone(), cx)); diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs index 04ba8fb946..05d8d9ffeb 100644 --- a/crates/ai/src/assistant_settings.rs +++ b/crates/ai/src/assistant_settings.rs @@ -3,6 +3,37 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Setting; +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub enum OpenAIModel { + #[serde(rename = "gpt-3.5-turbo-0613")] + ThreePointFiveTurbo, + #[serde(rename = "gpt-4-0613")] + Four, +} + +impl OpenAIModel { + pub fn full_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", + OpenAIModel::Four => "gpt-4-0613", + } + } + + pub fn short_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", + OpenAIModel::Four => "gpt-4", + } + } + + pub fn cycle(&self) -> Self { + match self { + OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, + OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AssistantDockPosition { @@ -17,6 +48,7 @@ pub struct AssistantSettings { pub dock: AssistantDockPosition, pub default_width: f32, pub default_height: f32, + pub default_open_ai_model: OpenAIModel, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -25,6 +57,7 @@ pub struct AssistantSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, + pub default_open_ai_model: Option, } impl Setting for AssistantSettings { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 1188018cd8..da84074d2a 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; use gpui::AppContext; use lazy_static::lazy_static; @@ -162,6 +162,7 @@ impl SettingsStore { if let Some(setting) = setting_value .load_setting(&default_settings, &user_values_stack, cx) + .context("A default setting must be added to the `default.json` file") .log_err() { setting_value.set_global_value(setting); From d27cebd97718783df3e940391f70b2c7eaf06d11 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:35:20 +0200 Subject: [PATCH 325/326] Z 2819 (#2872) This PR adds new config option to language config called `word_boundaries` that controls which characters should be recognised as word boundary for a given language. This will improve our UX for languages such as PHP and Tailwind. Release Notes: - Improved completions for PHP [#1820](https://github.com/zed-industries/community/issues/1820) --------- Co-authored-by: Julia Risley --- crates/editor/src/editor.rs | 1 - crates/editor/src/items.rs | 29 +++++++++++++---------- crates/editor/src/movement.rs | 30 +++++++++++++++++++----- crates/editor/src/multi_buffer.rs | 11 +++++---- crates/language/src/buffer.rs | 25 +++++++++++++------- crates/language/src/language.rs | 5 +++- crates/project/src/project.rs | 2 +- crates/project/src/search.rs | 27 ++++++++++++++++----- crates/vim/src/motion.rs | 18 ++++++++------ crates/vim/src/normal/change.rs | 9 ++++--- crates/vim/src/object.rs | 21 ++++++++++------- crates/zed/src/languages/php/config.toml | 1 + 12 files changed, 120 insertions(+), 59 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a38145f48c..cbc7a7cd42 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2667,7 +2667,6 @@ 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); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b99977a60e..4a2b03bbdf 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1028,7 +1028,7 @@ impl SearchableItem for Editor { if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { ranges.extend( query - .search(excerpt_buffer.as_rope()) + .search(excerpt_buffer, None) .await .into_iter() .map(|range| { @@ -1038,17 +1038,22 @@ impl SearchableItem for Editor { } else { for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer); - let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone()); - ranges.extend(query.search(&rope).await.into_iter().map(|range| { - let start = excerpt - .buffer - .anchor_after(excerpt_range.start + range.start); - let end = excerpt - .buffer - .anchor_before(excerpt_range.start + range.end); - buffer.anchor_in_excerpt(excerpt.id.clone(), start) - ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) - })); + ranges.extend( + query + .search(&excerpt.buffer, Some(excerpt_range.clone())) + .await + .into_iter() + .map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + }), + ); } } ranges diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 4eec92c8eb..6b3032b2a3 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -176,14 +176,21 @@ pub fn line_end( } pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); + find_preceding_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n' + (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) + || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_preceding_boundary(map, point, |left, right| { - let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace(); + let is_word_start = + char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); let is_subword_start = left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start || left == '\n' @@ -191,14 +198,20 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis } pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n' + (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) + || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); find_boundary(map, point, |left, right| { - let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace(); + let is_word_end = + (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); let is_subword_end = left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_end || is_subword_end || right == '\n' @@ -385,10 +398,15 @@ pub fn find_boundary_in_line( } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { + let raw_point = point.to_point(map); + let language = map.buffer_snapshot.language_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(char_kind); - let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind); + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c)); + let prev_char_kind = text + .reversed_chars_at(ix) + .next() + .map(|c| char_kind(language, c)); prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8417c411f2..d4061f25dc 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1865,13 +1865,16 @@ impl MultiBufferSnapshot { let mut end = start; let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); + + let language = self.language_at(start); + let kind = |c| char_kind(language, c); let word_kind = cmp::max( - prev_chars.peek().copied().map(char_kind), - next_chars.peek().copied().map(char_kind), + prev_chars.peek().copied().map(kind), + next_chars.peek().copied().map(kind), ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -1879,7 +1882,7 @@ impl MultiBufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e6ad3469ea..d032e8e025 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2192,13 +2192,16 @@ impl BufferSnapshot { let mut end = start; let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); + + let language = self.language_at(start); + let kind = |c| char_kind(language, c); let word_kind = cmp::max( - prev_chars.peek().copied().map(char_kind), - next_chars.peek().copied().map(char_kind), + prev_chars.peek().copied().map(kind), + next_chars.peek().copied().map(kind), ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -2206,7 +2209,7 @@ impl BufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind && ch != '\n' { + if Some(kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; @@ -3003,14 +3006,18 @@ pub fn contiguous_ranges( }) } -pub fn char_kind(c: char) -> CharKind { +pub fn char_kind(language: Option<&Arc>, c: char) -> CharKind { if c.is_whitespace() { - CharKind::Whitespace + return CharKind::Whitespace; } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation + return CharKind::Word; } + if let Some(language) = language { + if language.config.word_characters.contains(&c) { + return CharKind::Word; + } + } + CharKind::Punctuation } /// Find all of the ranges of whitespace that occur at the ends of lines diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 223f5679ae..82245d67ca 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -11,7 +11,7 @@ mod buffer_tests; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use collections::HashMap; +use collections::{HashMap, HashSet}; use futures::{ channel::oneshot, future::{BoxFuture, Shared}, @@ -344,6 +344,8 @@ pub struct LanguageConfig { pub block_comment: Option<(Arc, Arc)>, #[serde(default)] pub overrides: HashMap, + #[serde(default)] + pub word_characters: HashSet, } #[derive(Debug, Default)] @@ -411,6 +413,7 @@ impl Default for LanguageConfig { block_comment: Default::default(), overrides: Default::default(), collapsed_placeholder: Default::default(), + word_characters: Default::default(), } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index adb067b403..b120baa951 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5180,7 +5180,7 @@ impl Project { snapshot.file().map(|file| file.path().as_ref()), ) { query - .search(snapshot.as_rope()) + .search(&snapshot, None) .await .iter() .map(|range| { diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 08ff803598..a3c6583052 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -3,7 +3,7 @@ use anyhow::{Context, Result}; use client::proto; use globset::{Glob, GlobMatcher}; use itertools::Itertools; -use language::{char_kind, Rope}; +use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; use std::{ @@ -39,6 +39,7 @@ pub enum SearchQuery { case_sensitive: bool, inner: SearchInputs, }, + Regex { regex: Regex, @@ -214,12 +215,24 @@ impl SearchQuery { } } - pub async fn search(&self, rope: &Rope) -> Vec> { + pub async fn search( + &self, + buffer: &BufferSnapshot, + subrange: Option>, + ) -> Vec> { const YIELD_INTERVAL: usize = 20000; if self.as_str().is_empty() { return Default::default(); } + let language = buffer.language_at(0); + let rope = if let Some(range) = subrange { + buffer.as_rope().slice(range) + } else { + buffer.as_rope().clone() + }; + + let kind = |c| char_kind(language, c); let mut matches = Vec::new(); match self { @@ -236,10 +249,10 @@ impl SearchQuery { let mat = mat.unwrap(); if *whole_word { - let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind); + let start_kind = kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(kind); if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { continue; } @@ -247,6 +260,7 @@ impl SearchQuery { matches.push(mat.start()..mat.end()) } } + Self::Regex { regex, multiline, .. } => { @@ -284,6 +298,7 @@ impl SearchQuery { } } } + matches } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 13c22afdaa..8cd29e5e9f 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -439,11 +439,12 @@ pub(crate) fn next_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { + let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { let mut crossed_newline = false; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); let at_newline = right == '\n'; let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) @@ -463,11 +464,12 @@ fn next_word_end( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { + let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { *point.column_mut() += 1; point = movement::find_boundary(map, point, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); @@ -493,12 +495,13 @@ fn previous_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { + let language = map.buffer_snapshot.language_at(point.to_point(map)); for _ in 0..times { // This works even though find_preceding_boundary is called for every character in the line containing // cursor because the newline is checked only once. point = movement::find_preceding_boundary(map, point, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); (left_kind != right_kind && !right.is_whitespace()) || left == '\n' }); @@ -508,6 +511,7 @@ fn previous_word_start( fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { let mut last_point = DisplayPoint::new(from.row(), 0); + let language = map.buffer_snapshot.language_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { return from; @@ -515,7 +519,7 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi last_point = point; - if char_kind(ch) != CharKind::Whitespace { + if char_kind(language, ch) != CharKind::Whitespace { break; } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index d226c70410..50bc049a3a 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -82,16 +82,19 @@ fn expand_changed_word_selection( ignore_punctuation: bool, ) -> bool { if times.is_none() || times.unwrap() == 1 { + let language = map + .buffer_snapshot + .language_at(selection.start.to_point(map)); let in_word = map .chars_at(selection.head()) .next() - .map(|(c, _)| char_kind(c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) .unwrap_or_default(); if in_word { selection.end = movement::find_boundary(map, selection.end, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index c203a89f72..dd922e7af6 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -177,17 +177,18 @@ fn in_word( ignore_punctuation: bool, ) -> Option> { // Use motion::right so that we consider the character under the cursor when looking for the start + let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(left).coerce_punctuation(ignore_punctuation) - != char_kind(right).coerce_punctuation(ignore_punctuation) + char_kind(language, left).coerce_punctuation(ignore_punctuation) + != char_kind(language, right).coerce_punctuation(ignore_punctuation) }, ); let end = movement::find_boundary_in_line(map, relative_to, |left, right| { - char_kind(left).coerce_punctuation(ignore_punctuation) - != char_kind(right).coerce_punctuation(ignore_punctuation) + char_kind(language, left).coerce_punctuation(ignore_punctuation) + != char_kind(language, right).coerce_punctuation(ignore_punctuation) }); Some(start..end) @@ -210,10 +211,11 @@ fn around_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { + let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); let in_word = map .chars_at(relative_to) .next() - .map(|(c, _)| char_kind(c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) .unwrap_or(false); if in_word { @@ -237,20 +239,21 @@ fn around_next_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { + let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); // Get the start of the word let start = movement::find_preceding_boundary_in_line( map, right(map, relative_to, 1), |left, right| { - char_kind(left).coerce_punctuation(ignore_punctuation) - != char_kind(right).coerce_punctuation(ignore_punctuation) + char_kind(language, left).coerce_punctuation(ignore_punctuation) + != char_kind(language, right).coerce_punctuation(ignore_punctuation) }, ); let mut word_found = false; let end = movement::find_boundary(map, relative_to, |left, right| { - let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; diff --git a/crates/zed/src/languages/php/config.toml b/crates/zed/src/languages/php/config.toml index 19acb949e2..60dd233555 100644 --- a/crates/zed/src/languages/php/config.toml +++ b/crates/zed/src/languages/php/config.toml @@ -10,3 +10,4 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, ] collapsed_placeholder = "/* ... */" +word_characters = ["$"] From ebcb395f1f26d30b0dee86bd31be0f233ca8c417 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 22 Aug 2023 11:33:40 -0400 Subject: [PATCH 326/326] Add default bindings for new file and directory --- assets/keymaps/default.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 83875ab44a..1bd973e83b 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -543,6 +543,8 @@ "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", + "cmd-n": "project_panel::NewFile", + "alt-cmd-n": "project_panel::NewDirectory", "cmd-x": "project_panel::Cut", "cmd-c": "project_panel::Copy", "cmd-v": "project_panel::Paste",