diff --git a/Cargo.lock b/Cargo.lock index f2e86f509dd91047e6ba339550cf40a38a2ada71..be36a5cf78b1a7c74c7a9a9da53a427c54a1ce96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,25 @@ dependencies = [ "workspace", ] +[[package]] +name = "activity_indicator2" +version = "0.1.0" +dependencies = [ + "anyhow", + "auto_update2", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "project2", + "settings2", + "smallvec", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -1210,6 +1229,7 @@ dependencies = [ "fs2", "futures 0.3.28", "gpui2", + "image", "language2", "live_kit_client2", "log", @@ -1221,6 +1241,8 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "smallvec", + "ui2", "util", "workspace2", ] @@ -9485,6 +9507,27 @@ dependencies = [ "workspace", ] +[[package]] +name = "theme_selector2" +version = "0.1.0" +dependencies = [ + "editor2", + "feature_flags2", + "fs2", + "fuzzy2", + "gpui2", + "log", + "parking_lot 0.11.2", + "picker2", + "postage", + "settings2", + "smol", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "thiserror" version = "1.0.48" @@ -9944,7 +9987,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1" dependencies = [ "cc", "regex", @@ -10192,6 +10235,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-uiua" +version = "0.3.3" +source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-vue" version = "0.0.1" @@ -11059,6 +11111,31 @@ dependencies = [ "workspace", ] +[[package]] +name = "welcome2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "db2", + "editor2", + "fs2", + "fuzzy2", + "gpui2", + "install_cli2", + "log", + "picker2", + "project2", + "schemars", + "serde", + "settings2", + "theme2", + "theme_selector2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "which" version = "4.4.2" @@ -11513,7 +11590,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.115.0" +version = "0.116.0" dependencies = [ "activity_indicator", "ai", @@ -11630,6 +11707,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-uiua", "tree-sitter-vue", "tree-sitter-yaml", "unindent", @@ -11655,16 +11733,19 @@ dependencies = [ name = "zed2" version = "0.109.0" dependencies = [ + "activity_indicator2", "ai2", "anyhow", "async-compression", "async-recursion 0.3.2", "async-tar", "async-trait", + "audio2", "auto_update2", "backtrace", "breadcrumbs2", "call2", + "channel2", "chrono", "cli", "client2", @@ -11723,6 +11804,7 @@ dependencies = [ "terminal_view2", "text2", "theme2", + "theme_selector2", "thiserror", "tiny_http", "toml 0.5.11", @@ -11752,6 +11834,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-uiua", "tree-sitter-vue", "tree-sitter-yaml", "unindent", @@ -11759,6 +11842,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "welcome2", "workspace2", "zed_actions2", ] diff --git a/Cargo.toml b/Cargo.toml index 4a82d42e8af2ec5dcccc99c99538497353b3635e..d7d7165e159cae36cfa8498d1b2db16db1cc72e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/activity_indicator", + "crates/activity_indicator2", "crates/ai", "crates/assistant", "crates/audio", @@ -108,6 +109,7 @@ members = [ "crates/theme2", "crates/theme_importer", "crates/theme_selector", + "crates/theme_selector2", "crates/ui2", "crates/util", "crates/semantic_index", @@ -116,6 +118,7 @@ members = [ "crates/vcs_menu", "crates/workspace2", "crates/welcome", + "crates/welcome2", "crates/xtask", "crates/zed", "crates/zed2", @@ -196,8 +199,10 @@ tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} +tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} + [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" } 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/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg new file mode 100644 index 0000000000000000000000000000000000000000..7d78497e6d28f3088f035762c39e38ea9b0e9f7b --- /dev/null +++ b/assets/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg index 186c9c7457c48405508de337fa5d1904f2563f59..57ee7504906134de8662732207eeb261fc6359f2 100644 --- a/assets/icons/arrow_left.svg +++ b/assets/icons/arrow_left.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg index 7bae7f4801a10b0ee04dfab93048bbdaf526045a..7a5b1174eb5a98250be404e052f3e197d95756b8 100644 --- a/assets/icons/arrow_right.svg +++ b/assets/icons/arrow_right.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg new file mode 100644 index 0000000000000000000000000000000000000000..81dfee8042609ff2a2a8b26026cddb43557b2be2 --- /dev/null +++ b/assets/icons/arrow_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/command.svg b/assets/icons/command.svg new file mode 100644 index 0000000000000000000000000000000000000000..d38389aea4b08fb19d1641e8735c960de9073562 --- /dev/null +++ b/assets/icons/command.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/control.svg b/assets/icons/control.svg new file mode 100644 index 0000000000000000000000000000000000000000..94189dc07dbe761cd1cc71b22598e1d2a2869ed5 --- /dev/null +++ b/assets/icons/control.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/option.svg b/assets/icons/option.svg new file mode 100644 index 0000000000000000000000000000000000000000..9d54a6f34b1546bc7b1ed907f231eeb80f0c1264 --- /dev/null +++ b/assets/icons/option.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/return.svg b/assets/icons/return.svg new file mode 100644 index 0000000000000000000000000000000000000000..683519c3066fef61cb256a103dcd219fe59ff9f1 --- /dev/null +++ b/assets/icons/return.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/shift.svg b/assets/icons/shift.svg new file mode 100644 index 0000000000000000000000000000000000000000..02321147773469108283afbe65a68b303665f4d3 --- /dev/null +++ b/assets/icons/shift.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/activity_indicator2/Cargo.toml b/crates/activity_indicator2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..400869d2fd51a214e05162c364dbaf1d131d23a2 --- /dev/null +++ b/crates/activity_indicator2/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "activity_indicator2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/activity_indicator.rs" +doctest = false + +[dependencies] +auto_update = { path = "../auto_update2", package = "auto_update2" } +editor = { path = "../editor2", package = "editor2" } +language = { path = "../language2", package = "language2" } +gpui = { path = "../gpui2", package = "gpui2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +ui = { path = "../ui2", package = "ui2" } +util = { path = "../util" } +theme = { path = "../theme2", package = "theme2" } +workspace = { path = "../workspace2", package = "workspace2" } + +anyhow.workspace = true +futures.workspace = true +smallvec.workspace = true + +[dev-dependencies] +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } diff --git a/crates/activity_indicator2/src/activity_indicator.rs b/crates/activity_indicator2/src/activity_indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ee5a6689a54d8f27e6e1fd8d509c12c81624353 --- /dev/null +++ b/crates/activity_indicator2/src/activity_indicator.rs @@ -0,0 +1,333 @@ +use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; +use editor::Editor; +use futures::StreamExt; +use gpui::{ + actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model, + ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext as _, +}; +use language::{LanguageRegistry, LanguageServerBinaryStatus}; +use project::{LanguageServerProgress, Project}; +use smallvec::SmallVec; +use std::{cmp::Reverse, fmt::Write, sync::Arc}; +use ui::h_stack; +use util::ResultExt; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +actions!(ShowErrorMessage); + +const DOWNLOAD_ICON: &str = "icons/download.svg"; +const WARNING_ICON: &str = "icons/warning.svg"; + +pub enum Event { + ShowError { lsp_name: Arc, error: String }, +} + +pub struct ActivityIndicator { + statuses: Vec, + project: Model, + auto_updater: Option>, +} + +struct LspStatus { + name: Arc, + status: LanguageServerBinaryStatus, +} + +struct PendingWork<'a> { + language_server_name: &'a str, + progress_token: &'a str, + progress: &'a LanguageServerProgress, +} + +#[derive(Default)] +struct Content { + icon: Option<&'static str>, + message: String, + on_click: Option)>>, +} + +impl ActivityIndicator { + pub fn new( + workspace: &mut Workspace, + languages: Arc, + cx: &mut ViewContext, + ) -> View { + let project = workspace.project().clone(); + let auto_updater = AutoUpdater::get(cx); + let this = cx.build_view(|cx: &mut ViewContext| { + let mut status_events = languages.language_server_binary_statuses(); + cx.spawn(|this, mut cx| async move { + while let Some((language, event)) = status_events.next().await { + this.update(&mut cx, |this, cx| { + this.statuses.retain(|s| s.name != language.name()); + this.statuses.push(LspStatus { + name: language.name(), + status: event, + }); + cx.notify(); + })?; + } + anyhow::Ok(()) + }) + .detach(); + cx.observe(&project, |_, _, cx| cx.notify()).detach(); + + if let Some(auto_updater) = auto_updater.as_ref() { + cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); + } + + // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) + // .detach(); + + Self { + statuses: Default::default(), + project: project.clone(), + auto_updater, + } + }); + + cx.subscribe(&this, move |workspace, _, event, cx| match event { + Event::ShowError { lsp_name, error } => { + if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer(error, None, cx)) + .log_err() + { + buffer.update(cx, |buffer, cx| { + buffer.edit( + [(0..0, format!("Language server error: {}\n\n", lsp_name))], + None, + cx, + ); + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_buffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + } + } + }) + .detach(); + this + } + + fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext) { + self.statuses.retain(|status| { + if let LanguageServerBinaryStatus::Failed { error } = &status.status { + cx.emit(Event::ShowError { + lsp_name: status.name.clone(), + error: error.clone(), + }); + false + } else { + true + } + }); + + cx.notify(); + } + + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + if let Some(updater) = &self.auto_updater { + updater.update(cx, |updater, cx| { + updater.dismiss_error(cx); + }); + } + cx.notify(); + } + + fn pending_language_server_work<'a>( + &self, + cx: &'a AppContext, + ) -> impl Iterator> { + self.project + .read(cx) + .language_server_statuses() + .rev() + .filter_map(|status| { + if status.pending_work.is_empty() { + None + } else { + let mut pending_work = status + .pending_work + .iter() + .map(|(token, progress)| PendingWork { + language_server_name: status.name.as_str(), + progress_token: token.as_str(), + progress, + }) + .collect::>(); + pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); + Some(pending_work) + } + }) + .flatten() + } + + fn content_to_render(&mut self, cx: &mut ViewContext) -> Content { + // Show any language server has pending activity. + let mut pending_work = self.pending_language_server_work(cx); + if let Some(PendingWork { + language_server_name, + progress_token, + progress, + }) = pending_work.next() + { + let mut message = language_server_name.to_string(); + + message.push_str(": "); + if let Some(progress_message) = progress.message.as_ref() { + message.push_str(progress_message); + } else { + message.push_str(progress_token); + } + + if let Some(percentage) = progress.percentage { + write!(&mut message, " ({}%)", percentage).unwrap(); + } + + let additional_work_count = pending_work.count(); + if additional_work_count > 0 { + write!(&mut message, " + {} more", additional_work_count).unwrap(); + } + + return Content { + icon: None, + message, + on_click: None, + }; + } + + // Show any language server installation info. + let mut downloading = SmallVec::<[_; 3]>::new(); + let mut checking_for_update = SmallVec::<[_; 3]>::new(); + let mut failed = SmallVec::<[_; 3]>::new(); + for status in &self.statuses { + let name = status.name.clone(); + match status.status { + LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name), + LanguageServerBinaryStatus::Downloading => downloading.push(name), + LanguageServerBinaryStatus::Failed { .. } => failed.push(name), + LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} + } + } + + if !downloading.is_empty() { + return Content { + icon: Some(DOWNLOAD_ICON), + message: format!( + "Downloading {} language server{}...", + downloading.join(", "), + if downloading.len() > 1 { "s" } else { "" } + ), + on_click: None, + }; + } else if !checking_for_update.is_empty() { + return Content { + icon: Some(DOWNLOAD_ICON), + message: format!( + "Checking for updates to {} language server{}...", + checking_for_update.join(", "), + if checking_for_update.len() > 1 { + "s" + } else { + "" + } + ), + on_click: None, + }; + } else if !failed.is_empty() { + return Content { + icon: Some(WARNING_ICON), + message: format!( + "Failed to download {} language server{}. Click to show error.", + failed.join(", "), + if failed.len() > 1 { "s" } else { "" } + ), + on_click: Some(Arc::new(|this, cx| { + this.show_error_message(&Default::default(), cx) + })), + }; + } + + // Show any application auto-update info. + if let Some(updater) = &self.auto_updater { + return match &updater.read(cx).status() { + AutoUpdateStatus::Checking => Content { + icon: Some(DOWNLOAD_ICON), + message: "Checking for Zed updates…".to_string(), + on_click: None, + }, + AutoUpdateStatus::Downloading => Content { + icon: Some(DOWNLOAD_ICON), + message: "Downloading Zed update…".to_string(), + on_click: None, + }, + AutoUpdateStatus::Installing => Content { + icon: Some(DOWNLOAD_ICON), + message: "Installing Zed update…".to_string(), + on_click: None, + }, + AutoUpdateStatus::Updated => Content { + icon: None, + message: "Click to restart and update Zed".to_string(), + on_click: Some(Arc::new(|_, cx| { + workspace::restart(&Default::default(), cx) + })), + }, + AutoUpdateStatus::Errored => Content { + icon: Some(WARNING_ICON), + message: "Auto update failed".to_string(), + on_click: Some(Arc::new(|this, cx| { + this.dismiss_error_message(&Default::default(), cx) + })), + }, + AutoUpdateStatus::Idle => Default::default(), + }; + } + + // todo!(show active tasks) + // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { + // return Content { + // icon: None, + // message: most_recent_active_task.to_string(), + // on_click: None, + // }; + // } + + Default::default() + } +} + +impl EventEmitter for ActivityIndicator {} + +impl Render for ActivityIndicator { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let content = self.content_to_render(cx); + + let mut result = h_stack() + .id("activity-indicator") + .on_action(cx.listener(Self::show_error_message)) + .on_action(cx.listener(Self::dismiss_error_message)); + + if let Some(on_click) = content.on_click { + result = result + .cursor(CursorStyle::PointingHand) + .on_click(cx.listener(move |this, _, cx| { + on_click(this, cx); + })) + } + + result + .children(content.icon.map(|icon| svg().path(icon))) + .child(SharedString::from(content.message)) + } +} + +impl StatusItemView for ActivityIndicator { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} +} diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs index aeff68965fd07ce7eda4cc0aac9bb8a7aaeb4649..d2eab15d09967a84c5c11004ec70419795e0bf01 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting { pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { AutoUpdateSetting::register(cx); - cx.observe_new_views(|wokrspace: &mut Workspace, _cx| { - wokrspace + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace .register_action(|_, action: &Check, cx| check(action, cx)) .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| { let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]); @@ -94,6 +94,11 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo }) .detach(); }); + + // @nate - code to trigger update notification on launch + // workspace.show_notification(0, _cx, |cx| { + // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) + // }); }) .detach(); @@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) { } } -fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index e6a22b73248a8fce898c6871abb06d602a3a8e7a..4a2efcf8076bb882a67c1f25b6300dd4cd48c1d1 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -1,87 +1,56 @@ -use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext}; -use menu::Cancel; -use workspace::notifications::NotificationEvent; +use gpui::{ + div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render, + SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, +}; +use util::channel::ReleaseChannel; +use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; pub struct UpdateNotification { - _version: SemanticVersion, + version: SemanticVersion, } -impl EventEmitter for UpdateNotification {} +impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { type Element = Div; - fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { - div().child("Updated zed!") - // let theme = theme::current(cx).clone(); - // let theme = &theme.update_notification; - - // let app_name = cx.global::().display_name(); - - // MouseEventHandler::new::(0, cx, |state, cx| { - // Flex::column() - // .with_child( - // Flex::row() - // .with_child( - // Text::new( - // format!("Updated to {app_name} {}", self.version), - // theme.message.text.clone(), - // ) - // .contained() - // .with_style(theme.message.container) - // .aligned() - // .top() - // .left() - // .flex(1., true), - // ) - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = theme.dismiss_button.style_for(state); - // Svg::new("icons/x.svg") - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .contained() - // .with_style(style.container) - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // }) - // .with_padding(Padding::uniform(5.)) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.dismiss(&Default::default(), cx) - // }) - // .aligned() - // .constrained() - // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) - // .aligned() - // .top() - // .flex_float(), - // ), - // ) - // .with_child({ - // let style = theme.action_message.style_for(state); - // Text::new("View the release notes", style.text.clone()) - // .contained() - // .with_style(style.container) - // }) - // .contained() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // crate::view_release_notes(&Default::default(), cx) - // }) - // .into_any_named("update notification") + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + let app_name = cx.global::().display_name(); + + v_stack() + .elevation_3(cx) + .p_4() + .child( + h_stack() + .justify_between() + .child(Label::new(format!( + "Updated to {app_name} {}", + self.version + ))) + .child( + div() + .id("cancel") + .child(IconElement::new(Icon::Close)) + .cursor_pointer() + .on_click(cx.listener(|this, _, cx| this.dismiss(cx))), + ), + ) + .child( + div() + .id("notes") + .child(Label::new("View the release notes")) + .cursor_pointer() + .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), + ) } } impl UpdateNotification { pub fn new(version: SemanticVersion) -> Self { - Self { _version: version } + Self { version } } - pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(NotificationEvent::Dismiss); + pub fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(DismissEvent); } } diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index 43e19b4ccb4738c42654dffaf7797d75dc88c084..8dc37f68dd7bd9b91c1d1fab240448eb25119cbf 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -31,16 +31,19 @@ media = { path = "../media" } project = { package = "project2", path = "../project2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } +ui = {package = "ui2", path = "../ui2"} workspace = {package = "workspace2", path = "../workspace2"} async-trait.workspace = true anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true +image = "0.23" postage.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_derive.workspace = true +smallvec.workspace = true [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 8e553a9b166ba34bfd6f85070f2b062070315a35..a93305772312cab3624f995e0dd49751554867e1 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -1,8 +1,9 @@ pub mod call_settings; pub mod participant; pub mod room; +mod shared_screen; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; use async_trait::async_trait; use audio::Audio; use call_settings::CallSettings; @@ -13,8 +14,8 @@ use client::{ use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, AsyncWindowContext, Context, EventEmitter, Model, ModelContext, - Subscription, Task, View, ViewContext, WeakModel, WeakView, + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel, + Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle, }; pub use participant::ParticipantLocation; use postage::watch; @@ -22,6 +23,7 @@ use project::Project; use room::Event; pub use room::Room; use settings::Settings; +use shared_screen::SharedScreen; use std::sync::Arc; use util::ResultExt; use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; @@ -332,12 +334,55 @@ impl ActiveCall { pub fn join_channel( &mut self, channel_id: u64, + requesting_window: Option>, cx: &mut ModelContext, ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(Some(room))); - } else { + return cx.spawn(|_, _| async move { + todo!(); + // let future = room.update(&mut cx, |room, cx| { + // room.most_active_project(cx).map(|(host, project)| { + // room.join_project(project, host, app_state.clone(), cx) + // }) + // }) + + // if let Some(future) = future { + // future.await?; + // } + + // Ok(Some(room)) + }); + } + + let should_prompt = room.update(cx, |room, _| { + room.channel_id().is_some() + && room.is_sharing_project() + && room.remote_participants().len() > 0 + }); + if should_prompt && requesting_window.is_some() { + return cx.spawn(|this, mut cx| async move { + let answer = requesting_window.unwrap().update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + ) + })?; + if answer.await? == 1 { + return Ok(None); + } + + room.update(&mut cx, |room, cx| room.clear_state(cx))?; + + this.update(&mut cx, |this, cx| { + this.join_channel(channel_id, requesting_window, cx) + })? + .await + }); + } + + if room.read(cx).channel_id().is_some() { room.update(cx, |room, cx| room.clear_state(cx)); } } @@ -512,24 +557,17 @@ pub fn report_call_event_for_channel( pub struct Call { active_call: Option<(Model, Vec)>, - parent_workspace: WeakView, } impl Call { - pub fn new( - parent_workspace: WeakView, - cx: &mut ViewContext<'_, Workspace>, - ) -> Box { + pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box { let mut active_call = None; if cx.has_global::>() { let call = cx.global::>().clone(); let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; active_call = Some((call, subscriptions)); } - Box::new(Self { - active_call, - parent_workspace, - }) + Box::new(Self { active_call }) } fn on_active_call_event( workspace: &mut Workspace, @@ -549,45 +587,10 @@ impl Call { #[async_trait(?Send)] impl CallHandler for Call { - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - _pane: &View, - cx: &mut ViewContext, - ) -> Option> { - let (call, _) = self.active_call.as_ref()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; - let _track = participant.video_tracks.values().next()?.clone(); - let _user = participant.user.clone(); - todo!(); - // for item in pane.read(cx).items_of_type::() { - // if item.read(cx).peer_id == peer_id { - // return Box::new(Some(item)); - // } - // } - - // Some(Box::new(cx.build_view(|cx| { - // SharedScreen::new(&track, peer_id, user.clone(), cx) - // }))) - } - - fn room_id(&self, cx: &AppContext) -> Option { - Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) - } - fn hang_up(&self, mut cx: AsyncWindowContext) -> Result>> { - let Some((call, _)) = self.active_call.as_ref() else { - bail!("Cannot exit a call; not in a call"); - }; - - call.update(&mut cx, |this, cx| this.hang_up(cx)) - } - fn active_project(&self, cx: &AppContext) -> Option> { - ActiveCall::global(cx).read(cx).location().cloned() - } fn peer_state( &mut self, leader_id: PeerId, + project: &Model, cx: &mut ViewContext, ) -> Option<(bool, bool)> { let (call, _) = self.active_call.as_ref()?; @@ -599,12 +602,7 @@ impl CallHandler for Call { match participant.location { ParticipantLocation::SharedProject { project_id } => { leader_in_this_app = true; - leader_in_this_project = Some(project_id) - == self - .parent_workspace - .update(cx, |this, cx| this.project().read(cx).remote_id()) - .log_err() - .flatten(); + leader_in_this_project = Some(project_id) == project.read(cx).remote_id(); } ParticipantLocation::UnsharedProject => { leader_in_this_app = true; @@ -618,6 +616,134 @@ impl CallHandler for Call { Some((leader_in_this_project, leader_in_this_app)) } + + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + pane: &View, + cx: &mut ViewContext, + ) -> Option> { + let (call, _) = self.active_call.as_ref()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + let track = participant.video_tracks.values().next()?.clone(); + let user = participant.user.clone(); + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == peer_id { + return Some(Box::new(item)); + } + } + + Some(Box::new(cx.build_view(|cx| { + SharedScreen::new(&track, peer_id, user.clone(), cx) + }))) + } + fn room_id(&self, cx: &AppContext) -> Option { + Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) + } + fn hang_up(&self, cx: &mut AppContext) -> Task> { + let Some((call, _)) = self.active_call.as_ref() else { + return Task::ready(Err(anyhow!("Cannot exit a call; not in a call"))); + }; + + call.update(cx, |this, cx| this.hang_up(cx)) + } + fn active_project(&self, cx: &AppContext) -> Option> { + ActiveCall::global(cx).read(cx).location().cloned() + } + fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut AppContext, + ) -> Task> { + ActiveCall::global(cx).update(cx, |this, cx| { + this.invite(called_user_id, initial_project, cx) + }) + } + fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { + self.active_call + .as_ref() + .map(|call| { + call.0.read(cx).room().map(|room| { + room.read(cx) + .remote_participants() + .iter() + .map(|participant| { + (participant.1.user.clone(), participant.1.peer_id.clone()) + }) + .collect() + }) + }) + .flatten() + } + fn is_muted(&self, cx: &AppContext) -> Option { + self.active_call + .as_ref() + .map(|call| { + call.0 + .read(cx) + .room() + .map(|room| room.read(cx).is_muted(cx)) + }) + .flatten() + } + fn toggle_mute(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + let room = room.clone(); + cx.spawn(|_, mut cx| async move { + room.update(&mut cx, |this, cx| this.toggle_mute(cx))?? + .await + }) + .detach_and_log_err(cx); + }) + }) + }); + } + fn toggle_screen_share(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + if this.is_screen_sharing() { + this.unshare_screen(cx).log_err(); + } else { + let t = this.share_screen(cx); + cx.spawn(move |_, _| async move { + t.await.log_err(); + }) + .detach(); + } + }) + }) + }) + }); + } + fn toggle_deafen(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + this.toggle_deafen(cx).log_err(); + }) + }) + }) + }); + } + fn is_deafened(&self, cx: &AppContext) -> Option { + self.active_call + .as_ref() + .map(|call| { + call.0 + .read(cx) + .room() + .map(|room| room.read(cx).is_deafened()) + }) + .flatten() + .flatten() + } } #[cfg(test)] diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs index f62d103f1758d85f6e4a12cc10c1797c7cd80f50..325a4f812b2f58c1b1bb0cc56f042e891df435d4 100644 --- a/crates/call2/src/participant.rs +++ b/crates/call2/src/participant.rs @@ -4,7 +4,7 @@ use client::{proto, User}; use collections::HashMap; use gpui::WeakModel; pub use live_kit_client::Frame; -use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; use std::sync::Arc; diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index 87118764fdc717620521b32e5b77aa7e1c4aca9f..694966abe9d509f5dc9cad5353a63aac074eaf50 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -21,7 +21,7 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; -use settings::Settings; +use settings::Settings as _; use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -1267,7 +1267,6 @@ impl Room { .ok_or_else(|| anyhow!("live-kit was not initialized"))? .await }; - let publication = publish_track.await; this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? diff --git a/crates/call2/src/shared_screen.rs b/crates/call2/src/shared_screen.rs new file mode 100644 index 0000000000000000000000000000000000000000..c38ebeac021d59c810fc27ff528ddc773f9642f4 --- /dev/null +++ b/crates/call2/src/shared_screen.rs @@ -0,0 +1,111 @@ +use crate::participant::{Frame, RemoteVideoTrack}; +use anyhow::Result; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + div, img, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, FocusableView, + InteractiveElement, ParentElement, Render, SharedString, Styled, Task, View, ViewContext, + VisualContext, WindowContext, +}; +use std::sync::{Arc, Weak}; +use ui::{h_stack, Icon, IconElement}; +use workspace::{item::Item, ItemNavHistory, WorkspaceId}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, + focus: FocusHandle, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.focus_handle(); + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + focus: cx.focus_handle(), + } + } +} + +impl EventEmitter for SharedScreen {} +impl EventEmitter for SharedScreen {} + +impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } +} +impl Render for SharedScreen { + type Element = Focusable
; + + fn render(&mut self, _: &mut ViewContext) -> Self::Element { + div().track_focus(&self.focus).size_full().children( + self.frame + .as_ref() + .map(|frame| img(frame.image()).size_full()), + ) + } +} + +impl Item for SharedScreen { + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content(&self, _: Option, _: &WindowContext<'_>) -> gpui::AnyElement { + h_stack() + .gap_1() + .child(IconElement::new(Icon::Screen)) + .child(SharedString::from(format!( + "{}'s screen", + self.user.github_login + ))) + .into_any() + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> { + let track = self.track.upgrade()?; + Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) + } +} diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index 4ad354f2f91bd56cdb0c1f657137d1beac9a3e4f..4746c9c6e4222a9c08383001525cf0f9cc55ab09 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -551,7 +551,6 @@ impl Client { F: 'static + Future>, { let message_type_id = TypeId::of::(); - let mut state = self.state.write(); state .models_by_message_type @@ -694,8 +693,8 @@ impl Client { } } - pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { - read_credentials_from_keychain(cx).await.is_some() + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { + read_credentials_from_keychain(cx).is_some() } #[async_recursion(?Send)] @@ -726,7 +725,7 @@ impl Client { let mut read_from_keychain = false; let mut credentials = self.state.read().credentials.clone(); if credentials.is_none() && try_keychain { - credentials = read_credentials_from_keychain(cx).await; + credentials = read_credentials_from_keychain(cx); read_from_keychain = credentials.is_some(); } if credentials.is_none() { @@ -1325,7 +1324,7 @@ impl Client { } } -async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { +fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { if IMPERSONATE_LOGIN.is_some() { return None; } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index fa7c4fe67df4fed4645e8c6552e242b3d7662276..c5820b539526c94879edfd2a06412c7271ab3252 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3941,7 +3941,7 @@ async fn test_collaborating_with_diagnostics( // Ensure client B observes the new diagnostics. project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), &[( ProjectPath { worktree_id, @@ -3961,14 +3961,14 @@ async fn test_collaborating_with_diagnostics( let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c_diagnostic_summaries = Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { - project.diagnostic_summaries(cx).collect::>() + project.diagnostic_summaries(false, cx).collect::>() }))); project_c.update(cx_c, |_, cx| { let summaries = project_c_diagnostic_summaries.clone(); cx.subscribe(&project_c, { move |p, _, event, cx| { if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { - *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect(); + *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect(); } } }) @@ -4018,7 +4018,7 @@ async fn test_collaborating_with_diagnostics( deterministic.run_until_parked(); project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -4034,7 +4034,7 @@ async fn test_collaborating_with_diagnostics( }); project_c.read_with(cx_c, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -4097,13 +4097,22 @@ async fn test_collaborating_with_diagnostics( ); deterministic.run_until_parked(); project_a.read_with(cx_a, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_b.read_with(cx_b, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_c.read_with(cx_c, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); } diff --git a/crates/collab2/src/tests/channel_tests.rs b/crates/collab2/src/tests/channel_tests.rs index 8ce5d99b80d3c630a81181e5f03f78d385186a10..43d18ee7d13b850b634c67a0414831a64b455d5c 100644 --- a/crates/collab2/src/tests/channel_tests.rs +++ b/crates/collab2/src/tests/channel_tests.rs @@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member( 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)) + .update(cx_b, |active_call, cx| active_call + .join_channel(sub_id, None, cx)) .await .is_ok()); } @@ -394,7 +395,9 @@ async fn test_channel_room( 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)) + .update(cx_a, |active_call, cx| { + active_call.join_channel(zed_id, None, cx) + }) .await .unwrap(); @@ -442,7 +445,9 @@ async fn test_channel_room( }); active_call_b - .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) + .update(cx_b, |active_call, cx| { + active_call.join_channel(zed_id, None, cx) + }) .await .unwrap(); @@ -559,12 +564,16 @@ async fn test_channel_room( }); active_call_a - .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) + .update(cx_a, |active_call, cx| { + active_call.join_channel(zed_id, None, cx) + }) .await .unwrap(); active_call_b - .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) + .update(cx_b, |active_call, cx| { + active_call.join_channel(zed_id, None, cx) + }) .await .unwrap(); @@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo 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)) + .update(cx_a, |active_call, cx| { + active_call.join_channel(zed_id, None, cx) + }) .await .unwrap(); @@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo active_call_a .update(cx_a, |active_call, cx| { - active_call.join_channel(rust_id, cx) + active_call.join_channel(rust_id, None, cx) }) .await .unwrap(); @@ -793,7 +804,7 @@ async fn test_call_from_channel( let active_call_b = cx_b.read(ActiveCall::global); active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx)) .await .unwrap(); @@ -1286,7 +1297,7 @@ async fn test_guest_access( // Non-members should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx)) .await .is_err()); @@ -1308,7 +1319,7 @@ async fn test_guest_access( // Client B joins channel A as a guest active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx)) .await .unwrap(); @@ -1341,7 +1352,7 @@ async fn test_guest_access( assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx)) .await .unwrap(); @@ -1372,7 +1383,7 @@ async fn test_invite_access( // should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx)) .await .is_err()); @@ -1390,7 +1401,7 @@ async fn test_invite_access( .unwrap(); active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx)) .await .unwrap(); diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index f2a39f35113df98df00f42eba2ff5fce59059358..2268a51f2ba5f1671a02707101ecad8f65501d1c 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( // Simultaneously join channel 1 and then channel 2 active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_1, cx)) + .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)) .detach(); - let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx)); + let join_channel_2 = + active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx)); join_channel_2.await.unwrap(); @@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( call.invite(client_c.user_id().unwrap(), None, cx) }); - let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); + let join_channel = + active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)); b_invite.await.unwrap(); c_invite.await.unwrap(); @@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( .unwrap(); // Simultaneously join channel 1 and call user B and user C from client A. - let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); + let join_channel = + active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)); let b_invite = active_call_a.update(cx_a, |call, cx| { call.invite(client_b.user_id().unwrap(), None, cx) @@ -3685,7 +3688,7 @@ async fn test_collaborating_with_diagnostics( project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), &[( ProjectPath { worktree_id, @@ -3705,14 +3708,14 @@ async fn test_collaborating_with_diagnostics( let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c_diagnostic_summaries = Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { - project.diagnostic_summaries(cx).collect::>() + project.diagnostic_summaries(false, cx).collect::>() }))); project_c.update(cx_c, |_, cx| { let summaries = project_c_diagnostic_summaries.clone(); cx.subscribe(&project_c, { move |p, _, event, cx| { if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { - *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect(); + *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect(); } } }) @@ -3763,7 +3766,7 @@ async fn test_collaborating_with_diagnostics( project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -3780,7 +3783,7 @@ async fn test_collaborating_with_diagnostics( project_c.read_with(cx_c, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -3841,15 +3844,24 @@ async fn test_collaborating_with_diagnostics( executor.run_until_parked(); project_a.read_with(cx_a, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_b.read_with(cx_b, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_c.read_with(cx_c, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); } diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 969869599b33f4d9e4ec19d8df14fda875bc9ddf..5f95f00d6fcd5c74d81d90f9b4b455ab531862d5 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -221,7 +221,7 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - call_factory: |_, _| Box::new(workspace::TestCallHandler), + call_factory: |_| Box::new(workspace::TestCallHandler), }); cx.update(|cx| { diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6af188dfd200c82d21771a603b905a5e2377f182..b62056a3be92ba178e62b44d4b1557c0d848f2a8 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1,5 +1,6 @@ +#![allow(unused)] // mod channel_modal; -// mod contact_finder; +mod contact_finder; // use crate::{ // channel_view::{self, ChannelView}, @@ -15,7 +16,10 @@ // proto::{self, PeerId}, // Client, Contact, User, UserStore, // }; -// use contact_finder::ContactFinder; +use contact_finder::ContactFinder; +use menu::{Cancel, Confirm, SelectNext, SelectPrev}; +use rpc::proto; +use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; // use drag_and_drop::{DragAndDrop, Draggable}; @@ -88,10 +92,10 @@ // channel_id: ChannelId, // } -// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -// pub struct OpenChannelNotes { -// pub channel_id: ChannelId, -// } +#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct OpenChannelNotes { + pub channel_id: ChannelId, +} // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] // pub struct JoinChannelCall { @@ -148,31 +152,45 @@ actions!( // ] // ); -// #[derive(Debug, Copy, Clone, PartialEq, Eq)] -// struct ChannelMoveClipboard { -// channel_id: ChannelId, -// } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct ChannelMoveClipboard { + channel_id: ChannelId, +} const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -use std::sync::Arc; +use std::{iter::once, mem, sync::Arc}; +use call::ActiveCall; +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; +use client::{Client, Contact, User, UserStore}; use db::kvp::KEY_VALUE_STORE; +use editor::Editor; +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext, - VisualContext, WeakView, + actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext, + AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, + Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, + ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString, + Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; -use project::Fs; +use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; -use settings::Settings; -use util::ResultExt; +use settings::{Settings, SettingsStore}; +use ui::prelude::*; +use ui::{ + h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, + Label, List, ListHeader, ListItem, Tooltip, +}; +use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, + notifications::NotifyResultExt, Workspace, }; -use crate::CollaborationPanelSettings; +use crate::{face_pile::FacePile, CollaborationPanelSettings}; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { @@ -215,26 +233,6 @@ pub fn init(cx: &mut AppContext) { // }, // ); - // cx.add_action( - // |panel: &mut CollabPanel, - // action: &StartMoveChannelFor, - // _: &mut ViewContext| { - // panel.channel_clipboard = Some(ChannelMoveClipboard { - // channel_id: action.channel_id, - // }); - // }, - // ); - - // cx.add_action( - // |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { - // if let Some(channel) = panel.selected_channel() { - // panel.channel_clipboard = Some(ChannelMoveClipboard { - // channel_id: channel.id, - // }) - // } - // }, - // ); - // cx.add_action( // |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { // let Some(clipboard) = panel.channel_clipboard.take() else { @@ -266,63 +264,63 @@ pub fn init(cx: &mut AppContext) { // ); } -// #[derive(Debug)] -// pub enum ChannelEditingState { -// Create { -// location: Option, -// pending_name: Option, -// }, -// Rename { -// location: ChannelId, -// pending_name: Option, -// }, -// } +#[derive(Debug)] +pub enum ChannelEditingState { + Create { + location: Option, + pending_name: Option, + }, + Rename { + location: ChannelId, + 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(), -// } -// } -// } +impl ChannelEditingState { + fn pending_name(&self) -> Option { + match self { + ChannelEditingState::Create { pending_name, .. } => pending_name.clone(), + ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(), + } + } +} pub struct CollabPanel { - width: Option, + width: Option, fs: Arc, focus_handle: FocusHandle, - // channel_clipboard: Option, - // pending_serialization: Task>, - // context_menu: ViewHandle, - // filter_editor: ViewHandle, - // channel_name_editor: ViewHandle, - // channel_editing_state: Option, - // entries: Vec, - // selection: Option, - // user_store: ModelHandle, - // client: Arc, - // channel_store: ModelHandle, - // project: ModelHandle, - // match_candidates: Vec, - // list_state: ListState, - // subscriptions: Vec, - // collapsed_sections: Vec
, - // collapsed_channels: Vec, - // drag_target_channel: ChannelDragTarget, - _workspace: WeakView, + channel_clipboard: Option, + pending_serialization: Task>, + context_menu: Option<(View, Point, Subscription)>, + filter_editor: View, + channel_name_editor: View, + channel_editing_state: Option, + entries: Vec, + selection: Option, + channel_store: Model, + user_store: Model, + client: Arc, + project: Model, + match_candidates: Vec, + scroll_handle: ScrollHandle, + subscriptions: Vec, + collapsed_sections: Vec
, + collapsed_channels: Vec, + drag_target_channel: ChannelDragTarget, + workspace: WeakView, // context_menu_on_selected: bool, } -// #[derive(PartialEq, Eq)] -// enum ChannelDragTarget { -// None, -// Root, -// Channel(ChannelId), -// } +#[derive(PartialEq, Eq)] +enum ChannelDragTarget { + None, + Root, + Channel(ChannelId), +} #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { - width: Option, + width: Option, collapsed_channels: Option>, } @@ -333,124 +331,108 @@ struct SerializedCollabPanel { // Dismissed, // } -// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -// enum Section { -// ActiveCall, -// Channels, -// ChannelInvites, -// ContactRequests, -// Contacts, -// Online, -// Offline, -// } - -// #[derive(Clone, Debug)] -// enum ListEntry { -// Header(Section), -// CallParticipant { -// user: Arc, -// peer_id: Option, -// is_pending: bool, -// }, -// ParticipantProject { -// project_id: u64, -// worktree_root_names: Vec, -// host_user_id: u64, -// is_last: bool, -// }, -// ParticipantScreen { -// peer_id: Option, -// is_last: bool, -// }, -// IncomingRequest(Arc), -// OutgoingRequest(Arc), -// ChannelInvite(Arc), -// Channel { -// channel: Arc, -// depth: usize, -// has_children: bool, -// }, -// ChannelNotes { -// channel_id: ChannelId, -// }, -// ChannelChat { -// channel_id: ChannelId, -// }, -// ChannelEditor { -// depth: usize, -// }, -// Contact { -// contact: Arc, -// calling: bool, -// }, -// ContactPlaceholder, -// } +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + ActiveCall, + Channels, + ChannelInvites, + ContactRequests, + Contacts, + Online, + Offline, +} -// impl Entity for CollabPanel { -// type Event = Event; -// } +#[derive(Clone, Debug)] +enum ListEntry { + Header(Section), + // CallParticipant { + // user: Arc, + // peer_id: Option, + // is_pending: bool, + // }, + // ParticipantProject { + // project_id: u64, + // worktree_root_names: Vec, + // host_user_id: u64, + // is_last: bool, + // }, + // ParticipantScreen { + // peer_id: Option, + // is_last: bool, + // }, + IncomingRequest(Arc), + OutgoingRequest(Arc), + // ChannelInvite(Arc), + Channel { + channel: Arc, + depth: usize, + has_children: bool, + }, + // ChannelNotes { + // channel_id: ChannelId, + // }, + // ChannelChat { + // channel_id: ChannelId, + // }, + ChannelEditor { + depth: usize, + }, + Contact { + contact: Arc, + calling: bool, + }, + ContactPlaceholder, +} impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { cx.build_view(|cx| { // let view_id = cx.view_id(); - // let filter_editor = cx.add_view(|cx| { - // let mut editor = Editor::single_line( - // Some(Arc::new(|theme| { - // theme.collab_panel.user_query_editor.clone() - // })), - // cx, - // ); - // editor.set_placeholder_text("Filter channels, 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(true, cx); - // if !query.is_empty() { - // this.selection = this - // .entries - // .iter() - // .position(|entry| !matches!(entry, ListEntry::Header(_))); - // } - // } else if let editor::Event::Blurred = event { - // let query = this.filter_editor.read(cx).text(cx); - // if query.is_empty() { - // this.selection.take(); - // this.update_entries(true, cx); - // } - // } - // }) - // .detach(); - - // 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 { - // 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(); - // } - // }) - // .detach(); + let filter_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Filter channels, contacts", cx); + editor + }); + + cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { + if let editor::EditorEvent::BufferEdited = event { + let query = this.filter_editor.read(cx).text(cx); + if !query.is_empty() { + this.selection.take(); + } + this.update_entries(true, cx); + if !query.is_empty() { + this.selection = this + .entries + .iter() + .position(|entry| !matches!(entry, ListEntry::Header(_))); + } + } else if let editor::EditorEvent::Blurred = event { + let query = this.filter_editor.read(cx).text(cx); + if query.is_empty() { + this.selection.take(); + this.update_entries(true, cx); + } + } + }) + .detach(); + + let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx)); + + cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| { + if let editor::EditorEvent::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(); + } + }) + .detach(); // let list_state = // ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { @@ -583,86 +565,88 @@ impl CollabPanel { // } // }); - let this = Self { + let mut this = Self { width: None, focus_handle: cx.focus_handle(), - // channel_clipboard: None, + channel_clipboard: None, 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: ChannelStore::global(cx), - // project: workspace.project().clone(), - // subscriptions: Vec::default(), - // match_candidates: Vec::default(), - // collapsed_sections: vec![Section::Offline], - // collapsed_channels: Vec::default(), - _workspace: workspace.weak_handle(), - // client: workspace.app_state().client.clone(), + pending_serialization: Task::ready(None), + context_menu: None, + channel_name_editor, + filter_editor, + entries: Vec::default(), + channel_editing_state: None, + selection: None, + channel_store: ChannelStore::global(cx), + user_store: workspace.user_store().clone(), + project: workspace.project().clone(), + subscriptions: Vec::default(), + match_candidates: Vec::default(), + scroll_handle: ScrollHandle::new(), + collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), + workspace: workspace.weak_handle(), + client: workspace.app_state().client.clone(), // context_menu_on_selected: true, - // drag_target_channel: ChannelDragTarget::None, - // list_state, + drag_target_channel: ChannelDragTarget::None, }; - // this.update_entries(false, cx); - - // // Update the dock position when the setting changes. - // let mut old_dock_position = this.position(cx); - // this.subscriptions - // .push( - // cx.observe_global::(move |this: &mut Self, cx| { - // let new_dock_position = this.position(cx); - // if new_dock_position != old_dock_position { - // old_dock_position = new_dock_position; - // cx.emit(Event::DockPositionChanged); - // } - // cx.notify(); - // }), - // ); - - // let active_call = ActiveCall::global(cx); - // this.subscriptions - // .push(cx.observe(&this.user_store, |this, _, cx| { - // this.update_entries(true, cx) - // })); - // this.subscriptions - // .push(cx.observe(&this.channel_store, |this, _, cx| { - // this.update_entries(true, cx) - // })); - // this.subscriptions - // .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); - // this.subscriptions - // .push(cx.observe_flag::(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.update_entries(false, cx); + + // Update the dock position when the setting changes. + let mut old_dock_position = this.position(cx); + this.subscriptions.push(cx.observe_global::( + move |this: &mut Self, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(PanelEvent::ChangePosition); + } + cx.notify(); + }, + )); + + let active_call = ActiveCall::global(cx); + this.subscriptions + .push(cx.observe(&this.user_store, |this, _, cx| { + this.update_entries(true, cx) + })); + this.subscriptions + .push(cx.observe(&this.channel_store, |this, _, cx| { + this.update_entries(true, cx) + })); + this.subscriptions + .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); + this.subscriptions + .push(cx.observe_flag::(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 }) } + fn contacts(&self, cx: &AppContext) -> Option>> { + Some(self.user_store.read(cx).contacts().to_owned()) + } pub async fn load( workspace: WeakView, mut cx: AsyncWindowContext, @@ -684,10 +668,9 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; - //todo!(collapsed_channels) - // panel.collapsed_channels = serialized_panel - // .collapsed_channels - // .unwrap_or_else(|| Vec::new()); + panel.collapsed_channels = serialized_panel + .collapsed_channels + .unwrap_or_else(|| Vec::new()); cx.notify(); }); } @@ -695,469 +678,460 @@ impl CollabPanel { }) } - // 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( - // COLLABORATION_PANEL_KEY.into(), - // serde_json::to_string(&SerializedCollabPanel { - // width, - // collapsed_channels: Some(collapsed_channels), - // })?, - // ) - // .await?; - // anyhow::Ok(()) - // } - // .log_err(), - // ); - // } - - // 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); - // 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); - // let mut scroll_to_top = false; - - // if let Some(room) = ActiveCall::global(cx).read(cx).room() { - // self.entries.push(ListEntry::Header(Section::ActiveCall)); - // if !old_entries - // .iter() - // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) - // { - // scroll_to_top = true; - // } - - // if !self.collapsed_sections.contains(&Section::ActiveCall) { - // let room = room.read(cx); - - // if let Some(channel_id) = room.channel_id() { - // self.entries.push(ListEntry::ChannelNotes { channel_id }); - // self.entries.push(ListEntry::ChannelChat { channel_id }) - // } - - // // 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, - // peer_id: None, - // 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() && !room.is_screen_sharing(), - // }); - // } - // if room.is_screen_sharing() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: None, - // 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]; - // self.entries.push(ListEntry::CallParticipant { - // user: participant.user.clone(), - // peer_id: Some(participant.peer_id), - // is_pending: false, - // }); - // let mut projects = participant.projects.iter().peekable(); - // 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: 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: Some(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(), - // )); - // self.entries - // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { - // user: room.pending_participants()[mat.candidate_id].clone(), - // peer_id: None, - // is_pending: true, - // })); - // } - // } - - // let mut request_entries = Vec::new(); - - // if cx.has_flag::() { - // self.entries.push(ListEntry::Header(Section::Channels)); - - // if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend(channel_store.ordered_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, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if let Some(state) = &self.channel_editing_state { - // if matches!(state, ChannelEditingState::Create { location: None, .. }) { - // self.entries.push(ListEntry::ChannelEditor { depth: 0 }); - // } - // } - // let mut collapse_depth = None; - // for mat in matches { - // let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); - // let depth = channel.parent_path.len(); - - // if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { - // collapse_depth = Some(depth); - // } else if let Some(collapsed_depth) = collapse_depth { - // if depth > collapsed_depth { - // continue; - // } - // if self.is_channel_collapsed(channel.id) { - // collapse_depth = Some(depth); - // } else { - // collapse_depth = None; - // } - // } - - // let has_children = channel_store - // .channel_at_index(mat.candidate_id + 1) - // .map_or(false, |next_channel| { - // next_channel.parent_path.ends_with(&[channel.id]) - // }); - - // match &self.channel_editing_state { - // Some(ChannelEditingState::Create { - // location: parent_id, - // .. - // }) if *parent_id == Some(channel.id) => { - // self.entries.push(ListEntry::Channel { - // channel: channel.clone(), - // depth, - // has_children: false, - // }); - // self.entries - // .push(ListEntry::ChannelEditor { depth: depth + 1 }); - // } - // Some(ChannelEditingState::Rename { - // location: parent_id, - // .. - // }) if parent_id == &channel.id => { - // self.entries.push(ListEntry::ChannelEditor { depth }); - // } - // _ => { - // self.entries.push(ListEntry::Channel { - // channel: channel.clone(), - // depth, - // has_children, - // }); - // } - // } - // } - // } - - // 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()) - // })); - - // if !request_entries.is_empty() { - // self.entries - // .push(ListEntry::Header(Section::ChannelInvites)); - // if !self.collapsed_sections.contains(&Section::ChannelInvites) { - // self.entries.append(&mut request_entries); - // } - // } - // } - // } - - // self.entries.push(ListEntry::Header(Section::Contacts)); - - // request_entries.clear(); - // 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| ListEntry::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| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - // ); - // } - - // if !request_entries.is_empty() { - // self.entries - // .push(ListEntry::Header(Section::ContactRequests)); - // if !self.collapsed_sections.contains(&Section::ContactRequests) { - // 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 (online_contacts, offline_contacts) = matches - // .iter() - // .partition::, _>(|mat| contacts[mat.candidate_id].online); - - // for (matches, section) in [ - // (online_contacts, Section::Online), - // (offline_contacts, Section::Offline), - // ] { - // if !matches.is_empty() { - // self.entries.push(ListEntry::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(ListEntry::Contact { - // contact: contact.clone(), - // calling: active_call.pending_invites().contains(&contact.user.id), - // }); - // } - // } - // } - // } - // } - - // 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(); - // for (ix, entry) in self.entries.iter().enumerate() { - // if *entry == prev_selected_entry { - // self.selection = Some(ix); - // break; - // } - // } - // } - // } 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(); + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + let collapsed_channels = self.collapsed_channels.clone(); + self.pending_serialization = cx.background_executor().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + COLLABORATION_PANEL_KEY.into(), + serde_json::to_string(&SerializedCollabPanel { + width, + collapsed_channels: Some(collapsed_channels), + })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } - // self.list_state.reset(self.entries.len()); + 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); + let executor = cx.background_executor().clone(); + + let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + let old_entries = mem::take(&mut self.entries); + let scroll_to_top = false; + + // if let Some(room) = ActiveCall::global(cx).read(cx).room() { + // self.entries.push(ListEntry::Header(Section::ActiveCall)); + // if !old_entries + // .iter() + // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + // { + // scroll_to_top = true; + // } + + // if !self.collapsed_sections.contains(&Section::ActiveCall) { + // let room = room.read(cx); + + // if let Some(channel_id) = room.channel_id() { + // self.entries.push(ListEntry::ChannelNotes { channel_id }); + // self.entries.push(ListEntry::ChannelChat { channel_id }) + // } + + // // 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, + // peer_id: None, + // 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() && !room.is_screen_sharing(), + // }); + // } + // if room.is_screen_sharing() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: None, + // 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]; + // self.entries.push(ListEntry::CallParticipant { + // user: participant.user.clone(), + // peer_id: Some(participant.peer_id), + // is_pending: false, + // }); + // let mut projects = participant.projects.iter().peekable(); + // 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: 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: Some(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(), + // )); + // self.entries + // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + // user: room.pending_participants()[mat.candidate_id].clone(), + // peer_id: None, + // is_pending: true, + // })); + // } + // } + + let mut request_entries = Vec::new(); + + if cx.has_flag::() { + self.entries.push(ListEntry::Header(Section::Channels)); + + if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { + self.match_candidates.clear(); + self.match_candidates + .extend(channel_store.ordered_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, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if let Some(state) = &self.channel_editing_state { + if matches!(state, ChannelEditingState::Create { location: None, .. }) { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); + } + } + let mut collapse_depth = None; + for mat in matches { + let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let depth = channel.parent_path.len(); + + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } + + let has_children = channel_store + .channel_at_index(mat.candidate_id + 1) + .map_or(false, |next_channel| { + next_channel.parent_path.ends_with(&[channel.id]) + }); + + match &self.channel_editing_state { + Some(ChannelEditingState::Create { + location: parent_id, + .. + }) if *parent_id == Some(channel.id) => { + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + has_children: false, + }); + self.entries + .push(ListEntry::ChannelEditor { depth: depth + 1 }); + } + Some(ChannelEditingState::Rename { + location: parent_id, + .. + }) if parent_id == &channel.id => { + self.entries.push(ListEntry::ChannelEditor { depth }); + } + _ => { + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + has_children, + }); + } + } + } + } - // if scroll_to_top { - // self.list_state.scroll_to(ListOffset::default()); - // } else { - // // Attempt to maintain the same scroll position. - // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { - // let new_scroll_top = self - // .entries - // .iter() - // .position(|entry| entry == old_top_entry) - // .map(|item_ix| ListOffset { - // item_ix, - // offset_in_item: 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., - // }) - // }); + // 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()) + // })); - // self.list_state - // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); - // } - // } + // if !request_entries.is_empty() { + // self.entries + // .push(ListEntry::Header(Section::ChannelInvites)); + // if !self.collapsed_sections.contains(&Section::ChannelInvites) { + // self.entries.append(&mut request_entries); + // } + // } + // } + } + + self.entries.push(ListEntry::Header(Section::Contacts)); + + request_entries.clear(); + 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| ListEntry::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| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } + + if !request_entries.is_empty() { + self.entries + .push(ListEntry::Header(Section::ContactRequests)); + if !self.collapsed_sections.contains(&Section::ContactRequests) { + 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 (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ListEntry::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(ListEntry::Contact { + contact: contact.clone(), + calling: active_call.pending_invites().contains(&contact.user.id), + }); + } + } + } + } + } + + 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(); + for (ix, entry) in self.entries.iter().enumerate() { + if *entry == prev_selected_entry { + self.selection = Some(ix); + self.scroll_handle.scroll_to_item(ix); + break; + } + } + } + } else { + self.selection = self.selection.and_then(|prev_selection| { + if self.entries.is_empty() { + None + } else { + let ix = prev_selection.min(self.entries.len() - 1); + self.scroll_handle.scroll_to_item(ix); + Some(ix) + } + }); + } + + if scroll_to_top { + self.scroll_handle.scroll_to_item(0) + } else { + let (old_index, old_offset) = self.scroll_handle.logical_scroll_top(); + // Attempt to maintain the same scroll position. + if let Some(old_top_entry) = old_entries.get(old_index) { + let (new_index, new_offset) = self + .entries + .iter() + .position(|entry| entry == old_top_entry) + .map(|item_ix| (item_ix, old_offset)) + .or_else(|| { + let entry_after_old_top = old_entries.get(old_index + 1)?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_after_old_top)?; + Some((item_ix, px(0.))) + }) + .or_else(|| { + let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_before_old_top)?; + Some((item_ix, px(0.))) + }) + .unwrap_or_else(|| (old_index, old_offset)); + + self.scroll_handle + .set_logical_scroll_top(new_index, new_offset); + } + } - // cx.notify(); - // } + cx.notify(); + } // fn render_call_participant( // user: &User, @@ -1444,869 +1418,96 @@ impl CollabPanel { // .into_any() // } - // 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| { - // editor.set_text("", cx); - // }); - // true - // } else { - // false - // } - // } + 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| { + editor.set_text("", cx); + }); + true + } else { + false + } + } - // fn render_header( + // fn render_contact_placeholder( // &self, - // section: Section, - // theme: &theme::Theme, + // theme: &theme::CollabPanel, // is_selected: bool, - // is_collapsed: bool, // cx: &mut ViewContext, // ) -> AnyElement { - // enum Header {} - // enum LeaveCallContactList {} - // enum AddChannel {} - - // let tooltip_style = &theme.tooltip; - // let mut channel_link = None; - // let mut channel_tooltip_text = None; - // let mut channel_icon = None; - // let mut is_dragged_over = false; - - // let text = match section { - // Section::ActiveCall => { - // let channel_name = maybe!({ - // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - - // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - - // channel_link = Some(channel.link()); - // (channel_icon, channel_tooltip_text) = match channel.visibility { - // proto::ChannelVisibility::Public => { - // (Some("icons/public.svg"), Some("Copy public channel link.")) - // } - // proto::ChannelVisibility::Members => { - // (Some("icons/hash.svg"), Some("Copy private channel link.")) - // } - // }; - - // Some(channel.name.as_str()) - // }); - - // if let Some(name) = channel_name { - // Cow::Owned(format!("{}", name)) - // } 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 {} - // let button = match section { - // Section::ActiveCall => channel_link.map(|channel_link| { - // let channel_link_copy = channel_link.clone(); - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/link.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // let item = ClipboardItem::new(channel_link_copy.clone()); - // cx.write_to_clipboard(item) - // }) - // .with_tooltip::( - // 0, - // channel_tooltip_text.unwrap(), - // None, - // tooltip_style.clone(), - // cx, + // enum AddContacts {} + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = theme.list_empty_state.style_for(is_selected, state); + // Flex::row() + // .with_child( + // Svg::new("icons/plus.svg") + // .with_color(theme.list_empty_icon.color) + // .constrained() + // .with_width(theme.list_empty_icon.width) + // .aligned() + // .left(), // ) - // }), - // Section::Contacts => Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .add_contact_button - // .style_for(is_selected, state), - // "icons/plus.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, this, cx| { - // this.toggle_contact_finder(cx); - // }) - // .with_tooltip::( - // 0, - // "Search for new contact", - // None, - // tooltip_style.clone(), - // cx, - // ), - // ), - // Section::Channels => { - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Root - // { - // is_dragged_over = true; - // } - - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .add_contact_button - // .style_for(is_selected, state), - // "icons/plus.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) - // .with_tooltip::( - // 0, - // "Create a channel", - // None, - // tooltip_style.clone(), - // cx, - // ), + // .with_child( + // Label::new("Add a contact", style.text.clone()) + // .contained() + // .with_style(theme.list_empty_label_container), // ) - // } - // _ => None, - // }; + // .align_children_center() + // .contained() + // .with_style(style.container) + // .into_any() + // }) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.toggle_contact_finder(cx); + // }) + // .into_any() + // } - // let can_collapse = match section { - // Section::ActiveCall | Section::Channels | Section::Contacts => false, - // Section::ChannelInvites - // | Section::ContactRequests - // | Section::Online - // | Section::Offline => true, - // }; - // let icon_size = (&theme.collab_panel).section_icon_size; - // let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { - // let header_style = if can_collapse { - // theme - // .collab_panel - // .subheader_row - // .in_state(is_selected) - // .style_for(state) - // } else { - // &theme.collab_panel.header_row - // }; + // fn render_channel_notes( + // &self, + // channel_id: ChannelId, + // theme: &theme::CollabPanel, + // is_selected: bool, + // ix: usize, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum ChannelNotes {} + // let host_avatar_width = theme + // .contact_avatar + // .width + // .or(theme.contact_avatar.height) + // .unwrap_or(0.); - // Flex::row() - // .with_children(if can_collapse { - // Some( - // Svg::new(if is_collapsed { - // "icons/chevron_right.svg" - // } else { - // "icons/chevron_down.svg" - // }) - // .with_color(header_style.text.color) + // MouseEventHandler::new::(ix as usize, cx, |state, cx| { + // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + // let row = theme.project_row.in_state(is_selected).style_for(state); + + // Flex::::row() + // .with_child(render_tree_branch( + // tree_branch, + // &row.name.text, + // false, + // vec2f(host_avatar_width, theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/file.svg") + // .with_color(theme.channel_hash.color) // .constrained() - // .with_max_width(icon_size) - // .with_max_height(icon_size) + // .with_width(theme.channel_hash.width) // .aligned() - // .constrained() - // .with_width(icon_size) - // .contained() - // .with_margin_right( - // theme.collab_panel.contact_username.container.margin.left, - // ), - // ) - // } else if let Some(channel_icon) = channel_icon { - // Some( - // Svg::new(channel_icon) - // .with_color(header_style.text.color) - // .constrained() - // .with_max_width(icon_size) - // .with_max_height(icon_size) - // .aligned() - // .constrained() - // .with_width(icon_size) - // .contained() - // .with_margin_right( - // theme.collab_panel.contact_username.container.margin.left, - // ), - // ) - // } else { - // None - // }) + // .left(), + // ) // .with_child( - // Label::new(text, header_style.text.clone()) + // Label::new("notes", theme.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) // .aligned() // .left() // .flex(1., true), // ) - // .with_children(button.map(|button| button.aligned().right())) // .constrained() - // .with_height(theme.collab_panel.row_height) - // .contained() - // .with_style(if is_dragged_over { - // theme.collab_panel.dragged_over_header - // } else { - // header_style.container - // }) - // }); - - // result = result - // .on_move(move |_, this, cx| { - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // { - // this.drag_target_channel = ChannelDragTarget::Root; - // cx.notify() - // } - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // this.channel_store - // .update(cx, |channel_store, cx| { - // channel_store.move_channel(dragged_channel.id, None, cx) - // }) - // .detach_and_log_err(cx) - // } - // }); - - // if can_collapse { - // result = result - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if can_collapse { - // this.toggle_section_expanded(section, cx); - // } - // }) - // } - - // result.into_any() - // } - - // fn render_contact( - // contact: &Contact, - // calling: bool, - // project: &ModelHandle, - // theme: &theme::Theme, - // is_selected: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ContactTooltip {} - - // let collab_theme = &theme.collab_panel; - // let online = contact.online; - // let busy = contact.busy || calling; - // let user_id = contact.user.id; - // let github_login = contact.user.github_login.clone(); - // let initial_project = project.clone(); - - // let event_handler = - // 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 { - // Some( - // Empty::new() - // .collapsed() - // .contained() - // .with_style(if busy { - // collab_theme.contact_status_busy - // } else { - // collab_theme.contact_status_free - // }) - // .aligned(), - // ) - // } else { - // None - // }; - // Stack::new() - // .with_child( - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left(), - // ) - // .with_children(status_badge) - // })) - // .with_child( - // Label::new( - // contact.user.github_login.clone(), - // collab_theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(if state.hovered() { - // Some( - // MouseEventHandler::new::( - // contact.user.id as usize, - // cx, - // |mouse_state, _| { - // let button_style = - // collab_theme.contact_button.style_for(mouse_state); - // render_icon_button(button_style, "icons/x.svg") - // .aligned() - // .flex_float() - // }, - // ) - // .with_padding(Padding::uniform(2.)) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.remove_contact(user_id, &github_login, cx); - // }) - // .flex_float(), - // ) - // } else { - // None - // }) - // .with_children(if calling { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style( - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(state), - // ) - // }); - - // if online && !busy { - // let room = ActiveCall::global(cx).read(cx).room(); - // let label = if room.is_some() { - // format!("Invite {} to join call", contact.user.github_login) - // } else { - // format!("Call {}", contact.user.github_login) - // }; - - // event_handler - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.call(user_id, Some(initial_project.clone()), cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // contact.user.id as usize, - // label, - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else { - // event_handler - // .with_tooltip::( - // contact.user.id as usize, - // format!( - // "{} is {}", - // contact.user.github_login, - // if busy { "on a call" } else { "offline" } - // ), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } - // } - - // fn render_contact_placeholder( - // &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); - // Flex::row() - // .with_child( - // Svg::new("icons/plus.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() - // }) - // .on_click(MouseButton::Left, |_, this, cx| { - // this.toggle_contact_finder(cx); - // }) - // .into_any() - // } - - // fn render_channel_editor( - // &self, - // theme: &theme::Theme, - // depth: usize, - // cx: &AppContext, - // ) -> AnyElement { - // Flex::row() - // .with_child( - // Empty::new() - // .constrained() - // .with_width(theme.collab_panel.disclosure.button_space()), - // ) - // .with_child( - // Svg::new("icons/hash.svg") - // .with_color(theme.collab_panel.channel_hash.color) - // .constrained() - // .with_width(theme.collab_panel.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // 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.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(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, - // ) - // .into_any() - // } - - // fn render_channel( - // &self, - // channel: &Channel, - // depth: usize, - // theme: &theme::Theme, - // is_selected: bool, - // has_children: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let channel_id = channel.id; - // let collab_theme = &theme.collab_panel; - // let is_public = self - // .channel_store - // .read(cx) - // .channel_for_id(channel_id) - // .map(|channel| channel.visibility) - // == Some(proto::ChannelVisibility::Public); - // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); - // let disclosed = - // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); - - // let is_active = maybe!({ - // 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 = 3; - - // enum ChannelCall {} - // enum ChannelNote {} - // enum NotesTooltip {} - // enum ChatTooltip {} - // enum ChannelTooltip {} - - // let mut is_dragged_over = false; - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) - // { - // is_dragged_over = true; - // } - - // let has_messages_notification = channel.unseen_message_id.is_some(); - - // MouseEventHandler::new::(ix, cx, |state, cx| { - // let row_hovered = state.hovered(); - - // let mut select_state = |interactive: &Interactive| { - // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { - // interactive.clicked.as_ref().unwrap().clone() - // } else if state.hovered() || other_selected { - // interactive - // .hovered - // .as_ref() - // .unwrap_or(&interactive.default) - // .clone() - // } else { - // interactive.default.clone() - // } - // }; - - // Flex::::row() - // .with_child( - // Svg::new(if is_public { - // "icons/public.svg" - // } else { - // "icons/hash.svg" - // }) - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child({ - // let style = collab_theme.channel_name.inactive_state(); - // Flex::row() - // .with_child( - // Label::new(channel.name.clone(), style.text.clone()) - // .contained() - // .with_style(style.container) - // .aligned() - // .left() - // .with_tooltip::( - // ix, - // "Join channel", - // None, - // theme.tooltip.clone(), - // cx, - // ), - // ) - // .with_children({ - // let participants = - // self.channel_store.read(cx).channel_participants(channel_id); - - // if !participants.is_empty() { - // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - - // let result = FacePile::new(collab_theme.face_overlap) - // .with_children( - // participants - // .iter() - // .filter_map(|user| { - // Some( - // Image::from_data(user.avatar.clone()?) - // .with_style(collab_theme.channel_avatar), - // ) - // }) - // .take(FACEPILE_LIMIT), - // ) - // .with_children((extra_count > 0).then(|| { - // Label::new( - // format!("+{}", extra_count), - // collab_theme.extra_participant_label.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.extra_participant_label.container) - // })); - - // Some(result) - // } else { - // None - // } - // }) - // .with_spacing(8.) - // .align_children_center() - // .flex(1., true) - // }) - // .with_child( - // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { - // let container_style = collab_theme - // .disclosure - // .button - // .style_for(mouse_state) - // .container; - - // if channel.unseen_message_id.is_some() { - // Svg::new("icons/conversations.svg") - // .with_color(collab_theme.channel_note_active_color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .into_any() - // } else if row_hovered { - // Svg::new("icons/conversations.svg") - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - // }) - // .with_tooltip::( - // ix, - // "Open channel chat", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .contained() - // .with_margin_right(4.), - // ) - // .with_child( - // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { - // let container_style = collab_theme - // .disclosure - // .button - // .style_for(mouse_state) - // .container; - // if row_hovered || channel.unseen_note_version.is_some() { - // Svg::new("icons/file.svg") - // .with_color(if channel.unseen_note_version.is_some() { - // collab_theme.channel_note_active_color - // } else { - // collab_theme.channel_hash.color - // }) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .with_margin_right(collab_theme.channel_hash.container.margin.left) - // .with_tooltip::( - // ix as usize, - // "Open channel notes", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else if has_messages_notification { - // Empty::new() - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_uniform_padding(4.) - // .with_margin_right(collab_theme.channel_hash.container.margin.left) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - // }), - // ) - // .align_children_center() - // .styleable_component() - // .disclosable( - // disclosed, - // Box::new(ToggleCollapse { - // location: channel.id.clone(), - // }), - // ) - // .with_id(ix) - // .with_style(collab_theme.disclosure.clone()) - // .element() - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(select_state( - // collab_theme - // .channel_row - // .in_state(is_selected || is_active || is_dragged_over), - // )) - // .with_padding_left( - // collab_theme.channel_row.default_style().padding.left - // + collab_theme.channel_indent * depth as f32, - // ) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if this.drag_target_channel == ChannelDragTarget::None { - // if is_active { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) - // } else { - // this.join_channel(channel_id, cx) - // } - // } - // }) - // .on_click(MouseButton::Right, { - // let channel = channel.clone(); - // move |e, this, cx| { - // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); - // } - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // this.channel_store - // .update(cx, |channel_store, cx| { - // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) - // }) - // .detach_and_log_err(cx) - // } - // }) - // .on_move({ - // let channel = channel.clone(); - // move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // if channel.id != dragged_channel.id { - // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); - // } - // cx.notify() - // } - // } - // }) - // .as_draggable::<_, Channel>( - // channel.clone(), - // move |_, channel, cx: &mut ViewContext| { - // let theme = &theme::current(cx).collab_panel; - - // Flex::::row() - // .with_child( - // Svg::new("icons/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.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left(), - // ) - // .align_children_center() - // .contained() - // .with_background_color( - // theme - // .container - // .background_color - // .unwrap_or(gpui::color::Color::transparent_black()), - // ) - // .contained() - // .with_padding_left( - // theme.channel_row.default_style().padding.left - // + theme.channel_indent * depth as f32, - // ) - // .into_any() - // }, - // ) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - // } - - // fn render_channel_notes( - // &self, - // channel_id: ChannelId, - // theme: &theme::CollabPanel, - // is_selected: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ChannelNotes {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // false, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/file.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("notes", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) + // .with_height(theme.row_height) // .contained() // .with_style(*theme.channel_row.style_for(is_selected, state)) // .with_padding_left(theme.channel_row.default_style().padding.left) @@ -2454,476 +1655,331 @@ impl CollabPanel { // .into_any() // } - // fn render_contact_request( - // user: Arc, - // user_store: ModelHandle, - // theme: &theme::CollabPanel, - // 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.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.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.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 has_subchannels(&self, ix: usize) -> bool { - // self.entries.get(ix).map_or(false, |entry| { - // if let ListEntry::Channel { has_children, .. } = entry { - // *has_children - // } else { - // false - // } - // }) - // } - - // fn deploy_channel_context_menu( - // &mut self, - // position: Option, - // channel: &Channel, - // ix: usize, - // cx: &mut ViewContext, - // ) { - // self.context_menu_on_selected = position.is_none(); - - // let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { - // self.channel_store - // .read(cx) - // .channel_for_id(clipboard.channel_id) - // .map(|channel| channel.name.clone()) - // }); - - // self.context_menu.update(cx, |context_menu, cx| { - // context_menu.set_position_mode(if self.context_menu_on_selected { - // OverlayPositionMode::Local - // } else { - // OverlayPositionMode::Window - // }); - - // let mut items = Vec::new(); - - // let select_action_name = if self.selection == Some(ix) { - // "Unselect" - // } else { - // "Select" - // }; - - // items.push(ContextMenuItem::action( - // select_action_name, - // ToggleSelectedIx { ix }, - // )); - - // if self.has_subchannels(ix) { - // let expand_action_name = if self.is_channel_collapsed(channel.id) { - // "Expand Subchannels" - // } else { - // "Collapse Subchannels" - // }; - // items.push(ContextMenuItem::action( - // expand_action_name, - // ToggleCollapse { - // location: channel.id, - // }, - // )); - // } - - // items.push(ContextMenuItem::action( - // "Open Notes", - // OpenChannelNotes { - // channel_id: channel.id, - // }, - // )); - - // items.push(ContextMenuItem::action( - // "Open Chat", - // JoinChannelChat { - // channel_id: channel.id, - // }, - // )); - - // items.push(ContextMenuItem::action( - // "Copy Channel Link", - // CopyChannelLink { - // channel_id: channel.id, - // }, - // )); - - // if self.channel_store.read(cx).is_channel_admin(channel.id) { - // items.extend([ - // ContextMenuItem::Separator, - // ContextMenuItem::action( - // "New Subchannel", - // NewChannel { - // location: channel.id, - // }, - // ), - // ContextMenuItem::action( - // "Rename", - // RenameChannel { - // channel_id: channel.id, - // }, - // ), - // ContextMenuItem::action( - // "Move this channel", - // StartMoveChannelFor { - // channel_id: channel.id, - // }, - // ), - // ]); - - // if let Some(channel_name) = clipboard_channel_name { - // items.push(ContextMenuItem::Separator); - // items.push(ContextMenuItem::action( - // format!("Move '#{}' here", channel_name), - // MoveChannel { to: channel.id }, - // )); - // } + fn has_subchannels(&self, ix: usize) -> bool { + self.entries.get(ix).map_or(false, |entry| { + if let ListEntry::Channel { has_children, .. } = entry { + *has_children + } else { + false + } + }) + } - // items.extend([ - // ContextMenuItem::Separator, - // ContextMenuItem::action( - // "Invite Members", - // InviteMembers { - // channel_id: channel.id, - // }, - // ), - // ContextMenuItem::action( - // "Manage Members", - // ManageMembers { - // channel_id: channel.id, - // }, - // ), - // ContextMenuItem::Separator, - // ContextMenuItem::action( - // "Delete", - // RemoveChannel { - // channel_id: channel.id, - // }, - // ), - // ]); - // } + fn deploy_channel_context_menu( + &mut self, + position: Point, + channel_id: ChannelId, + ix: usize, + cx: &mut ViewContext, + ) { + let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { + self.channel_store + .read(cx) + .channel_for_id(clipboard.channel_id) + .map(|channel| channel.name.clone()) + }); + let this = cx.view().clone(); + + let context_menu = ContextMenu::build(cx, |mut context_menu, cx| { + if self.has_subchannels(ix) { + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + context_menu = context_menu.entry( + expand_action_name, + cx.handler_for(&this, move |this, cx| { + this.toggle_channel_collapsed(channel_id, cx) + }), + ); + } - // context_menu.show( - // position.unwrap_or_default(), - // if self.context_menu_on_selected { - // gpui::elements::AnchorCorner::TopRight - // } else { - // gpui::elements::AnchorCorner::BottomLeft - // }, - // items, - // cx, - // ); - // }); + context_menu = context_menu + .entry( + "Open Notes", + cx.handler_for(&this, move |this, cx| { + this.open_channel_notes(channel_id, cx) + }), + ) + .entry( + "Open Chat", + cx.handler_for(&this, move |this, cx| { + this.join_channel_chat(channel_id, cx) + }), + ) + .entry( + "Copy Channel Link", + cx.handler_for(&this, move |this, cx| { + this.copy_channel_link(channel_id, cx) + }), + ); + + if self.channel_store.read(cx).is_channel_admin(channel_id) { + context_menu = context_menu + .separator() + .entry( + "New Subchannel", + cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)), + ) + .entry( + "Rename", + cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)), + ) + .entry( + "Move this channel", + cx.handler_for(&this, move |this, cx| { + this.start_move_channel(channel_id, cx) + }), + ); + + if let Some(channel_name) = clipboard_channel_name { + context_menu = context_menu.separator().entry( + format!("Move '#{}' here", channel_name), + cx.handler_for(&this, move |this, cx| { + this.move_channel_on_clipboard(channel_id, cx) + }), + ); + } + + context_menu = context_menu + .separator() + .entry( + "Invite Members", + cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)), + ) + .entry( + "Manage Members", + cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)), + ) + .entry( + "Delete", + cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)), + ); + } - // cx.notify(); - // } + context_menu + }); - // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - // if self.take_editing_state(cx) { - // 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); - // } - // }); - // } + cx.focus_view(&context_menu); + let subscription = + cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + if this.context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(cx) + }) { + cx.focus_self(); + } + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); - // self.update_entries(false, cx); - // } + cx.notify(); + } - // fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - // let ix = self.selection.map_or(0, |ix| ix + 1); - // if ix < self.entries.len() { - // self.selection = Some(ix); - // } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_editing_state(cx) { + cx.focus_view(&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(false, cx); + } - // 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_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + let ix = self.selection.map_or(0, |ix| ix + 1); + if ix < self.entries.len() { + self.selection = Some(ix); + } - // fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - // let ix = self.selection.take().unwrap_or(0); - // if ix > 0 { - // self.selection = Some(ix - 1); - // } + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } - // 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) { + let ix = self.selection.take().unwrap_or(0); + if ix > 0 { + self.selection = Some(ix - 1); + } - // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - // if self.confirm_channel_edit(cx) { - // return; - // } + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } - // if let Some(selection) = self.selection { - // if let Some(entry) = self.entries.get(selection) { - // match entry { - // 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_section_expanded(*section, cx); - // } - // }, - // ListEntry::Contact { contact, calling } => { - // if contact.online && !contact.busy && !calling { - // self.call(contact.user.id, Some(self.project.clone()), cx); - // } - // } - // ListEntry::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); - // } - // } - // ListEntry::ParticipantScreen { peer_id, .. } => { - // let Some(peer_id) = peer_id else { - // return; - // }; - // if let Some(workspace) = self.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.open_shared_screen(*peer_id, cx) - // }); - // } - // } - // ListEntry::Channel { channel, .. } => { - // let is_active = maybe!({ - // let call_channel = ActiveCall::global(cx) - // .read(cx) - // .room()? - // .read(cx) - // .channel_id()?; - - // Some(call_channel == channel.id) - // }) - // .unwrap_or(false); - // if is_active { - // self.open_channel_notes( - // &OpenChannelNotes { - // channel_id: channel.id, - // }, - // cx, - // ) - // } else { - // self.join_channel(channel.id, cx) - // } - // } - // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), - // _ => {} - // } - // } - // } - // } + 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 { + 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_section_expanded(*section, cx); + } + }, + ListEntry::Contact { contact, calling } => { + if contact.online && !contact.busy && !calling { + self.call(contact.user.id, cx); + } + } + // ListEntry::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); + // } + // } + // ListEntry::ParticipantScreen { peer_id, .. } => { + // let Some(peer_id) = peer_id else { + // return; + // }; + // if let Some(workspace) = self.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // workspace.open_shared_screen(*peer_id, cx) + // }); + // } + // } + ListEntry::Channel { channel, .. } => { + let is_active = maybe!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + + Some(call_channel == channel.id) + }) + .unwrap_or(false); + if is_active { + self.open_channel_notes(channel.id, cx) + } else { + self.join_channel(channel.id, cx) + } + } + ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), + _ => {} + } + } + } + } - // fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { - // if self.channel_editing_state.is_some() { - // self.channel_name_editor.update(cx, |editor, cx| { - // editor.insert(" ", cx); - // }); - // } - // } + fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { + if self.channel_editing_state.is_some() { + self.channel_name_editor.update(cx, |editor, cx| { + editor.insert(" ", 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 { - // location, - // 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.create_channel(&channel_name, *location, cx) - // }) - // .detach(); - // cx.notify(); - // } - // ChannelEditingState::Rename { - // location, - // 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(*location, &channel_name, cx) - // }) - // .detach(); - // cx.notify(); - // } - // } - // cx.focus_self(); - // true - // } else { - // false - // } - // } + 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 { + location, + 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.create_channel(&channel_name, *location, cx) + }) + .detach(); + cx.notify(); + } + ChannelEditingState::Rename { + location, + 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(*location, &channel_name, cx) + }) + .detach(); + cx.notify(); + } + } + cx.focus_self(); + true + } else { + false + } + } - // fn toggle_section_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(false, cx); - // } + fn toggle_section_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(false, cx); + } - // fn collapse_selected_channel( - // &mut self, - // _: &CollapseSelectedChannel, - // cx: &mut ViewContext, - // ) { - // let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { - // return; - // }; + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; - // if self.is_channel_collapsed(channel_id) { - // return; - // } + if self.is_channel_collapsed(channel_id) { + return; + } - // self.toggle_channel_collapsed(channel_id, cx); - // } + self.toggle_channel_collapsed(channel_id, cx); + } - // fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - // let Some(id) = self.selected_channel().map(|channel| channel.id) else { - // return; - // }; + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; - // if !self.is_channel_collapsed(id) { - // return; - // } + if !self.is_channel_collapsed(id) { + return; + } - // self.toggle_channel_collapsed(id, cx) - // } + self.toggle_channel_collapsed(id, cx) + } // fn toggle_channel_collapsed_action( // &mut self, @@ -2933,143 +1989,172 @@ impl CollabPanel { // self.toggle_channel_collapsed(action.location, cx); // } - // fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { - // match self.collapsed_channels.binary_search(&channel_id) { - // Ok(ix) => { - // self.collapsed_channels.remove(ix); - // } - // Err(ix) => { - // self.collapsed_channels.insert(ix, channel_id); - // } - // }; - // self.serialize(cx); - // self.update_entries(true, cx); - // cx.notify(); - // cx.focus_self(); - // } + fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + match self.collapsed_channels.binary_search(&channel_id) { + Ok(ix) => { + self.collapsed_channels.remove(ix); + } + Err(ix) => { + self.collapsed_channels.insert(ix, channel_id); + } + }; + self.serialize(cx); + self.update_entries(true, cx); + cx.notify(); + cx.focus_self(); + } - // fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { - // self.collapsed_channels.binary_search(&channel_id).is_ok() - // } + fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel_id).is_ok() + } - // fn leave_call(cx: &mut ViewContext) { - // ActiveCall::global(cx) - // .update(cx, |call, cx| call.hang_up(cx)) - // .detach_and_log_err(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| { - // workspace.toggle_modal(cx, |_, cx| { - // cx.add_view(|cx| { - // let mut finder = ContactFinder::new(self.user_store.clone(), cx); - // finder.set_query(self.filter_editor.read(cx).text(cx), cx); - // finder - // }) - // }); - // }); - // } - // } + fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + let mut finder = ContactFinder::new(self.user_store.clone(), cx); + finder.set_query(self.filter_editor.read(cx).text(cx), cx); + finder + }); + }); + } + } - // fn new_root_channel(&mut self, cx: &mut ViewContext) { - // self.channel_editing_state = Some(ChannelEditingState::Create { - // location: None, - // pending_name: None, - // }); - // self.update_entries(false, cx); - // self.select_channel_editor(); - // cx.focus(self.channel_name_editor.as_any()); - // cx.notify(); - // } + fn new_root_channel(&mut self, cx: &mut ViewContext) { + self.channel_editing_state = Some(ChannelEditingState::Create { + location: None, + pending_name: None, + }); + self.update_entries(false, cx); + self.select_channel_editor(); + cx.focus_view(&self.channel_name_editor); + cx.notify(); + } - // fn select_channel_editor(&mut self) { - // self.selection = self.entries.iter().position(|entry| match entry { - // ListEntry::ChannelEditor { .. } => true, - // _ => false, - // }); - // } + 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.collapsed_channels - // .retain(|channel| *channel != action.location); - // self.channel_editing_state = Some(ChannelEditingState::Create { - // location: Some(action.location.to_owned()), - // pending_name: None, - // }); - // self.update_entries(false, cx); - // self.select_channel_editor(); - // cx.focus(self.channel_name_editor.as_any()); - // cx.notify(); - // } + fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|channel| *channel != channel_id); + self.channel_editing_state = Some(ChannelEditingState::Create { + location: Some(channel_id), + pending_name: None, + }); + self.update_entries(false, cx); + self.select_channel_editor(); + cx.focus_view(&self.channel_name_editor); + cx.notify(); + } - // fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext) { - // self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); - // } + fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + todo!(); + // self.show_channel_modal(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 manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + todo!(); + // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); + } - // fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - // if let Some(channel) = self.selected_channel() { - // self.remove_channel(channel.id, cx) - // } - // } + fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.remove_channel(channel.id, cx) + } + } - // 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_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.rename_channel(channel.id, cx); + } + } - // fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { - // let channel_store = self.channel_store.read(cx); - // if !channel_store.is_channel_admin(action.channel_id) { - // return; - // } - // if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { - // self.channel_editing_state = Some(ChannelEditingState::Rename { - // location: action.channel_id.to_owned(), - // pending_name: None, - // }); - // self.channel_name_editor.update(cx, |editor, cx| { - // editor.set_text(channel.name.clone(), cx); - // editor.select_all(&Default::default(), cx); - // }); - // cx.focus(self.channel_name_editor.as_any()); - // self.update_entries(false, cx); - // self.select_channel_editor(); - // } - // } + fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + if !channel_store.is_channel_admin(channel_id) { + return; + } + if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() { + self.channel_editing_state = Some(ChannelEditingState::Rename { + location: 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); + }); + cx.focus_view(&self.channel_name_editor); + self.update_entries(false, cx); + self.select_channel_editor(); + } + } - // fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { - // if let Some(workspace) = self.workspace.upgrade(cx) { - // ChannelView::open(action.channel_id, workspace, cx).detach(); - // } - // } + fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + self.channel_clipboard = Some(ChannelMoveClipboard { channel_id }); + } - // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { - // let Some(channel) = self.selected_channel() else { - // return; - // }; + fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.channel_clipboard = Some(ChannelMoveClipboard { + channel_id: channel.id, + }) + } + } - // self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); - // } + fn move_channel_on_clipboard( + &mut self, + to_channel_id: ChannelId, + cx: &mut ViewContext, + ) { + if let Some(clipboard) = self.channel_clipboard.take() { + self.channel_store.update(cx, |channel_store, cx| { + channel_store + .move_channel(clipboard.channel_id, Some(to_channel_id), cx) + .detach_and_log_err(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 open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + todo!(); + // ChannelView::open(action.channel_id, workspace, cx).detach(); + } + } + + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { + let Some(channel) = self.selected_channel() else { + return; + }; + let Some(bounds) = self + .selection + .and_then(|ix| self.scroll_handle.bounds_for_item(ix)) + else { + return; + }; + + self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); + cx.stop_propagation(); + } + + 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, @@ -3108,77 +2193,65 @@ impl CollabPanel { // 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!( - // "Are you sure you want to remove the channel \"{}\"?", - // channel.name - // ); - // let mut answer = - // cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); - // let window = cx.window(); - // 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)) - // .await - // { - // window.prompt( - // PromptLevel::Info, - // &format!("Failed to remove channel: {}", e), - // &["Ok"], - // &mut cx, - // ); - // } - // this.update(&mut cx, |_, cx| cx.focus_self()).ok(); - // } - // }) - // .detach(); - // } - // } + 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!( + "Are you sure you want to remove the channel \"{}\"?", + channel.name + ); + let mut answer = + cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + let window = cx.window(); + cx.spawn(|this, mut cx| async move { + if answer.await? == 0 { + channel_store + .update(&mut cx, |channels, _| channels.remove_channel(channel_id))? + .await + .notify_async_err(&mut cx); + this.update(&mut cx, |_, cx| cx.focus_self()).ok(); + } + anyhow::Ok(()) + }) + .detach(); + } + } // // 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!( - // "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 = cx.window(); - // 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 - // { - // window.prompt( - // PromptLevel::Info, - // &format!("Failed to remove contact: {}", e), - // &["Ok"], - // &mut cx, - // ); - // } - // } - // }) - // .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!( + "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 = cx.window(); + cx.spawn(|_, mut cx| async move { + if answer.await? == 0 { + user_store + .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))? + .await + .notify_async_err(&mut cx); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } - // 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 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_and_log_err(cx); + } // fn respond_to_channel_invite( // &mut self, @@ -3193,60 +2266,915 @@ impl CollabPanel { // .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); - // } + fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| { + call.invite(recipient_user_id, Some(self.project.clone()), cx) + }) + .detach_and_log_err(cx); + } - // fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { - // let Some(workspace) = self.workspace.upgrade(cx) else { - // return; - // }; - // let Some(handle) = cx.window().downcast::() else { - // return; - // }; - // workspace::join_channel( - // channel_id, - // workspace.read(cx).app_state().clone(), - // Some(handle), - // cx, - // ) - // .detach_and_log_err(cx) - // } + fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { + let Some(handle) = cx.window_handle().downcast::() else { + return; + }; + let active_call = ActiveCall::global(cx); + cx.spawn(|_, mut cx| async move { + active_call + .update(&mut cx, |active_call, cx| { + active_call.join_channel(channel_id, Some(handle), cx) + }) + .log_err()? + .await + .notify_async_err(&mut cx) + }) + .detach() + } - // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { - // let channel_id = action.channel_id; - // if let Some(workspace) = self.workspace.upgrade(cx) { - // cx.app_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // if let Some(panel) = workspace.focus_panel::(cx) { - // panel.update(cx, |panel, cx| { - // panel - // .select_channel(channel_id, None, cx) - // .detach_and_log_err(cx); - // }); - // } - // }); - // }); - // } - // } + fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + todo!(); + // if let Some(panel) = workspace.focus_panel::(cx) { + // panel.update(cx, |panel, cx| { + // panel + // .select_channel(channel_id, None, cx) + // .detach_and_log_err(cx); + // }); + // } + }); + }); + } - // fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { - // let channel_store = self.channel_store.read(cx); - // let Some(channel) = channel_store.channel_for_id(action.channel_id) else { - // return; - // }; - // let item = ClipboardItem::new(channel.link()); - // cx.write_to_clipboard(item) - // } + fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + let Some(channel) = channel_store.channel_for_id(channel_id) else { + return; + }; + let item = ClipboardItem::new(channel.link()); + cx.write_to_clipboard(item) + } + + fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { + v_stack().child( + Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener( + |this, _, cx| { + let client = this.client.clone(); + cx.spawn(|_, mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach() + }, + )), + ) + } + + fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { + v_stack() + .size_full() + .child( + div() + .p_2() + .child(div().rounded(px(2.0)).child(self.filter_editor.clone())), + ) + .child( + v_stack() + .size_full() + .id("scroll") + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .children( + self.entries + .clone() + .into_iter() + .enumerate() + .map(|(ix, entry)| { + let is_selected = self.selection == Some(ix); + match entry { + ListEntry::Header(section) => { + let is_collapsed = + self.collapsed_sections.contains(§ion); + self.render_header(section, is_selected, is_collapsed, cx) + .into_any_element() + } + ListEntry::Contact { contact, calling } => self + .render_contact(&*contact, calling, is_selected, cx) + .into_any_element(), + ListEntry::ContactPlaceholder => self + .render_contact_placeholder(is_selected, cx) + .into_any_element(), + ListEntry::IncomingRequest(user) => self + .render_contact_request(user, true, is_selected, cx) + .into_any_element(), + ListEntry::OutgoingRequest(user) => self + .render_contact_request(user, false, is_selected, cx) + .into_any_element(), + ListEntry::Channel { + channel, + depth, + has_children, + } => self + .render_channel( + &*channel, + depth, + has_children, + is_selected, + ix, + cx, + ) + .into_any_element(), + ListEntry::ChannelEditor { depth } => { + self.render_channel_editor(depth, cx).into_any_element() + } + } + }), + ), + ) + } + + fn render_header( + &mut self, + section: Section, + is_selected: bool, + is_collapsed: bool, + cx: &ViewContext, + ) -> impl IntoElement { + // let mut channel_link = None; + // let mut channel_tooltip_text = None; + // let mut channel_icon = None; + // let mut is_dragged_over = false; + + let text = match section { + Section::ActiveCall => { + // let channel_name = maybe!({ + // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + + // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; + + // channel_link = Some(channel.link()); + // (channel_icon, channel_tooltip_text) = match channel.visibility { + // proto::ChannelVisibility::Public => { + // (Some("icons/public.svg"), Some("Copy public channel link.")) + // } + // proto::ChannelVisibility::Members => { + // (Some("icons/hash.svg"), Some("Copy private channel link.")) + // } + // }; + + // Some(channel.name.as_str()) + // }); + + // if let Some(name) = channel_name { + // SharedString::from(format!("{}", name)) + // } else { + // SharedString::from("Current Call") + // } + todo!() + } + Section::ContactRequests => SharedString::from("Requests"), + Section::Contacts => SharedString::from("Contacts"), + Section::Channels => SharedString::from("Channels"), + Section::ChannelInvites => SharedString::from("Invites"), + Section::Online => SharedString::from("Online"), + Section::Offline => SharedString::from("Offline"), + }; + + let button = match section { + Section::ActiveCall => + // channel_link.map(|channel_link| { + // let channel_link_copy = channel_link.clone(); + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .leave_call_button + // .style_for(is_selected, state), + // "icons/link.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, _, cx| { + // let item = ClipboardItem::new(channel_link_copy.clone()); + // cx.write_to_clipboard(item) + // }) + // .with_tooltip::( + // 0, + // channel_tooltip_text.unwrap(), + // None, + // tooltip_style.clone(), + // cx, + // ) + // }), + { + todo!() + } + Section::Contacts => Some( + IconButton::new("add-contact", Icon::Plus) + .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) + .tooltip(|cx| Tooltip::text("Search for new contact", cx)), + ), + Section::Channels => Some( + IconButton::new("add-channel", Icon::Plus) + .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) + .tooltip(|cx| Tooltip::text("Create a channel", cx)), + ), + _ => None, + }; + + let can_collapse = match section { + Section::ActiveCall | Section::Channels | Section::Contacts => false, + Section::ChannelInvites + | Section::ContactRequests + | Section::Online + | Section::Offline => true, + }; + + h_stack() + .w_full() + .map(|el| { + if can_collapse { + el.child( + ListItem::new(text.clone()) + .child(div().w_full().child(Label::new(text))) + .selected(is_selected) + .toggle(Some(!is_collapsed)) + .on_click(cx.listener(move |this, _, cx| { + this.toggle_section_expanded(section, cx) + })), + ) + } else { + el.child( + ListHeader::new(text) + .when_some(button, |el, button| el.right_button(button)) + .selected(is_selected), + ) + } + }) + .when(section == Section::Channels, |el| { + el.drag_over::(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .on_drop(cx.listener( + move |this, view: &View, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(view.read(cx).channel.id, None, cx) + }) + .detach_and_log_err(cx) + }, + )) + }) + } + + fn render_contact( + &mut self, + contact: &Contact, + calling: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + enum ContactTooltip {} + + let online = contact.online; + let busy = contact.busy || calling; + let user_id = contact.user.id; + let github_login = SharedString::from(contact.user.github_login.clone()); + let mut item = ListItem::new(github_login.clone()) + .on_click(cx.listener(move |this, _, cx| { + this.workspace + .update(cx, |this, cx| { + this.call_state() + .invite(user_id, None, cx) + .detach_and_log_err(cx) + }) + .log_err(); + })) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(github_login.clone())) + .when(calling, |el| { + el.child(Label::new("Calling").color(Color::Muted)) + }) + .when(!calling, |el| { + el.child( + div() + .id("remove_contact") + .invisible() + .group_hover("", |style| style.visible()) + .child( + IconButton::new("remove_contact", Icon::Close) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), + ), + ) + }), + ) + .left_child( + // todo!() handle contacts with no avatar + Avatar::data(contact.user.avatar.clone().unwrap()) + .availability_indicator(if online { Some(!busy) } else { None }), + ) + .when(online && !busy, |el| { + el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) + }); + + div() + .id(github_login.clone()) + .group("") + .child(item) + .tooltip(move |cx| { + let text = if !online { + format!(" {} is offline", &github_login) + } else if busy { + format!(" {} is on a call", &github_login) + } else { + let room = ActiveCall::global(cx).read(cx).room(); + if room.is_some() { + format!("Invite {} to join call", &github_login) + } else { + format!("Call {}", &github_login) + } + }; + Tooltip::text(text, cx) + }) + } + + fn render_contact_request( + &mut self, + user: Arc, + is_incoming: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let github_login = SharedString::from(user.github_login.clone()); + let user_id = user.id; + let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user); + let color = if is_contact_request_pending { + Color::Muted + } else { + Color::Default + }; + + let controls = if is_incoming { + vec![ + IconButton::new("remove_contact", Icon::Close) + .on_click(cx.listener(move |this, _, cx| { + this.respond_to_contact_request(user_id, false, cx); + })) + .icon_color(color) + .tooltip(|cx| Tooltip::text("Decline invite", cx)), + IconButton::new("remove_contact", Icon::Check) + .on_click(cx.listener(move |this, _, cx| { + this.respond_to_contact_request(user_id, true, cx); + })) + .icon_color(color) + .tooltip(|cx| Tooltip::text("Accept invite", cx)), + ] + } else { + let github_login = github_login.clone(); + vec![IconButton::new("remove_contact", Icon::Close) + .on_click(cx.listener(move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + })) + .icon_color(color) + .tooltip(|cx| Tooltip::text("Cancel invite", cx))] + }; + + ListItem::new(github_login.clone()) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(github_login.clone())) + .child(h_stack().children(controls)), + ) + .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar)) + } + + fn render_contact_placeholder( + &self, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + ListItem::new("contact-placeholder") + .child(IconElement::new(Icon::Plus)) + .child(Label::new("Add a Contact")) + .selected(is_selected) + .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) + } + + fn render_channel( + &self, + channel: &Channel, + depth: usize, + has_children: bool, + is_selected: bool, + ix: usize, + cx: &mut ViewContext, + ) -> impl IntoElement { + let channel_id = channel.id; + + let is_active = maybe!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + Some(call_channel == channel_id) + }) + .unwrap_or(false); + let is_public = self + .channel_store + .read(cx) + .channel_for_id(channel_id) + .map(|channel| channel.visibility) + == Some(proto::ChannelVisibility::Public); + let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); + + let has_messages_notification = channel.unseen_message_id.is_some(); + let has_notes_notification = channel.unseen_note_version.is_some(); + + const FACEPILE_LIMIT: usize = 3; + let participants = self.channel_store.read(cx).channel_participants(channel_id); + + let face_pile = if !participants.is_empty() { + let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + let user = &participants[0]; + + let result = FacePile { + faces: participants + .iter() + .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element())) + .take(FACEPILE_LIMIT) + .chain(if extra_count > 0 { + // todo!() @nate - this label looks wrong. + Some(Label::new(format!("+{}", extra_count)).into_any_element()) + } else { + None + }) + .collect::>(), + }; + + Some(result) + } else { + None + }; + + let width = self.width.unwrap_or(px(240.)); + + div() + .id(channel_id as usize) + .group("") + .on_drag({ + let channel = channel.clone(); + move |cx| { + let channel = channel.clone(); + cx.build_view({ |cx| DraggedChannelView { channel, width } }) + } + }) + .drag_over::(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .on_drop( + cx.listener(move |this, view: &View, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel( + view.read(cx).channel.id, + Some(channel_id), + cx, + ) + }) + .detach_and_log_err(cx) + }), + ) + .child( + ListItem::new(channel_id as usize) + .indent_level(depth) + .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle + .left_icon(if is_public { Icon::Public } else { Icon::Hash }) + .selected(is_selected || is_active) + .child( + h_stack() + .w_full() + .justify_between() + .child( + h_stack() + .id(channel_id as usize) + .child(Label::new(channel.name.clone())) + .children(face_pile.map(|face_pile| face_pile.render(cx))), + ) + .child( + h_stack() + .child( + div() + .id("channel_chat") + .when(!has_messages_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new( + "channel_chat", + Icon::MessageBubbles, + ) + .icon_color(if has_messages_notification { + Color::Default + } else { + Color::Muted + }), + ) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)), + ) + .child( + div() + .id("channel_notes") + .when(!has_notes_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_notes", Icon::File) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .tooltip(|cx| { + Tooltip::text("Open channel notes", cx) + }), + ), + ), + ), + ) + .toggle(disclosed) + .on_toggle( + cx.listener(move |this, _, cx| { + this.toggle_channel_collapsed(channel_id, cx) + }), + ) + .on_click(cx.listener(move |this, _, cx| { + if this.drag_target_channel == ChannelDragTarget::None { + if is_active { + this.open_channel_notes(channel_id, cx) + } else { + this.join_channel(channel_id, cx) + } + } + })) + .on_secondary_mouse_down(cx.listener( + move |this, event: &MouseDownEvent, cx| { + this.deploy_channel_context_menu(event.position, channel_id, ix, cx) + }, + )), + ) + .tooltip(|cx| Tooltip::text("Join channel", cx)) + + // let channel_id = channel.id; + // let collab_theme = &theme.collab_panel; + // let is_public = self + // .channel_store + // .read(cx) + // .channel_for_id(channel_id) + // .map(|channel| channel.visibility) + // == Some(proto::ChannelVisibility::Public); + // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + // let disclosed = + // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); + + // enum ChannelCall {} + // enum ChannelNote {} + // enum NotesTooltip {} + // enum ChatTooltip {} + // enum ChannelTooltip {} + + // let mut is_dragged_over = false; + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) + // { + // is_dragged_over = true; + // } + + // let has_messages_notification = channel.unseen_message_id.is_some(); + + // MouseEventHandler::new::(ix, cx, |state, cx| { + // let row_hovered = state.hovered(); + + // let mut select_state = |interactive: &Interactive| { + // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { + // interactive.clicked.as_ref().unwrap().clone() + // } else if state.hovered() || other_selected { + // interactive + // .hovered + // .as_ref() + // .unwrap_or(&interactive.default) + // .clone() + // } else { + // interactive.default.clone() + // } + // }; + + // Flex::::row() + // .with_child( + // Svg::new(if is_public { + // "icons/public.svg" + // } else { + // "icons/hash.svg" + // }) + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child({ + // let style = collab_theme.channel_name.inactive_state(); + // Flex::row() + // .with_child( + // Label::new(channel.name.clone(), style.text.clone()) + // .contained() + // .with_style(style.container) + // .aligned() + // .left() + // .with_tooltip::( + // ix, + // "Join channel", + // None, + // theme.tooltip.clone(), + // cx, + // ), + // ) + // .with_children({ + // let participants = + // self.channel_store.read(cx).channel_participants(channel_id); + + // if !participants.is_empty() { + // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + // let result = FacePile::new(collab_theme.face_overlap) + // .with_children( + // participants + // .iter() + // .filter_map(|user| { + // Some( + // Image::from_data(user.avatar.clone()?) + // .with_style(collab_theme.channel_avatar), + // ) + // }) + // .take(FACEPILE_LIMIT), + // ) + // .with_children((extra_count > 0).then(|| { + // Label::new( + // format!("+{}", extra_count), + // collab_theme.extra_participant_label.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.extra_participant_label.container) + // })); + + // Some(result) + // } else { + // None + // } + // }) + // .with_spacing(8.) + // .align_children_center() + // .flex(1., true) + // }) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + + // if channel.unseen_message_id.is_some() { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_note_active_color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else if row_hovered { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + // }) + // .with_tooltip::( + // ix, + // "Open channel chat", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .contained() + // .with_margin_right(4.), + // ) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + // if row_hovered || channel.unseen_note_version.is_some() { + // Svg::new("icons/file.svg") + // .with_color(if channel.unseen_note_version.is_some() { + // collab_theme.channel_note_active_color + // } else { + // collab_theme.channel_hash.color + // }) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .with_tooltip::( + // ix as usize, + // "Open channel notes", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else if has_messages_notification { + // Empty::new() + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + // }), + // ) + // .align_children_center() + // .styleable_component() + // .disclosable( + // disclosed, + // Box::new(ToggleCollapse { + // location: channel.id.clone(), + // }), + // ) + // .with_id(ix) + // .with_style(collab_theme.disclosure.clone()) + // .element() + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(select_state( + // collab_theme + // .channel_row + // .in_state(is_selected || is_active || is_dragged_over), + // )) + // .with_padding_left( + // collab_theme.channel_row.default_style().padding.left + // + collab_theme.channel_indent * depth as f32, + // ) + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if this. + // drag_target_channel == ChannelDragTarget::None { + // if is_active { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) + // } else { + // this.join_channel(channel_id, cx) + // } + // } + // }) + // .on_click(MouseButton::Right, { + // let channel = channel.clone(); + // move |e, this, cx| { + // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); + // } + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) + // }) + // .detach_and_log_err(cx) + // } + // }) + // .on_move({ + // let channel = channel.clone(); + // move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // if channel.id != dragged_channel.id { + // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); + // } + // cx.notify() + // } + // } + // }) + // .as_draggable::<_, Channel>( + // channel.clone(), + // move |_, channel, cx: &mut ViewContext| { + // let theme = &theme::current(cx).collab_panel; + + // Flex::::row() + // .with_child( + // Svg::new("icons/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.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) + // .aligned() + // .left(), + // ) + // .align_children_center() + // .contained() + // .with_background_color( + // theme + // .container + // .background_color + // .unwrap_or(gpui::color::Color::transparent_black()), + // ) + // .contained() + // .with_padding_left( + // theme.channel_row.default_style().padding.left + // + theme.channel_indent * depth as f32, + // ) + // .into_any() + // }, + // ) + // .with_cursor_style(CursorStyle::PointingHand) + // .into_any() + } + + fn render_channel_editor( + &mut self, + depth: usize, + cx: &mut ViewContext, + ) -> impl IntoElement { + let item = ListItem::new("channel-editor") + .inset(false) + .indent_level(depth) + .left_icon(Icon::Hash); + + if let Some(pending_name) = self + .channel_editing_state + .as_ref() + .and_then(|state| state.pending_name()) + { + item.child(Label::new(pending_name)) + } else { + item.child( + div() + .w_full() + .py_1() // todo!() @nate this is a px off at the default font size. + .child(self.channel_name_editor.clone()), + ) + } + } } // fn render_tree_branch( @@ -3297,11 +3225,41 @@ impl CollabPanel { impl Render for CollabPanel { type Element = Focusable
; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - div() + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + v_stack() .key_context("CollabPanel") + .on_action(cx.listener(CollabPanel::cancel)) + .on_action(cx.listener(CollabPanel::select_next)) + .on_action(cx.listener(CollabPanel::select_prev)) + .on_action(cx.listener(CollabPanel::confirm)) + .on_action(cx.listener(CollabPanel::insert_space)) + // .on_action(cx.listener(CollabPanel::remove)) + .on_action(cx.listener(CollabPanel::remove_selected_channel)) + .on_action(cx.listener(CollabPanel::show_inline_context_menu)) + // .on_action(cx.listener(CollabPanel::new_subchannel)) + // .on_action(cx.listener(CollabPanel::invite_members)) + // .on_action(cx.listener(CollabPanel::manage_members)) + .on_action(cx.listener(CollabPanel::rename_selected_channel)) + // .on_action(cx.listener(CollabPanel::rename_channel)) + // .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action)) + .on_action(cx.listener(CollabPanel::collapse_selected_channel)) + .on_action(cx.listener(CollabPanel::expand_selected_channel)) + // .on_action(cx.listener(CollabPanel::open_channel_notes)) + // .on_action(cx.listener(CollabPanel::join_channel_chat)) + // .on_action(cx.listener(CollabPanel::copy_channel_link)) .track_focus(&self.focus_handle) - .child("COLLAB PANEL") + .size_full() + .child(if self.user_store.read(cx).current_user().is_none() { + self.render_signed_out(cx) + } else { + self.render_signed_in(cx) + }) + .children(self.context_menu.as_ref().map(|(menu, position, _)| { + overlay() + .position(*position) + .anchor(gpui::AnchorCorner::TopLeft) + .child(menu.clone()) + })) } } @@ -3424,14 +3382,15 @@ impl Panel for CollabPanel { } fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width) + self.width.map_or_else( + || CollaborationPanelSettings::get_global(cx).default_width, + |width| width.0, + ) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - // todo!() - // self.serialize(cx); + self.width = size.map(|s| px(s)); + self.serialize(cx); cx.notify(); } @@ -3455,111 +3414,111 @@ impl Panel for CollabPanel { } impl FocusableView for CollabPanel { - fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.filter_editor.focus_handle(cx).clone() } } -// impl PartialEq for ListEntry { -// fn eq(&self, other: &Self) -> bool { -// match self { -// ListEntry::Header(section_1) => { -// if let ListEntry::Header(section_2) = other { -// return section_1 == section_2; -// } -// } -// ListEntry::CallParticipant { user: user_1, .. } => { -// if let ListEntry::CallParticipant { user: user_2, .. } = other { -// return user_1.id == user_2.id; -// } -// } -// ListEntry::ParticipantProject { -// project_id: project_id_1, -// .. -// } => { -// if let ListEntry::ParticipantProject { -// project_id: project_id_2, -// .. -// } = other -// { -// return project_id_1 == project_id_2; -// } -// } -// ListEntry::ParticipantScreen { -// peer_id: peer_id_1, .. -// } => { -// if let ListEntry::ParticipantScreen { -// peer_id: peer_id_2, .. -// } = other -// { -// return peer_id_1 == peer_id_2; -// } -// } -// ListEntry::Channel { -// channel: channel_1, .. -// } => { -// if let ListEntry::Channel { -// channel: channel_2, .. -// } = other -// { -// return channel_1.id == channel_2.id; -// } -// } -// ListEntry::ChannelNotes { channel_id } => { -// if let ListEntry::ChannelNotes { -// channel_id: other_id, -// } = other -// { -// return channel_id == other_id; -// } -// } -// ListEntry::ChannelChat { channel_id } => { -// if let ListEntry::ChannelChat { -// channel_id: other_id, -// } = other -// { -// return channel_id == other_id; -// } -// } -// ListEntry::ChannelInvite(channel_1) => { -// if let ListEntry::ChannelInvite(channel_2) = other { -// return channel_1.id == channel_2.id; -// } -// } -// ListEntry::IncomingRequest(user_1) => { -// if let ListEntry::IncomingRequest(user_2) = other { -// return user_1.id == user_2.id; -// } -// } -// ListEntry::OutgoingRequest(user_1) => { -// if let ListEntry::OutgoingRequest(user_2) = other { -// return user_1.id == user_2.id; -// } -// } -// ListEntry::Contact { -// contact: contact_1, .. -// } => { -// 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; -// } -// } -// ListEntry::ContactPlaceholder => { -// if let ListEntry::ContactPlaceholder = other { -// return true; -// } -// } -// } -// false -// } -// } +impl PartialEq for ListEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ListEntry::Header(section_1) => { + if let ListEntry::Header(section_2) = other { + return section_1 == section_2; + } + } + // ListEntry::CallParticipant { user: user_1, .. } => { + // if let ListEntry::CallParticipant { user: user_2, .. } = other { + // return user_1.id == user_2.id; + // } + // } + // ListEntry::ParticipantProject { + // project_id: project_id_1, + // .. + // } => { + // if let ListEntry::ParticipantProject { + // project_id: project_id_2, + // .. + // } = other + // { + // return project_id_1 == project_id_2; + // } + // } + // ListEntry::ParticipantScreen { + // peer_id: peer_id_1, .. + // } => { + // if let ListEntry::ParticipantScreen { + // peer_id: peer_id_2, .. + // } = other + // { + // return peer_id_1 == peer_id_2; + // } + // } + ListEntry::Channel { + channel: channel_1, .. + } => { + if let ListEntry::Channel { + channel: channel_2, .. + } = other + { + return channel_1.id == channel_2.id; + } + } + // ListEntry::ChannelNotes { channel_id } => { + // if let ListEntry::ChannelNotes { + // channel_id: other_id, + // } = other + // { + // return channel_id == other_id; + // } + // } + // ListEntry::ChannelChat { channel_id } => { + // if let ListEntry::ChannelChat { + // channel_id: other_id, + // } = other + // { + // return channel_id == other_id; + // } + // } + // ListEntry::ChannelInvite(channel_1) => { + // if let ListEntry::ChannelInvite(channel_2) = other { + // return channel_1.id == channel_2.id; + // } + // } + ListEntry::IncomingRequest(user_1) => { + if let ListEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ListEntry::OutgoingRequest(user_1) => { + if let ListEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ListEntry::Contact { + contact: contact_1, .. + } => { + 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; + } + } + ListEntry::ContactPlaceholder => { + if let ListEntry::ContactPlaceholder = other { + return true; + } + } + } + false + } +} // fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { // Svg::new(svg_path) @@ -3573,3 +3532,34 @@ impl FocusableView for CollabPanel { // .contained() // .with_style(style.container) // } + +struct DraggedChannelView { + channel: Channel, + width: Pixels, +} + +impl Render for DraggedChannelView { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + h_stack() + .font(ui_font) + .bg(cx.theme().colors().background) + .w(self.width) + .p_1() + .gap_1() + .child( + IconElement::new( + if self.channel.visibility == proto::ChannelVisibility::Public { + Icon::Public + } else { + Icon::Hash + }, + ) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new(self.channel.name.clone())) + } +} diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index d0c12a7f90a430a70615f6c4b91ca555619081fe..717ab2d8970bdba55d88dd51dc207b03bc146f3c 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -1,37 +1,34 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, + div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle, + FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View, + ViewContext, VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use util::TryFutureExt; -use workspace::Modal; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack, Label}; +use util::{ResultExt as _, TryFutureExt}; pub fn init(cx: &mut AppContext) { - Picker::::init(cx); - cx.add_action(ContactFinder::dismiss) + //Picker::::init(cx); + //cx.add_action(ContactFinder::dismiss) } pub struct ContactFinder { - picker: ViewHandle>, + picker: View>, has_focus: bool, } 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(); + pub fn new(user_store: Model, cx: &mut ViewContext) -> Self { + let delegate = ContactFinderDelegate { + parent: cx.view().downgrade(), + user_store, + potential_contacts: Arc::from([]), + selected_index: 0, + }; + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); Self { picker, @@ -41,105 +38,72 @@ impl ContactFinder { pub fn set_query(&mut self, query: String, cx: &mut ViewContext) { self.picker.update(cx, |picker, cx| { - picker.set_query(query, cx); + // todo!() + // picker.set_query(query, cx); }); } - - fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(PickerEvent::Dismiss); - } -} - -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() +impl Render for ContactFinder { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render_mode_button(text: &'static str) -> AnyElement { + Label::new(text).into_any_element() } - 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), + v_stack() + .child( + v_stack() + .child(Label::new("Contacts")) + .child(h_stack().children([render_mode_button("Invite new contacts")])) + .bg(cx.theme().colors().element_background), ) - .constrained() - .with_max_height(theme.max_height) - .with_max_width(theme.max_width) - .contained() - .with_style(theme.modal) - .into_any() + .child(self.picker.clone()) + .w_96() } - 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_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; - } + // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + // self.has_focus = false; + // } + + type Element = Div; } -impl Modal for ContactFinder { - fn has_focus(&self) -> bool { - self.has_focus - } +// 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, - } - } -} +// fn dismiss_on_event(event: &Self::Event) -> bool { +// match event { +// PickerEvent::Dismiss => true, +// } +// } +// } pub struct ContactFinderDelegate { + parent: WeakView, potential_contacts: Arc<[Arc]>, - user_store: ModelHandle, + user_store: Model, selected_index: usize, } -impl PickerDelegate for ContactFinderDelegate { - fn placeholder_text(&self) -> Arc { - "Search collaborator by username...".into() +impl EventEmitter for ContactFinder {} + +impl FocusableView for ContactFinder { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } +} +impl PickerDelegate for ContactFinderDelegate { + type ListItem = Div; fn match_count(&self) -> usize { self.potential_contacts.len() } @@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate { self.selected_index = ix; } + fn placeholder_text(&self) -> Arc { + "Search collaborator by username...".into() + } + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { let search_users = self .user_store @@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate { async { let potential_contacts = search_users.await?; picker.update(&mut cx, |picker, cx| { - picker.delegate_mut().potential_contacts = potential_contacts.into(); + picker.delegate.potential_contacts = potential_contacts.into(); cx.notify(); })?; anyhow::Ok(()) @@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - cx.emit(PickerEvent::Dismiss); + //cx.emit(PickerEvent::Dismiss); + self.parent + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &gpui::AppContext, - ) -> AnyElement> { - let full_theme = &theme::current(cx); - let theme = &full_theme.collab_panel.contact_finder; - let tabbed_modal = &full_theme.collab_panel.tabbed_modal; + cx: &mut ViewContext>, + ) -> Option { let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); @@ -214,48 +181,46 @@ impl PickerDelegate for ContactFinderDelegate { ContactRequestStatus::RequestSent => Some("icons/x.svg"), ContactRequestStatus::RequestAccepted => 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 = tabbed_modal - .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(tabbed_modal.row_height) - .into_any() + Some( + div() + .flex_1() + .justify_between() + .children(user.avatar.clone().map(|avatar| img(avatar))) + .child(Label::new(user.github_login.clone())) + .children(icon_path.map(|icon_path| svg().path(icon_path))), + ) + // 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(tabbed_modal.row_height) + // .into_any() } } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 9eb285f0e7382500d7c644c63fde2bfab627d77c..d76242afa32e766ce5b885870e0fd20006ff6517 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,14 +31,17 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Div, InteractiveElement, IntoElement, Model, ParentElement, Render, - Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, - WeakView, WindowBounds, + div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton, + ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, + ViewContext, VisualContext, WeakView, WindowBounds, }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip}; -use workspace::Workspace; +use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip}; +use util::ResultExt; +use workspace::{notifications::NotifyResultExt, Workspace}; + +use crate::face_pile::FacePile; // const MAX_PROJECT_NAME_LENGTH: usize = 40; // const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let is_in_room = self + .workspace + .update(cx, |this, cx| this.call_state().is_in_room(cx)) + .unwrap_or_default(); + let is_shared = is_in_room && self.project.read(cx).is_shared(); + let current_user = self.user_store.read(cx).current_user(); + let client = self.client.clone(); + let users = self + .workspace + .update(cx, |this, cx| this.call_state().remote_participants(cx)) + .log_err() + .flatten(); + let mic_icon = if self + .workspace + .update(cx, |this, cx| this.call_state().is_muted(cx)) + .log_err() + .flatten() + .unwrap_or_default() + { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }; + let speakers_icon = if self + .workspace + .update(cx, |this, cx| this.call_state().is_deafened(cx)) + .log_err() + .flatten() + .unwrap_or_default() + { + ui::Icon::AudioOff + } else { + ui::Icon::AudioOn + }; + let workspace = self.workspace.clone(); h_stack() .id("titlebar") .justify_between() @@ -115,8 +153,8 @@ impl Render for CollabTitlebarItem { .border_color(gpui::red()) .id("project_owner_indicator") .child( - Button::new("player") - .variant(ButtonVariant::Ghost) + Button::new("player", "player") + .style(ButtonStyle2::Subtle) .color(Some(Color::Player(0))), ) .tooltip(move |cx| Tooltip::text("Toggle following", cx)), @@ -127,7 +165,10 @@ impl Render for CollabTitlebarItem { .border() .border_color(gpui::red()) .id("titlebar_project_menu_button") - .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .child( + Button::new("project_name", "project_name") + .style(ButtonStyle2::Subtle), + ) .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), ) // TODO - Add git menu @@ -137,8 +178,8 @@ impl Render for CollabTitlebarItem { .border_color(gpui::red()) .id("titlebar_git_menu_button") .child( - Button::new("branch_name") - .variant(ButtonVariant::Ghost) + Button::new("branch_name", "branch_name") + .style(ButtonStyle2::Subtle) .color(Some(Color::Muted)), ) .tooltip(move |cx| { @@ -155,8 +196,116 @@ impl Render for CollabTitlebarItem { .into() }), ), - ) // self.titlebar_item - .child(h_stack().child(Label::new("Right side titlebar item"))) + ) + .when_some( + users.zip(current_user.clone()), + |this, (remote_participants, current_user)| { + let mut pile = FacePile::default(); + pile.extend( + current_user + .avatar + .clone() + .map(|avatar| { + div().child(Avatar::data(avatar.clone())).into_any_element() + }) + .into_iter() + .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| { + user.avatar.as_ref().map(|avatar| { + div() + .child( + Avatar::data(avatar.clone()).into_element().into_any(), + ) + .on_mouse_down(MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.open_shared_screen(peer_id, cx); + }) + .log_err(); + } + }) + .into_any_element() + }) + })), + ); + this.child(pile.render(cx)) + }, + ) + .child(div().flex_1()) + .when(is_in_room, |this| { + this.child( + h_stack() + .child( + h_stack() + .child(Button::new( + "toggle_sharing", + if is_shared { "Unshare" } else { "Share" }, + )) + .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().hang_up(cx).detach(); + }) + .log_err(); + } + })), + ) + .child( + h_stack() + .child(IconButton::new("mute-microphone", mic_icon).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_mute(cx); + }) + .log_err(); + } + })) + .child(IconButton::new("mute-sound", speakers_icon).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_deafen(cx); + }) + .log_err(); + } + })) + .child(IconButton::new("screen-share", ui::Icon::Screen).on_click( + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_screen_share(cx); + }) + .log_err(); + }, + )) + .pl_2(), + ), + ) + }) + .map(|this| { + if let Some(user) = current_user { + this.when_some(user.avatar.clone(), |this, avatar| { + this.child(ui::Avatar::data(avatar)) + }) + } else { + this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { + let client = client.clone(); + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach(); + })) + } + }) } } diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index d2e6b28115dacf147d1a2ac7dff25e9de9dd8de8..57a33c6790868bcd97a597da5a68a2608d0a684a 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -7,11 +7,14 @@ pub mod notification_panel; pub mod notifications; mod panel_settings; -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; -use gpui::AppContext; +use gpui::{ + point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind, + WindowOptions, +}; pub use panel_settings::{ ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, }; @@ -23,7 +26,7 @@ use workspace::AppState; // [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] // ); -pub fn init(_app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); ChatPanelSettings::register(cx); NotificationPanelSettings::register(cx); @@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { collab_titlebar_item::init(cx); collab_panel::init(cx); // chat_panel::init(cx); - // notifications::init(&app_state, cx); + notifications::init(&app_state, cx); // cx.add_global_action(toggle_screen_sharing); // cx.add_global_action(toggle_mute); @@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { // } // } -// fn notification_window_options( -// screen: Rc, -// window_size: Vector2F, -// ) -> WindowOptions<'static> { -// const NOTIFICATION_PADDING: f32 = 16.; +fn notification_window_options( + screen: Rc, + window_size: Size, +) -> WindowOptions { + let notification_margin_width = GlobalPixels::from(16.); + let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.); -// 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), -// } -// } + let screen_bounds = screen.bounds(); + let size: Size = window_size.into(); + + // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument. + let bounds = gpui::Bounds:: { + origin: screen_bounds.upper_right() + - point( + size.width + notification_margin_width, + notification_margin_height, + ), + size: window_size.into(), + }; + WindowOptions { + bounds: WindowBounds::Fixed(bounds), + titlebar: None, + center: false, + focus: false, + show: true, + kind: WindowKind::PopUp, + is_movable: false, + display_id: Some(screen.id()), + } +} // fn render_avatar( // avatar: Option>, diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 077b813fbd9124ad2ae585632e0870c4f233d51a..162a3f261a75dd121dd50049b7a62b678cdf8915 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -1,54 +1,48 @@ -// use std::ops::Range; +use gpui::{ + div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext, +}; -// use gpui::{ -// geometry::{ -// rect::RectF, -// vector::{vec2f, Vector2F}, -// }, -// json::ToJson, -// serde_json::{self, json}, -// AnyElement, Axis, Element, View, ViewContext, -// }; +#[derive(Default)] +pub struct FacePile { + pub faces: Vec, +} -// pub(crate) struct FacePile { -// overlap: f32, -// faces: Vec>, -// } +impl RenderOnce for FacePile { + type Rendered = Div; -// impl FacePile { -// pub fn new(overlap: f32) -> Self { -// Self { -// overlap, -// faces: Vec::new(), -// } -// } -// } + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let player_count = self.faces.len(); + let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { + let isnt_last = ix < player_count - 1; -// impl Element for FacePile { -// type LayoutState = (); -// type PaintState = (); + div().when(isnt_last, |div| div.neg_mr_1()).child(player) + }); + div().p_1().flex().items_center().children(player_list) + } +} +// impl Element for FacePile { +// type State = (); // fn layout( // &mut self, -// constraint: gpui::SizeConstraint, -// view: &mut V, -// cx: &mut ViewContext, -// ) -> (Vector2F, Self::LayoutState) { -// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); - +// state: Option, +// cx: &mut WindowContext, +// ) -> (LayoutId, Self::State) { // let mut width = 0.; // let mut max_height = 0.; +// let mut faces = Vec::with_capacity(self.faces.len()); // for face in &mut self.faces { -// let layout = face.layout(constraint, view, cx); +// let layout = face.layout(cx); // width += layout.x(); // max_height = f32::max(max_height, layout.y()); +// faces.push(layout); // } // width -= self.overlap * self.faces.len().saturating_sub(1) as f32; - -// ( -// Vector2F::new(width, max_height.clamp(1., constraint.max.y())), -// (), -// ) +// (cx.request_layout(&Style::default(), faces), ()) +// // ( +// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())), +// // (), +// // )) // } // fn paint( @@ -77,37 +71,10 @@ // () // } - -// 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, -// ) -> serde_json::Value { -// json!({ -// "type": "FacePile", -// "bounds": bounds.to_json() -// }) -// } // } -// impl Extend> for FacePile { -// fn extend>>(&mut self, children: T) { -// self.faces.extend(children); -// } -// } +impl Extend for FacePile { + fn extend>(&mut self, children: T) { + self.faces.extend(children); + } +} diff --git a/crates/collab_ui2/src/notifications.rs b/crates/collab_ui2/src/notifications.rs index bc5d7ad3bf0f80fa1f16e8f60337e8abdbde5018..b58473476acb40b29ecb9754133ce6ce6e8fbf5c 100644 --- a/crates/collab_ui2/src/notifications.rs +++ b/crates/collab_ui2/src/notifications.rs @@ -1,11 +1,11 @@ -// use gpui::AppContext; -// use std::sync::Arc; -// use workspace::AppState; +use gpui::AppContext; +use std::sync::Arc; +use workspace::AppState; -// pub mod incoming_call_notification; +pub mod incoming_call_notification; // pub mod project_shared_notification; -// pub fn init(app_state: &Arc, cx: &mut AppContext) { -// incoming_call_notification::init(app_state, cx); -// project_shared_notification::init(app_state, cx); -// } +pub fn init(app_state: &Arc, cx: &mut AppContext) { + incoming_call_notification::init(app_state, cx); + //project_shared_notification::init(app_state, cx); +} diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index c614a814caf1757b37306cfca5b7d570fc0fac0f..ce6c8f0f573cef1e9d477e7a22442f76cdd3054a 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -1,14 +1,13 @@ use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; -use client::proto; use futures::StreamExt; use gpui::{ - elements::*, - geometry::vector::vec2f, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Entity, View, ViewContext, WindowHandle, + div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext, + VisualContext as _, WindowHandle, }; use std::sync::{Arc, Weak}; +use ui::prelude::*; +use ui::{h_stack, v_stack, Avatar, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -19,23 +18,44 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { let mut notification_windows: Vec> = Vec::new(); while let Some(incoming_call) = incoming_call.next().await { for window in notification_windows.drain(..) { - window.remove(&mut cx); + window + .update(&mut cx, |_, cx| { + // todo!() + cx.remove_window(); + }) + .log_err(); } if let Some(incoming_call) = incoming_call { - let window_size = cx.read(|cx| { - let theme = &theme::current(cx).incoming_call_notification; - vec2f(theme.window_width, theme.window_height) - }); + let unique_screens = cx.update(|cx| cx.displays()).unwrap(); + let window_size = gpui::Size { + width: px(380.), + height: px(64.), + }; - for screen in cx.platform().screens() { + for window in unique_screens { + let options = notification_window_options(window, window_size); let window = cx - .add_window(notification_window_options(screen, window_size), |_| { - IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) - }); - + .open_window(options, |cx| { + cx.build_view(|_| { + IncomingCallNotification::new( + incoming_call.clone(), + app_state.clone(), + ) + }) + }) + .unwrap(); notification_windows.push(window); } + + // for screen in cx.platform().screens() { + // let window = cx + // .add_window(notification_window_options(screen, window_size), |_| { + // IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) + // }); + + // notification_windows.push(window); + // } } } }) @@ -47,167 +67,206 @@ struct RespondToCall { accept: bool, } -pub struct IncomingCallNotification { +struct IncomingCallNotificationState { call: IncomingCall, app_state: Weak, } -impl IncomingCallNotification { +pub struct IncomingCallNotification { + state: Arc, +} +impl IncomingCallNotificationState { pub fn new(call: IncomingCall, app_state: Weak) -> Self { Self { call, app_state } } - fn respond(&mut self, accept: bool, cx: &mut ViewContext) { + fn respond(&self, accept: bool, cx: &mut AppContext) { let active_call = ActiveCall::global(cx); if accept { let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); - let caller_user_id = self.call.calling_user.id; let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); let app_state = self.app_state.clone(); - cx.app_context() - .spawn(|mut cx| async move { - join.await?; - if let Some(project_id) = initial_project_id { - cx.update(|cx| { - if let Some(app_state) = app_state.upgrade() { - workspace::join_remote_project( - project_id, - caller_user_id, - app_state, - cx, - ) - .detach_and_log_err(cx); - } - }); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let cx: &mut AppContext = cx; + cx.spawn(|cx| async move { + join.await?; + if let Some(_project_id) = initial_project_id { + cx.update(|_cx| { + if let Some(_app_state) = app_state.upgrade() { + // workspace::join_remote_project( + // project_id, + // caller_user_id, + // app_state, + // cx, + // ) + // .detach_and_log_err(cx); + } + }) + .log_err(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } else { active_call.update(cx, |active_call, cx| { active_call.decline_incoming(cx).log_err(); }); } } +} - fn render_caller(&self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).incoming_call_notification; - let default_project = proto::ParticipantProject::default(); - let initial_project = self - .call - .initial_project - .as_ref() - .unwrap_or(&default_project); - Flex::row() - .with_children(self.call.calling_user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.caller_avatar) - .aligned() - })) - .with_child( - Flex::column() - .with_child( - Label::new( - self.call.calling_user.github_login.clone(), - theme.caller_username.text.clone(), - ) - .contained() - .with_style(theme.caller_username.container), - ) - .with_child( - Label::new( - format!( - "is sharing a project in Zed{}", - if initial_project.worktree_root_names.is_empty() { - "" - } else { - ":" - } - ), - theme.caller_message.text.clone(), - ) - .contained() - .with_style(theme.caller_message.container), - ) - .with_children(if initial_project.worktree_root_names.is_empty() { - None - } else { - Some( - Label::new( - initial_project.worktree_root_names.join(", "), - theme.worktree_roots.text.clone(), - ) - .contained() - .with_style(theme.worktree_roots.container), - ) - }) - .contained() - .with_style(theme.caller_metadata) - .aligned(), +impl IncomingCallNotification { + pub fn new(call: IncomingCall, app_state: Weak) -> Self { + Self { + state: Arc::new(IncomingCallNotificationState::new(call, app_state)), + } + } + fn render_caller(&self, cx: &mut ViewContext) -> impl Element { + h_stack() + .children( + self.state + .call + .calling_user + .avatar + .as_ref() + .map(|avatar| Avatar::data(avatar.clone())), + ) + .child( + v_stack() + .child(Label::new(format!( + "{} is sharing a project in Zed", + self.state.call.calling_user.github_login + ))) + .child(self.render_buttons(cx)), ) - .contained() - .with_style(theme.caller_container) - .flex(1., true) - .into_any() + // let theme = &theme::current(cx).incoming_call_notification; + // let default_project = proto::ParticipantProject::default(); + // let initial_project = self + // .call + // .initial_project + // .as_ref() + // .unwrap_or(&default_project); + // Flex::row() + // .with_children(self.call.calling_user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.caller_avatar) + // .aligned() + // })) + // .with_child( + // Flex::column() + // .with_child( + // Label::new( + // self.call.calling_user.github_login.clone(), + // theme.caller_username.text.clone(), + // ) + // .contained() + // .with_style(theme.caller_username.container), + // ) + // .with_child( + // Label::new( + // format!( + // "is sharing a project in Zed{}", + // if initial_project.worktree_root_names.is_empty() { + // "" + // } else { + // ":" + // } + // ), + // theme.caller_message.text.clone(), + // ) + // .contained() + // .with_style(theme.caller_message.container), + // ) + // .with_children(if initial_project.worktree_root_names.is_empty() { + // None + // } else { + // Some( + // Label::new( + // initial_project.worktree_root_names.join(", "), + // theme.worktree_roots.text.clone(), + // ) + // .contained() + // .with_style(theme.worktree_roots.container), + // ) + // }) + // .contained() + // .with_style(theme.caller_metadata) + // .aligned(), + // ) + // .contained() + // .with_style(theme.caller_container) + // .flex(1., true) + // .into_any() } - fn render_buttons(&self, cx: &mut ViewContext) -> AnyElement { - enum Accept {} - enum Decline {} - - let theme = theme::current(cx); - Flex::column() - .with_child( - MouseEventHandler::new::(0, cx, |_, _| { - let theme = &theme.incoming_call_notification; - Label::new("Accept", theme.accept_button.text.clone()) - .aligned() - .contained() - .with_style(theme.accept_button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.respond(true, cx); - }) - .flex(1., true), + fn render_buttons(&self, cx: &mut ViewContext) -> impl Element { + h_stack() + .child( + Button::new("accept", "Accept") + .render(cx) + // .bg(green()) + .on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + }), ) - .with_child( - MouseEventHandler::new::(0, cx, |_, _| { - let theme = &theme.incoming_call_notification; - Label::new("Decline", theme.decline_button.text.clone()) - .aligned() - .contained() - .with_style(theme.decline_button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.respond(false, cx); - }) - .flex(1., true), + .child( + Button::new("decline", "Decline") + .render(cx) + // .bg(red()) + .on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) + }), ) - .constrained() - .with_width(theme.incoming_call_notification.button_width) - .into_any() - } -} -impl Entity for IncomingCallNotification { - type Event = (); -} + // enum Accept {} + // enum Decline {} -impl View for IncomingCallNotification { - fn ui_name() -> &'static str { - "IncomingCallNotification" + // let theme = theme::current(cx); + // Flex::column() + // .with_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // let theme = &theme.incoming_call_notification; + // Label::new("Accept", theme.accept_button.text.clone()) + // .aligned() + // .contained() + // .with_style(theme.accept_button.container) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.respond(true, cx); + // }) + // .flex(1., true), + // ) + // .with_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // let theme = &theme.incoming_call_notification; + // Label::new("Decline", theme.decline_button.text.clone()) + // .aligned() + // .contained() + // .with_style(theme.decline_button.container) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.respond(false, cx); + // }) + // .flex(1., true), + // ) + // .constrained() + // .with_width(theme.incoming_call_notification.button_width) + // .into_any() } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let background = theme::current(cx).incoming_call_notification.background; - Flex::row() - .with_child(self.render_caller(cx)) - .with_child(self.render_buttons(cx)) - .contained() - .with_background_color(background) - .expanded() - .into_any() +} +impl Render for IncomingCallNotification { + type Element = Div; + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().bg(red()).flex_none().child(self.render_caller(cx)) + // Flex::row() + // .with_child() + // .with_child(self.render_buttons(cx)) + // .contained() + // .with_background_color(background) + // .expanded() + // .into_any() } } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 3c6f2fff92150fd302576635e7589eb238e5d01b..f9b58b1d56139326192edf7e5fa008d90351283d 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,16 +1,17 @@ +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; + use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView, - Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, + actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; -use std::{ - cmp::{self, Reverse}, - sync::Arc, -}; -use theme::ActiveTheme; -use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; + +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -68,7 +69,7 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} +impl EventEmitter for CommandPalette {} impl FocusableView for CommandPalette { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { @@ -80,7 +81,7 @@ impl Render for CommandPalette { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - v_stack().w_96().child(self.picker.clone()) + v_stack().min_w_96().child(self.picker.clone()) } } @@ -140,7 +141,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Execute a command...".into() @@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } @@ -293,32 +294,26 @@ impl PickerDelegate for CommandPaletteDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { - let colors = cx.theme().colors(); + ) -> Option { let Some(r#match) = self.matches.get(ix) else { - return div(); + return None; }; let Some(command) = self.commands.get(r#match.candidate_id) else { - return div(); + return None; }; - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( + Some( + ListItem::new(ix).inset(true).selected(selected).child( h_stack() + .w_full() .justify_between() .child(HighlightedLabel::new( command.name.clone(), r#match.positions.clone(), )) .children(KeyBinding::for_action(&*command.action, cx)), - ) + ), + ) } } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 4748f63e5d37e29174777ea326ce8158787acd09..9ff3c04de85d9afd12f2da0a2020a0f26d05fc2a 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -126,7 +126,7 @@ impl View for ProjectDiagnosticsEditor { json!({ "project": json!({ "language_servers": project.language_server_statuses().collect::>(), - "summary": project.diagnostic_summary(cx), + "summary": project.diagnostic_summary(false, cx), }), "summary": self.summary, "paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)| @@ -195,7 +195,7 @@ impl ProjectDiagnosticsEditor { }); let project = project_handle.read(cx); - let summary = project.diagnostic_summary(cx); + let summary = project.diagnostic_summary(false, cx); let mut this = Self { project: project_handle, summary, @@ -241,7 +241,7 @@ impl ProjectDiagnosticsEditor { let mut new_summaries: HashMap> = self .project .read(cx) - .diagnostic_summaries(cx) + .diagnostic_summaries(false, cx) .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { summaries.entry(server_id).or_default().insert(path); summaries @@ -320,7 +320,7 @@ impl ProjectDiagnosticsEditor { .context("rechecking diagnostics for paths")?; this.update(&mut cx, |this, cx| { - this.summary = this.project.read(cx).diagnostic_summary(cx); + this.summary = this.project.read(cx).diagnostic_summary(false, cx); cx.emit(Event::TitleChanged); })?; anyhow::Ok(()) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 8d3c2fedd6d9d65d7d28e0a88f05f1e85161d117..86d8d01db1e837ab56f8a221768b38cc3096ad6d 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -34,19 +34,19 @@ impl DiagnosticIndicator { } project::Event::DiskBasedDiagnosticsFinished { language_server_id } | project::Event::LanguageServerRemoved(language_server_id) => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); this.in_progress_checks.remove(language_server_id); cx.notify(); } project::Event::DiagnosticsUpdated { .. } => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); cx.notify(); } _ => {} }) .detach(); Self { - summary: project.read(cx).diagnostic_summary(cx), + summary: project.read(cx).diagnostic_summary(false, cx), in_progress_checks: project .read(cx) .language_servers_running_disk_based_diagnostics() diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index dc7f0a1f3f58dfe87784dcd3debbb3035471fb99..0a0f4da8932a50b1ddac155e27d3fb8ff22cd53a 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -165,7 +165,7 @@ impl ProjectDiagnosticsEditor { }); let project = project_handle.read(cx); - let summary = project.diagnostic_summary(cx); + let summary = project.diagnostic_summary(false, cx); let mut this = Self { project: project_handle, summary, @@ -252,7 +252,7 @@ impl ProjectDiagnosticsEditor { let mut new_summaries: HashMap> = self .project .read(cx) - .diagnostic_summaries(cx) + .diagnostic_summaries(false, cx) .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { summaries.entry(server_id).or_default().insert(path); summaries @@ -332,7 +332,7 @@ impl ProjectDiagnosticsEditor { .context("rechecking diagnostics for paths")?; this.update(&mut cx, |this, cx| { - this.summary = this.project.read(cx).diagnostic_summary(cx); + this.summary = this.project.read(cx).diagnostic_summary(false, cx); cx.emit(ItemEvent::UpdateTab); cx.emit(ItemEvent::UpdateBreadcrumbs); })?; diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs index ac24b7ad508613c6d41e02238d8c030e29abdb22..92b0641deae4d049fda3d968cf88b28568b50d41 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -77,13 +77,13 @@ impl DiagnosticIndicator { project::Event::DiskBasedDiagnosticsFinished { language_server_id } | project::Event::LanguageServerRemoved(language_server_id) => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); this.in_progress_checks.remove(language_server_id); cx.notify(); } project::Event::DiagnosticsUpdated { .. } => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); cx.notify(); } @@ -92,7 +92,7 @@ impl DiagnosticIndicator { .detach(); Self { - summary: project.read(cx).diagnostic_summary(cx), + summary: project.read(cx).diagnostic_summary(false, cx), in_progress_checks: project .read(cx) .language_servers_running_disk_based_diagnostics() diff --git a/crates/diagnostics2/src/toolbar_controls.rs b/crates/diagnostics2/src/toolbar_controls.rs index e513076ec829322d4393babc35b159c6b2bc3312..1a604b76c86503099239e06edc92a60604485ec0 100644 --- a/crates/diagnostics2/src/toolbar_controls.rs +++ b/crates/diagnostics2/src/toolbar_controls.rs @@ -1,5 +1,6 @@ use crate::ProjectDiagnosticsEditor; use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; +use ui::prelude::*; use ui::{Icon, IconButton, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index a91827fae7430863862c062663446fd2f0f4a377..65b8b27b6f80011bda55521261ec766d736e94a9 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -40,11 +40,12 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, - AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, ElementId, - EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, - Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels, - Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, + DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, + FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, + MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, + Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, + VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -72,7 +73,7 @@ use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; -use rpc::proto::*; +use rpc::proto::{self, *}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -98,12 +99,13 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip}; +use ui::prelude::*; +use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, searchable::SearchEvent, - ItemNavHistory, SplitDirection, ViewId, Workspace, + ItemNavHistory, Pane, SplitDirection, ViewId, Workspace, }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -152,7 +154,6 @@ pub fn render_parsed_markdown( }), ); - // todo!("add the ability to change cursor style for link ranges") let mut links = Vec::new(); let mut link_ranges = Vec::new(); for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { @@ -529,8 +530,6 @@ pub fn init(cx: &mut AppContext) { // cx.register_action_type(Editor::context_menu_next); // cx.register_action_type(Editor::context_menu_last); - hover_popover::init(cx); - workspace::register_project_item::(cx); workspace::register_followable_item::(cx); workspace::register_deserializable_item::(cx); @@ -663,6 +662,7 @@ pub struct Editor { pixel_position_of_newest_cursor: Option>, gutter_width: Pixels, style: Option, + editor_actions: Vec)>>, } pub struct EditorSnapshot { @@ -970,95 +970,94 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - _editor: &Editor, - _cx: &mut ViewContext, + editor: &Editor, + cx: &mut ViewContext, ) -> Option> { - // todo!("implementation below "); - None - } - // { - // let settings = EditorSettings::get_global(cx); - // if !settings.show_completion_documentation { - // return None; - // } + let settings = EditorSettings::get_global(cx); + if !settings.show_completion_documentation { + return None; + } - // let Some(project) = editor.project.clone() else { - // return None; - // }; + let Some(project) = editor.project.clone() else { + return None; + }; - // let client = project.read(cx).client(); - // let language_registry = project.read(cx).languages().clone(); + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); - // let is_remote = project.read(cx).is_remote(); - // let project_id = project.read(cx).remote_id(); + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); - // let completions = self.completions.clone(); - // let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - // Some(cx.spawn(move |this, mut cx| async move { - // if is_remote { - // let Some(project_id) = project_id else { - // log::error!("Remote project without remote_id"); - // return; - // }; + Some(cx.spawn(move |this, mut cx| async move { + if is_remote { + let Some(project_id) = project_id else { + log::error!("Remote project without remote_id"); + return; + }; - // for completion_index in completion_indices { - // let completions_guard = completions.read(); - // let completion = &completions_guard[completion_index]; - // if completion.documentation.is_some() { - // continue; - // } + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } - // let server_id = completion.server_id; - // let completion = completion.lsp_completion.clone(); - // drop(completions_guard); - - // Self::resolve_completion_documentation_remote( - // project_id, - // server_id, - // completions.clone(), - // completion_index, - // completion, - // client.clone(), - // language_registry.clone(), - // ) - // .await; - - // _ = this.update(&mut cx, |_, cx| cx.notify()); - // } - // } else { - // for completion_index in completion_indices { - // let completions_guard = completions.read(); - // let completion = &completions_guard[completion_index]; - // if completion.documentation.is_some() { - // continue; - // } + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } - // let server_id = completion.server_id; - // let completion = completion.lsp_completion.clone(); - // drop(completions_guard); + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); - // let server = project.read_with(&mut cx, |project, _| { - // project.language_server_for_id(server_id) - // }); - // let Some(server) = server else { - // return; - // }; - - // Self::resolve_completion_documentation_local( - // server, - // completions.clone(), - // completion_index, - // completion, - // language_registry.clone(), - // ) - // .await; - - // _ = this.update(&mut cx, |_, cx| cx.notify()); - // } - // } - // })) - // } + let server = project + .read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }) + .ok() + .flatten(); + let Some(server) = server else { + return; + }; + + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } + })) + } fn attempt_resolve_selected_completion_documentation( &mut self, @@ -1079,10 +1078,9 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completions_guard = completions.read(); let completion = &completions_guard[completion_index]; - // todo!() - // if completion.documentation.is_some() { - // return; - // } + if completion.documentation.is_some() { + return; + } let server_id = completion.server_id; let completion = completion.lsp_completion.clone(); @@ -1141,41 +1139,40 @@ impl CompletionsMenu { client: Arc, language_registry: Arc, ) { - // todo!() - // let request = proto::ResolveCompletionDocumentation { - // project_id, - // language_server_id: server_id.0 as u64, - // lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), - // }; - - // let Some(response) = client - // .request(request) - // .await - // .context("completion documentation resolve proto request") - // .log_err() - // else { - // return; - // }; - - // if response.text.is_empty() { - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(Documentation::Undocumented); - // } - - // let documentation = if response.is_markdown { - // Documentation::MultiLineMarkdown( - // markdown::parse_markdown(&response.text, &language_registry, None).await, - // ) - // } else if response.text.lines().count() <= 1 { - // Documentation::SingleLine(response.text) - // } else { - // Documentation::MultiLinePlainText(response.text) - // }; - - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(documentation); + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; + + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; + + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } + + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); } async fn resolve_completion_documentation_local( @@ -1185,38 +1182,37 @@ impl CompletionsMenu { completion: lsp::CompletionItem, language_registry: Arc, ) { - // todo!() - // let can_resolve = server - // .capabilities() - // .completion_provider - // .as_ref() - // .and_then(|options| options.resolve_provider) - // .unwrap_or(false); - // if !can_resolve { - // return; - // } - - // let request = server.request::(completion); - // let Some(completion_item) = request.await.log_err() else { - // return; - // }; - - // if let Some(lsp_documentation) = completion_item.documentation { - // let documentation = language::prepare_completion_documentation( - // &lsp_documentation, - // &language_registry, - // None, // TODO: Try to reasonably work out which language the completion is for - // ) - // .await; - - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(documentation); - // } else { - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(Documentation::Undocumented); - // } + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return; + } + + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; + + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; + + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } } fn visible(&self) -> bool { @@ -1272,6 +1268,13 @@ impl CompletionsMenu { multiline_docs.map(|div| { div.id("multiline_docs") .max_h(max_height) + .flex_1() + .px_1p5() + .py_1() + .min_w(px(260.)) + .max_w(px(640.)) + .w(px(500.)) + .text_ui() .overflow_y_scroll() // Prevent a mouse down on documentation from being propagated to the editor, // because that would move the cursor. @@ -1322,13 +1325,18 @@ impl CompletionsMenu { div() .id(mat.candidate_id) - .min_w(px(300.)) - .max_w(px(700.)) + .min_w(px(220.)) + .max_w(px(540.)) .whitespace_nowrap() .overflow_hidden() - .bg(gpui::green()) - .hover(|style| style.bg(gpui::blue())) - .when(item_ix == selected_item, |div| div.bg(gpui::red())) + .text_ui() + .px_1() + .rounded(px(4.)) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .when(item_ix == selected_item, |div| { + div.bg(cx.theme().colors().ghost_element_selected) + }) .on_mouse_down( MouseButton::Left, cx.listener(move |editor, event, cx| { @@ -1887,6 +1895,7 @@ impl Editor { pixel_position_of_newest_cursor: None, gutter_width: Default::default(), style: None, + editor_actions: Default::default(), _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -2018,10 +2027,14 @@ impl Editor { &self.buffer } - fn workspace(&self) -> Option> { + pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } + pub fn pane(&self, cx: &AppContext) -> Option> { + self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?) + } + pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { self.buffer().read(cx).title(cx) } @@ -4369,7 +4382,7 @@ impl Editor { editor.fold_at(&FoldAt { buffer_row }, cx); } })) - .color(ui::Color::Muted) + .icon_color(ui::Color::Muted) }) }) .flatten() @@ -9178,6 +9191,26 @@ impl Editor { cx.emit(EditorEvent::Blurred); cx.notify(); } + + pub fn register_action( + &mut self, + listener: impl Fn(&A, &mut WindowContext) + 'static, + ) -> &mut Self { + let listener = Arc::new(listener); + + self.editor_actions.push(Box::new(move |cx| { + let view = cx.view().clone(); + let cx = cx.window_context(); + let listener = listener.clone(); + cx.on_action(TypeId::of::(), move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(action, cx) + } + }) + })); + self + } } pub trait CollaborationHub { diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 6865e81cfa5d830f9dbe76f1154cbc0fe180edad..e640be8efe030250876138ed23a885119a5b7dbb 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -5427,178 +5427,177 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) ); } -//todo!(completion) -// #[gpui::test] -// async fn test_completion(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_completion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["first_completion", "second_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor.context_menu_next(&Default::default(), cx); -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// "}); + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["first_completion", "second_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor.context_menu_next(&Default::default(), cx); + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + "}); -// handle_resolve_completion_request( -// &mut cx, -// Some(vec![ -// ( -// //This overlaps with the primary completion edit which is -// //misbehavior from the LSP spec, test that we filter it out -// indoc! {" -// one.second_ˇcompletion -// two -// threeˇ -// "}, -// "overlapping additional edit", -// ), -// ( -// indoc! {" -// one.second_completion -// two -// threeˇ -// "}, -// "\nadditional edit", -// ), -// ]), -// ) -// .await; -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// additional edit -// "}); + handle_resolve_completion_request( + &mut cx, + Some(vec![ + ( + //This overlaps with the primary completion edit which is + //misbehavior from the LSP spec, test that we filter it out + indoc! {" + one.second_ˇcompletion + two + threeˇ + "}, + "overlapping additional edit", + ), + ( + indoc! {" + one.second_completion + two + threeˇ + "}, + "\nadditional edit", + ), + ]), + ) + .await; + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + additional edit + "}); -// cx.set_state(indoc! {" -// one.second_completion -// twoˇ -// threeˇ -// additional edit -// "}); -// cx.simulate_keystroke(" "); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("s"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.set_state(indoc! {" + one.second_completion + twoˇ + threeˇ + additional edit + "}); + cx.simulate_keystroke(" "); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("s"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sˇ -// three sˇ -// additional edit -// "}); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two s -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; + cx.assert_editor_state(indoc! {" + one.second_completion + two sˇ + three sˇ + additional edit + "}); + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two s + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; -// cx.simulate_keystroke("i"); + cx.simulate_keystroke("i"); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two si -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two si + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sixth_completionˇ -// three sixth_completionˇ -// additional edit -// "}); + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completion + two sixth_completionˇ + three sixth_completionˇ + additional edit + "}); -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); -// cx.update(|cx| { -// cx.update_global::(|settings, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.show_completions_on_input = Some(false); -// }); -// }) -// }); -// cx.set_state("editorˇ"); -// cx.simulate_keystroke("."); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("c"); -// cx.simulate_keystroke("l"); -// cx.simulate_keystroke("o"); -// cx.assert_editor_state("editor.cloˇ"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.update_editor(|editor, cx| { -// editor.show_completions(&ShowCompletions, cx); -// }); -// handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state("editor.closeˇ"); -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); -// } + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.show_completions_on_input = Some(false); + }); + }) + }); + cx.set_state("editorˇ"); + cx.simulate_keystroke("."); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("c"); + cx.simulate_keystroke("l"); + cx.simulate_keystroke("o"); + cx.assert_editor_state("editor.cloˇ"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.update_editor(|editor, cx| { + editor.show_completions(&ShowCompletions, cx); + }); + handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state("editor.closeˇ"); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); +} #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { @@ -7803,197 +7802,196 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test ); } -//todo!(completions) -// #[gpui::test] -// async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); -// cx.simulate_keystroke("."); -// let completion_item = lsp::CompletionItem { -// label: "some".into(), -// kind: Some(lsp::CompletionItemKind::SNIPPET), -// detail: Some("Wrap the expression in an `Option::Some`".to_string()), -// documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "```rust\nSome(2)\n```".to_string(), -// })), -// deprecated: Some(false), -// sort_text: Some("fffffff2".to_string()), -// filter_text: Some("some".to_string()), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// range: lsp::Range { -// start: lsp::Position { -// line: 0, -// character: 22, -// }, -// end: lsp::Position { -// line: 0, -// character: 22, -// }, -// }, -// new_text: "Some(2)".to_string(), -// })), -// additional_text_edits: Some(vec![lsp::TextEdit { -// range: lsp::Range { -// start: lsp::Position { -// line: 0, -// character: 20, -// }, -// end: lsp::Position { -// line: 0, -// character: 22, -// }, -// }, -// new_text: "".to_string(), -// }]), -// ..Default::default() -// }; - -// let closure_completion_item = completion_item.clone(); -// let mut request = cx.handle_request::(move |_, _, _| { -// let task_completion_item = closure_completion_item.clone(); -// async move { -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// task_completion_item, -// ]))) -// } -// }); + cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); + cx.simulate_keystroke("."); + let completion_item = lsp::CompletionItem { + label: "some".into(), + kind: Some(lsp::CompletionItemKind::SNIPPET), + detail: Some("Wrap the expression in an `Option::Some`".to_string()), + documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "```rust\nSome(2)\n```".to_string(), + })), + deprecated: Some(false), + sort_text: Some("fffffff2".to_string()), + filter_text: Some("some".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 22, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "Some(2)".to_string(), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "".to_string(), + }]), + ..Default::default() + }; -// request.next().await; + let closure_completion_item = completion_item.clone(); + let mut request = cx.handle_request::(move |_, _, _| { + let task_completion_item = closure_completion_item.clone(); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + task_completion_item, + ]))) + } + }); -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); - -// cx.handle_request::(move |_, _, _| { -// let task_completion_item = completion_item.clone(); -// async move { Ok(task_completion_item) } -// }) -// .next() -// .await -// .unwrap(); -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); -// } + request.next().await; -// #[gpui::test] -// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); + + cx.handle_request::(move |_, _, _| { + let task_completion_item = completion_item.clone(); + async move { Ok(task_completion_item) } + }) + .next() + .await + .unwrap(); + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); +} -// let mut cx = EditorLspTestContext::new( -// Language::new( -// LanguageConfig { -// path_suffixes: vec!["jsx".into()], -// overrides: [( -// "element".into(), -// LanguageConfigOverride { -// word_characters: Override::Set(['-'].into_iter().collect()), -// ..Default::default() -// }, -// )] -// .into_iter() -// .collect(), -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_tsx()), -// ) -// .with_override_query("(jsx_self_closing_element) @element") -// .unwrap(), -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; +#[gpui::test] +async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// cx.lsp -// .handle_request::(move |_, _| async move { -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// lsp::CompletionItem { -// label: "bg-blue".into(), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "bg-red".into(), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "bg-yellow".into(), -// ..Default::default() -// }, -// ]))) -// }); + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + path_suffixes: vec!["jsx".into()], + overrides: [( + "element".into(), + LanguageConfigOverride { + word_characters: Override::Set(['-'].into_iter().collect()), + ..Default::default() + }, + )] + .into_iter() + .collect(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + ) + .with_override_query("(jsx_self_closing_element) @element") + .unwrap(), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// cx.set_state(r#"

"#); + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "bg-blue".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-red".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-yellow".into(), + ..Default::default() + }, + ]))) + }); -// // Trigger completion when typing a dash, because the dash is an extra -// // word character in the 'element' scope, which contains the cursor. -// cx.simulate_keystroke("-"); -// cx.executor().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-red", "bg-blue", "bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); + cx.set_state(r#"

"#); -// cx.simulate_keystroke("l"); -// cx.executor().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-blue", "bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); + // Trigger completion when typing a dash, because the dash is an extra + // word character in the 'element' scope, which contains the cursor. + cx.simulate_keystroke("-"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-red", "bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); -// // When filtering completions, consider the character after the '-' to -// // be the start of a subword. -// cx.set_state(r#"

"#); -// cx.simulate_keystroke("l"); -// cx.executor().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); -// } + cx.simulate_keystroke("l"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + // When filtering completions, consider the character after the '-' to + // be the start of a subword. + cx.set_state(r#"

"#); + cx.simulate_keystroke("l"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} #[gpui::test] async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index e591dd84cf55da1ed71292dd39ac102d38736554..24402c7e379e112bf941bd85a51e249d95d257e9 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -5,7 +5,9 @@ use crate::{ }, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, - hover_popover::hover_at, + hover_popover::{ + self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, @@ -20,10 +22,11 @@ use collections::{BTreeMap, HashMap}; use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, - ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, - TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, + ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement, + IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size, + StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, + ViewContext, WeakView, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -45,6 +48,7 @@ use std::{ }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; +use ui::prelude::*; use ui::{h_stack, IconButton, Tooltip}; use util::ResultExt; use workspace::item::Item; @@ -126,6 +130,11 @@ impl EditorElement { fn register_actions(&self, cx: &mut WindowContext) { let view = &self.editor; + self.editor.update(cx, |editor, cx| { + for action in editor.editor_actions.iter() { + (action)(cx) + } + }); register_action(view, cx, Editor::move_left); register_action(view, cx, Editor::move_right); register_action(view, cx, Editor::move_down); @@ -257,6 +266,7 @@ impl EditorElement { // on_action(cx, Editor::open_excerpts); todo!() register_action(view, cx, Editor::toggle_soft_wrap); register_action(view, cx, Editor::toggle_inlay_hints); + register_action(view, cx, hover_popover::hover); register_action(view, cx, Editor::reveal_in_finder); register_action(view, cx, Editor::copy_path); register_action(view, cx, Editor::copy_relative_path); @@ -308,6 +318,7 @@ impl EditorElement { position_map: &PositionMap, text_bounds: Bounds, gutter_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, ) -> bool { let mut click_count = event.click_count; @@ -318,6 +329,9 @@ impl EditorElement { } else if !text_bounds.contains_point(&event.position) { return false; } + if !cx.was_top_layer(&event.position, stacking_order) { + return false; + } let point_for_position = position_map.point_for_position(text_bounds, event.position); let position = point_for_position.previous_valid; @@ -376,6 +390,7 @@ impl EditorElement { event: &MouseUpEvent, position_map: &PositionMap, text_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, ) -> bool { let end_selection = editor.has_pending_selection(); @@ -388,6 +403,7 @@ impl EditorElement { if !pending_nonempty_selections && event.modifiers.command && text_bounds.contains_point(&event.position) + && cx.was_top_layer(&event.position, stacking_order) { let point = position_map.point_for_position(text_bounds, event.position); let could_be_inlay = point.as_valid().is_none(); @@ -410,6 +426,7 @@ impl EditorElement { position_map: &PositionMap, text_bounds: Bounds, gutter_bounds: Bounds, + stacking_order: &StackingOrder, cx: &mut ViewContext, ) -> bool { let modifiers = event.modifiers; @@ -449,10 +466,12 @@ impl EditorElement { let text_hovered = text_bounds.contains_point(&event.position); let gutter_hovered = gutter_bounds.contains_point(&event.position); + let was_top = cx.was_top_layer(&event.position, stacking_order); + editor.set_gutter_hovered(gutter_hovered, cx); // Don't trigger hover popover if mouse is hovering over context menu - if text_hovered { + if text_hovered && was_top { let point_for_position = position_map.point_for_position(text_bounds, event.position); match point_for_position.as_valid() { @@ -482,7 +501,7 @@ impl EditorElement { } else { update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx); hover_at(editor, None, cx); - gutter_hovered + gutter_hovered && was_top } } @@ -490,10 +509,10 @@ impl EditorElement { editor: &mut Editor, event: &ScrollWheelEvent, position_map: &PositionMap, - bounds: Bounds, + bounds: &InteractiveBounds, cx: &mut ViewContext, ) -> bool { - if !bounds.contains_point(&event.position) { + if !bounds.visibly_contains(&event.position, cx) { return false; } @@ -1024,8 +1043,8 @@ impl EditorElement { } }); - if let Some((position, mut context_menu)) = layout.context_menu.take() { - cx.with_z_index(1, |cx| { + cx.with_z_index(1, |cx| { + if let Some((position, mut context_menu)) = layout.context_menu.take() { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); let context_menu_size = context_menu.measure(available_space, cx); @@ -1052,81 +1071,74 @@ impl EditorElement { list_origin.y -= layout.position_map.line_height + list_height; } - context_menu.draw(list_origin, available_space, cx); - }) - } + cx.break_content_mask(|cx| { + context_menu.draw(list_origin, available_space, cx) + }); + } - // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - // cx.scene().push_stacking_context(None, None); - - // // This is safe because we check on layout whether the required row is available - // let hovered_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - - // // Minimum required size: Take the first popover, and add 1.5 times the minimum popover - // // height. This is the size we will use to decide whether to render popovers above or below - // // the hovered line. - // let first_size = hover_popovers[0].size(); - // let height_to_reserve = first_size.y - // + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; - - // // Compute Hovered Point - // let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; - // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; - // let hovered_point = content_origin + point(x, y); - - // if hovered_point.y - height_to_reserve > 0.0 { - // // There is enough space above. Render popovers above the hovered point - // let mut current_y = hovered_point.y; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y - size.y); - - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } - - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); - - // current_y = popover_origin.y - HOVER_POPOVER_GAP; - // } - // } else { - // // There is not enough space above. Render popovers below the hovered point - // let mut current_y = hovered_point.y + layout.position_map.line_height; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y); - - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } - - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); - - // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; - // } - // } - - // cx.scene().pop_stacking_context(); - // } + if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + + // This is safe because we check on layout whether the required row is available + let hovered_row_layout = &layout.position_map.line_layouts + [(position.row() - start_row) as usize] + .line; + + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].measure(available_space, cx); + let height_to_reserve = first_size.height + + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height; + + // Compute Hovered Point + let x = hovered_row_layout.x_for_index(position.column() as usize) + - layout.position_map.scroll_position.x; + let y = position.row() as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let hovered_point = content_origin + point(x, y); + + if hovered_point.y - height_to_reserve > Pixels::ZERO { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = + point(hovered_point.x, current_y - size.height); + + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } + + cx.break_content_mask(|cx| { + hover_popover.draw(popover_origin, available_space, cx) + }); + + current_y = popover_origin.y - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y + layout.position_map.line_height; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(hovered_point.x, current_y); + + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } + + hover_popover.draw(popover_origin, available_space, cx); + + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; + } + } + } + }) }, ) } @@ -1992,15 +2004,23 @@ impl EditorElement { } let visible_rows = start_row..start_row + line_layouts.len() as u32; - // todo!("hover") - // let mut hover = editor.hover_state.render( - // &snapshot, - // &style, - // visible_rows, - // editor.workspace.as_ref().map(|(w, _)| w.clone()), - // cx, - // ); - // let mode = editor.mode; + let max_size = size( + (120. * em_width) // Default size + .min(bounds.size.width / 2.) // Shrink to half of the editor width + .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + (16. * line_height) // Default size + .min(bounds.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let mut hover = editor.hover_state.render( + &snapshot, + &style, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ); let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { editor.render_fold_indicators( @@ -2013,27 +2033,6 @@ impl EditorElement { ) }); - // todo!("hover popovers") - // if let Some((_, hover_popovers)) = hover.as_mut() { - // for hover_popover in hover_popovers.iter_mut() { - // hover_popover.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point( - // (120. * em_width) // Default size - // .min(size.x / 2.) // Shrink to half of the editor width - // .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters - // (16. * line_height) // Default size - // .min(size.y / 2.) // Shrink to half of the editor height - // .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines - // ), - // }, - // editor, - // cx, - // ); - // } - // } - let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx .text_system() @@ -2102,7 +2101,7 @@ impl EditorElement { fold_indicators, tab_invisible, space_invisible, - // hover_popovers: hover, + hover_popovers: hover, } }) } @@ -2294,10 +2293,15 @@ impl EditorElement { cx: &mut WindowContext, ) { let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let interactive_bounds = InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }; cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let interactive_bounds = interactive_bounds.clone(); move |event: &ScrollWheelEvent, phase, cx| { if phase != DispatchPhase::Bubble { @@ -2305,7 +2309,7 @@ impl EditorElement { } let should_cancel = editor.update(cx, |editor, cx| { - Self::scroll(editor, event, &position_map, bounds, cx) + Self::scroll(editor, event, &position_map, &interactive_bounds, cx) }); if should_cancel { cx.stop_propagation(); @@ -2316,6 +2320,7 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); move |event: &MouseDownEvent, phase, cx| { if phase != DispatchPhase::Bubble { @@ -2323,7 +2328,15 @@ impl EditorElement { } let should_cancel = editor.update(cx, |editor, cx| { - Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) + Self::mouse_down( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) }); if should_cancel { @@ -2335,9 +2348,18 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + move |event: &MouseUpEvent, phase, cx| { let should_cancel = editor.update(cx, |editor, cx| { - Self::mouse_up(editor, event, &position_map, text_bounds, cx) + Self::mouse_up( + editor, + event, + &position_map, + text_bounds, + &stacking_order, + cx, + ) }); if should_cancel { @@ -2363,13 +2385,23 @@ impl EditorElement { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); + let stacking_order = cx.stacking_order().clone(); + move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } let stop_propogating = editor.update(cx, |editor, cx| { - Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) + Self::mouse_moved( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) }); if stop_propogating { @@ -2629,9 +2661,11 @@ impl Element for EditorElement { // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - // Paint mouse listeners first, so any elements we paint on top of the editor + // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor // take precedence. - self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + cx.with_z_index(0, |cx| { + self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + }); let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx); cx.handle_input(&focus_handle, input_handler); @@ -3287,7 +3321,7 @@ pub struct LayoutState { max_row: u32, context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, - // hover_popovers: Option<(DisplayPoint, Vec)>, + hover_popovers: Option<(DisplayPoint, Vec)>, fold_indicators: Vec>, tab_invisible: ShapedLine, space_invisible: ShapedLine, @@ -4085,7 +4119,7 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn register_action( +pub fn register_action( view: &View, cx: &mut WindowContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 07d108cd6525babd12bf55404ba1b561cf2d67f4..2f2e8ee93732b0cb81fcf88775208519545b330d 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -1,15 +1,21 @@ use crate::{ - display_map::InlayOffset, + display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; -use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView}; +use gpui::{ + actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, + Task, ViewContext, WeakView, +}; use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; +use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; +use ui::{StyledExt, Tooltip}; use util::TryFutureExt; use workspace::Workspace; @@ -17,22 +23,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; -pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; -pub const HOVER_POPOVER_GAP: f32 = 10.; +pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); +pub const HOVER_POPOVER_GAP: Pixels = px(10.); -// actions!(editor, [Hover]); +actions!(Hover); -pub fn init(cx: &mut AppContext) { - // cx.add_action(hover); +/// Bindable action which uses the most recent selection head to trigger a hover +pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { + let head = editor.selections.newest_display(cx).head(); + show_hover(editor, head, true, cx); } -// todo!() -// /// Bindable action which uses the most recent selection head to trigger a hover -// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { -// let head = editor.selections.newest_display(cx).head(); -// show_hover(editor, head, true, cx); -// } - /// The internal hover action dispatches between `show_hover` or `hide_hover` /// depending on whether a point to hover over is provided. pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewContext) { @@ -74,64 +75,63 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - todo!() - // if EditorSettings::get_global(cx).hover_popover_enabled { - // if editor.pending_rename.is_some() { - // return; - // } - - // let Some(project) = editor.project.clone() else { - // return; - // }; - - // if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - // if let RangeInEditor::Inlay(range) = symbol_range { - // if range == &inlay_hover.range { - // // Hover triggered from same location as last time. Don't show again. - // return; - // } - // } - // hide_hover(editor, cx); - // } - - // let task = cx.spawn(|this, mut cx| { - // async move { - // cx.background_executor() - // .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) - // .await; - // this.update(&mut cx, |this, _| { - // this.hover_state.diagnostic_popover = None; - // })?; - - // let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; - // let blocks = vec![inlay_hover.tooltip]; - // let parsed_content = parse_blocks(&blocks, &language_registry, None).await; - - // let hover_popover = InfoPopover { - // project: project.clone(), - // symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), - // blocks, - // parsed_content, - // }; - - // this.update(&mut cx, |this, cx| { - // // Highlight the selected symbol using a background highlight - // this.highlight_inlay_background::( - // vec![inlay_hover.range], - // |theme| theme.editor.hover_popover.highlight, - // cx, - // ); - // this.hover_state.info_popover = Some(hover_popover); - // cx.notify(); - // })?; - - // anyhow::Ok(()) - // } - // .log_err() - // }); - - // editor.hover_state.info_task = Some(task); - // } + if EditorSettings::get_global(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background_executor() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), + blocks, + parsed_content, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.element_hover, // todo!("use a proper background here") + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } } /// Hides the type information popup. @@ -420,43 +420,42 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + max_size: Size, workspace: Option>, cx: &mut ViewContext, ) -> Option<(DisplayPoint, Vec)> { - todo!("old version below") + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| match &info_popover.symbol_range { + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, + }) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, max_size, cx)); + } + if let Some(info_popover) = self.info_popover.as_mut() { + elements.push(info_popover.render(style, max_size, workspace, cx)); + } + + Some((point, elements)) } - // // If there is a diagnostic, position the popovers based on that. - // // Otherwise use the start of the hover range - // let anchor = self - // .diagnostic_popover - // .as_ref() - // .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) - // .or_else(|| { - // self.info_popover - // .as_ref() - // .map(|info_popover| match &info_popover.symbol_range { - // RangeInEditor::Text(range) => &range.start, - // RangeInEditor::Inlay(range) => &range.inlay_position, - // }) - // })?; - // let point = anchor.to_display_point(&snapshot.display_snapshot); - - // // Don't render if the relevant point isn't on screen - // if !self.visible() || !visible_rows.contains(&point.row()) { - // return None; - // } - - // let mut elements = Vec::new(); - - // if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { - // elements.push(diagnostic_popover.render(style, cx)); - // } - // if let Some(info_popover) = self.info_popover.as_mut() { - // elements.push(info_popover.render(style, workspace, cx)); - // } - - // Some((point, elements)) - // } } #[derive(Debug, Clone)] @@ -467,35 +466,35 @@ pub struct InfoPopover { parsed_content: ParsedMarkdown, } -// impl InfoPopover { -// pub fn render( -// &mut self, -// style: &EditorStyle, -// workspace: Option>, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(0, cx, |_, cx| { -// Flex::column() -// .scrollable::(0, None, cx) -// .with_child(crate::render_parsed_markdown::( -// &self.parsed_content, -// style, -// workspace, -// cx, -// )) -// .contained() -// .with_style(style.hover_popover.container) -// }) -// .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. -// .with_cursor_style(CursorStyle::Arrow) -// .with_padding(Padding { -// bottom: HOVER_POPOVER_GAP, -// top: HOVER_POPOVER_GAP, -// ..Default::default() -// }) -// .into_any() -// } -// } +impl InfoPopover { + pub fn render( + &mut self, + style: &EditorStyle, + max_size: Size, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + div() + .id("info_popover") + .elevation_2(cx) + .text_ui() + .p_2() + .overflow_y_scroll() + .max_w(max_size.width) + .max_h(max_size.height) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + .child(crate::render_parsed_markdown( + "content", + &self.parsed_content, + style, + workspace, + cx, + )) + .into_any_element() + } +} #[derive(Debug, Clone)] pub struct DiagnosticPopover { @@ -504,57 +503,40 @@ pub struct DiagnosticPopover { } impl DiagnosticPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { - todo!() - // enum PrimaryDiagnostic {} - - // let mut text_style = style.hover_popover.prose.clone(); - // text_style.font_size = style.text.font_size; - // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); - - // let text = match &self.local_diagnostic.diagnostic.source { - // Some(source) => Text::new( - // format!("{source}: {}", self.local_diagnostic.diagnostic.message), - // text_style, - // ) - // .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), - - // None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), - // }; - - // let container_style = match self.local_diagnostic.diagnostic.severity { - // DiagnosticSeverity::HINT => style.hover_popover.info_container, - // DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, - // DiagnosticSeverity::WARNING => style.hover_popover.warning_container, - // DiagnosticSeverity::ERROR => style.hover_popover.error_container, - // _ => style.hover_popover.container, - // }; - - // let tooltip_style = theme::current(cx).tooltip.clone(); - - // MouseEventHandler::new::(0, cx, |_, _| { - // text.with_soft_wrap(true) - // .contained() - // .with_style(container_style) - // }) - // .with_padding(Padding { - // top: HOVER_POPOVER_GAP, - // bottom: HOVER_POPOVER_GAP, - // ..Default::default() - // }) - // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. - // .on_click(MouseButton::Left, |_, this, cx| { - // this.go_to_diagnostic(&Default::default(), cx) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // 0, - // "Go To Diagnostic".to_string(), - // Some(Box::new(crate::GoToDiagnostic)), - // tooltip_style, - // cx, - // ) - // .into_any() + pub fn render( + &self, + style: &EditorStyle, + max_size: Size, + cx: &mut ViewContext, + ) -> AnyElement { + let text = match &self.local_diagnostic.diagnostic.source { + Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message), + None => self.local_diagnostic.diagnostic.message.clone(), + }; + + let container_bg = crate::diagnostic_style( + self.local_diagnostic.diagnostic.severity, + true, + &style.diagnostic_style, + ); + + div() + .id("diagnostic") + .overflow_y_scroll() + .bg(container_bg) + .max_w(max_size.width) + .max_h(max_size.height) + .cursor(CursorStyle::PointingHand) + .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx)) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx))) + .child(SharedString::from(text)) + .into_any_element() } pub fn activation_info(&self) -> (usize, Anchor) { @@ -567,763 +549,763 @@ impl DiagnosticPopover { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// editor_tests::init_test, -// element::PointForPosition, -// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, -// link_go_to_definition::update_inlay_link_and_hover_points, -// test::editor_lsp_test_context::EditorLspTestContext, -// InlayId, -// }; -// use collections::BTreeSet; -// use gpui::fonts::{HighlightStyle, Underline, Weight}; -// use indoc::indoc; -// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; -// use lsp::LanguageServerId; -// use project::{HoverBlock, HoverBlockKind}; -// use smol::stream::StreamExt; -// use unindent::Unindent; -// use util::test::marked_text_ranges; - -// #[gpui::test] -// async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Basic hover delays and then pops without moving the mouse -// cx.set_state(indoc! {" -// fn ˇtest() { println!(); } -// "}); -// let hover_point = cx.display_point(indoc! {" -// fn test() { printˇln!(); } -// "}); - -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// assert!(!cx.editor(|editor, _| editor.hover_state.visible())); - -// // After delay, hover should be visible. -// let symbol_range = cx.lsp_range(indoc! {" -// fn test() { «println!»(); } -// "}); -// let mut requests = -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some basic docs".to_string(), -// }), -// range: Some(symbol_range), -// })) -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// requests.next().await; - -// cx.editor(|editor, _| { -// assert!(editor.hover_state.visible()); -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "some basic docs".to_string(), -// kind: HoverBlockKind::Markdown, -// },] -// ) -// }); - -// // Mouse moved with no hover response dismisses -// let hover_point = cx.display_point(indoc! {" -// fn teˇst() { println!(); } -// "}); -// let mut request = cx -// .lsp -// .handle_request::(|_, _| async move { Ok(None) }); -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// request.next().await; -// cx.editor(|editor, _| { -// assert!(!editor.hover_state.visible()); -// }); -// } - -// #[gpui::test] -// async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some other basic docs".to_string(), -// }), -// range: Some(symbol_range), -// })) -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "some other basic docs".to_string(), -// kind: HoverBlockKind::Markdown, -// }] -// ) -// }); -// } - -// #[gpui::test] -// async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Array(vec![ -// lsp::MarkedString::String("regular text for hover to show".to_string()), -// lsp::MarkedString::String("".to_string()), -// lsp::MarkedString::LanguageString(lsp::LanguageString { -// language: "Rust".to_string(), -// value: "".to_string(), -// }), -// ]), -// range: Some(symbol_range), -// })) -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "regular text for hover to show".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// "No empty string hovers should be shown" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); - -// let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; -// let markdown_string = format!("\n```rust\n{code_str}```"); - -// let closure_markdown_string = markdown_string.clone(); -// cx.handle_request::(move |_, _, _| { -// let future_markdown_string = closure_markdown_string.clone(); -// async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: future_markdown_string, -// }), -// range: Some(symbol_range), -// })) -// } -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; -// assert_eq!( -// blocks, -// vec![HoverBlock { -// text: markdown_string, -// kind: HoverBlockKind::Markdown, -// }], -// ); - -// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); -// assert_eq!( -// rendered.text, -// code_str.trim(), -// "Should not have extra line breaks at end of rendered hover" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with just diagnostic, pops DiagnosticPopover immediately and then -// // info popover once request completes -// cx.set_state(indoc! {" -// fn teˇst() { println!(); } -// "}); - -// // Send diagnostic to client -// let range = cx.text_anchor_range(indoc! {" -// fn «test»() { println!(); } -// "}); -// cx.update_buffer(|buffer, cx| { -// let snapshot = buffer.text_snapshot(); -// let set = DiagnosticSet::from_sorted_entries( -// vec![DiagnosticEntry { -// range, -// diagnostic: Diagnostic { -// message: "A test diagnostic message.".to_string(), -// ..Default::default() -// }, -// }], -// &snapshot, -// ); -// buffer.update_diagnostics(LanguageServerId(0), set, cx); -// }); - -// // Hover pops diagnostic immediately -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// cx.foreground().run_until_parked(); - -// cx.editor(|Editor { hover_state, .. }, _| { -// assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) -// }); - -// // Info Popover shows after request responded to -// let range = cx.lsp_range(indoc! {" -// fn «test»() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some new docs".to_string(), -// }), -// range: Some(range), -// })) -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - -// cx.foreground().run_until_parked(); -// cx.editor(|Editor { hover_state, .. }, _| { -// hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() -// }); -// } - -// #[gpui::test] -// fn test_render_blocks(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// cx.add_window(|cx| { -// let editor = Editor::single_line(None, cx); -// let style = editor.style(cx); - -// struct Row { -// blocks: Vec, -// expected_marked_text: String, -// expected_styles: Vec, -// } - -// let rows = &[ -// // Strong emphasis -// Row { -// blocks: vec![HoverBlock { -// text: "one **two** three".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: "one «two» three".to_string(), -// expected_styles: vec![HighlightStyle { -// weight: Some(Weight::BOLD), -// ..Default::default() -// }], -// }, -// // Links -// Row { -// blocks: vec![HoverBlock { -// text: "one [two](https://the-url) three".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: "one «two» three".to_string(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// // Lists -// Row { -// blocks: vec![HoverBlock { -// text: " -// lists: -// * one -// - a -// - b -// * two -// - [c](https://the-url) -// - d" -// .unindent(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: " -// lists: -// - one -// - a -// - b -// - two -// - «c» -// - d" -// .unindent(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// // Multi-paragraph list items -// Row { -// blocks: vec![HoverBlock { -// text: " -// * one two -// three - -// * four five -// * six seven -// eight - -// nine -// * ten -// * six" -// .unindent(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: " -// - one two three -// - four five -// - six seven eight - -// nine -// - ten -// - six" -// .unindent(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// ]; - -// for Row { -// blocks, -// expected_marked_text, -// expected_styles, -// } in &rows[0..] -// { -// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); - -// let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); -// let expected_highlights = ranges -// .into_iter() -// .zip(expected_styles.iter().cloned()) -// .collect::>(); -// assert_eq!( -// rendered.text, expected_text, -// "wrong text for input {blocks:?}" -// ); - -// let rendered_highlights: Vec<_> = rendered -// .highlights -// .iter() -// .filter_map(|(range, highlight)| { -// let highlight = highlight.to_highlight_style(&style.syntax)?; -// Some((range.clone(), highlight)) -// }) -// .collect(); - -// assert_eq!( -// rendered_highlights, expected_highlights, -// "wrong highlights for input {blocks:?}" -// ); -// } - -// editor -// }); -// } - -// #[gpui::test] -// async fn test_hover_inlay_label_parts(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 cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Right( -// lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { -// resolve_provider: Some(true), -// ..Default::default() -// }), -// )), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "}); - -// let hint_start_offset = cx.ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "})[0] -// .start; -// let hint_position = cx.to_lsp(hint_start_offset); -// let new_type_target_range = cx.lsp_range(indoc! {" -// struct TestStruct; - -// // ================== - -// struct «TestNewType»(T); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); -// let struct_target_range = cx.lsp_range(indoc! {" -// struct «TestStruct»; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); - -// let uri = cx.buffer_lsp_url.clone(); -// let new_type_label = "TestNewType"; -// let struct_label = "TestStruct"; -// let entire_hint_label = ": TestNewType"; -// let closure_uri = uri.clone(); -// cx.lsp -// .handle_request::(move |params, _| { -// let task_uri = closure_uri.clone(); -// async move { -// assert_eq!(params.text_document.uri, task_uri); -// Ok(Some(vec![lsp::InlayHint { -// position: hint_position, -// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { -// value: entire_hint_label.to_string(), -// ..Default::default() -// }]), -// kind: Some(lsp::InlayHintKind::TYPE), -// text_edits: None, -// tooltip: None, -// padding_left: Some(false), -// padding_right: Some(false), -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let expected_layers = vec![entire_hint_label.to_string()]; -// assert_eq!(expected_layers, cached_hint_labels(editor)); -// assert_eq!(expected_layers, visible_hint_labels(editor, cx)); -// }); - -// let inlay_range = cx -// .ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variable« »= TestNewType(TestStruct); -// } -// "}) -// .get(0) -// .cloned() -// .unwrap(); -// let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() -// + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) -// as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// new_type_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); - -// let resolve_closure_uri = uri.clone(); -// cx.lsp -// .handle_request::( -// move |mut hint_to_resolve, _| { -// let mut resolved_hint_positions = BTreeSet::new(); -// let task_uri = resolve_closure_uri.clone(); -// async move { -// let inserted = resolved_hint_positions.insert(hint_to_resolve.position); -// assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); - -// // `: TestNewType` -// hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ -// lsp::InlayHintLabelPart { -// value: ": ".to_string(), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: new_type_label.to_string(), -// location: Some(lsp::Location { -// uri: task_uri.clone(), -// range: new_type_target_range, -// }), -// tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( -// "A tooltip for `{new_type_label}`" -// ))), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: "<".to_string(), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: struct_label.to_string(), -// location: Some(lsp::Location { -// uri: task_uri, -// range: struct_target_range, -// }), -// tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( -// lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: format!("A tooltip for `{struct_label}`"), -// }, -// )), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: ">".to_string(), -// ..Default::default() -// }, -// ]); - -// Ok(hint_to_resolve) -// } -// }, -// ) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// new_type_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let hover_state = &editor.hover_state; -// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); -// let popover = hover_state.info_popover.as_ref().unwrap(); -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// assert_eq!( -// popover.symbol_range, -// RangeInEditor::Inlay(InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: ": ".len()..": ".len() + new_type_label.len(), -// }), -// "Popover range should match the new type label part" -// ); -// assert_eq!( -// popover.parsed_content.text, -// format!("A tooltip for `{new_type_label}`"), -// "Rendered text should not anyhow alter backticks" -// ); -// }); - -// let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() -// + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) -// as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// struct_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let hover_state = &editor.hover_state; -// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); -// let popover = hover_state.info_popover.as_ref().unwrap(); -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// assert_eq!( -// popover.symbol_range, -// RangeInEditor::Inlay(InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: ": ".len() + new_type_label.len() + "<".len() -// ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), -// }), -// "Popover range should match the struct label part" -// ); -// assert_eq!( -// popover.parsed_content.text, -// format!("A tooltip for {struct_label}"), -// "Rendered markdown element should remove backticks from text" -// ); -// }); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + InlayId, + }; + use collections::BTreeSet; + use gpui::{FontWeight, HighlightStyle, UnderlineStyle}; + use indoc::indoc; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; + use lsp::LanguageServerId; + use project::{HoverBlock, HoverBlockKind}; + use smol::stream::StreamExt; + use unindent::Unindent; + use util::test::marked_text_ranges; + + #[gpui::test] + async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Basic hover delays and then pops without moving the mouse + cx.set_state(indoc! {" + fn ˇtest() { println!(); } + "}); + let hover_point = cx.display_point(indoc! {" + fn test() { printˇln!(); } + "}); + + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + assert!(!cx.editor(|editor, _| editor.hover_state.visible())); + + // After delay, hover should be visible. + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «println!»(); } + "}); + let mut requests = + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + requests.next().await; + + cx.editor(|editor, _| { + assert!(editor.hover_state.visible()); + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some basic docs".to_string(), + kind: HoverBlockKind::Markdown, + },] + ) + }); + + // Mouse moved with no hover response dismisses + let hover_point = cx.display_point(indoc! {" + fn teˇst() { println!(); } + "}); + let mut request = cx + .lsp + .handle_request::(|_, _| async move { Ok(None) }); + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + request.next().await; + cx.editor(|editor, _| { + assert!(!editor.hover_state.visible()); + }); + } + + #[gpui::test] + async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some other basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some other basic docs".to_string(), + kind: HoverBlockKind::Markdown, + }] + ) + }); + } + + #[gpui::test] + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("regular text for hover to show".to_string()), + lsp::MarkedString::String("".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "".to_string(), + }), + ]), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "regular text for hover to show".to_string(), + kind: HoverBlockKind::Markdown, + }], + "No empty string hovers should be shown" + ); + }); + } + + #[gpui::test] + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; + let markdown_string = format!("\n```rust\n{code_str}```"); + + let closure_markdown_string = markdown_string.clone(); + cx.handle_request::(move |_, _, _| { + let future_markdown_string = closure_markdown_string.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: future_markdown_string, + }), + range: Some(symbol_range), + })) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; + assert_eq!( + blocks, + vec![HoverBlock { + text: markdown_string, + kind: HoverBlockKind::Markdown, + }], + ); + + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + assert_eq!( + rendered.text, + code_str.trim(), + "Should not have extra line breaks at end of rendered hover" + ); + }); + } + + #[gpui::test] + async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with just diagnostic, pops DiagnosticPopover immediately and then + // info popover once request completes + cx.set_state(indoc! {" + fn teˇst() { println!(); } + "}); + + // Send diagnostic to client + let range = cx.text_anchor_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.update_buffer(|buffer, cx| { + let snapshot = buffer.text_snapshot(); + let set = DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + range, + diagnostic: Diagnostic { + message: "A test diagnostic message.".to_string(), + ..Default::default() + }, + }], + &snapshot, + ); + buffer.update_diagnostics(LanguageServerId(0), set, cx); + }); + + // Hover pops diagnostic immediately + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + cx.background_executor.run_until_parked(); + + cx.editor(|Editor { hover_state, .. }, _| { + assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) + }); + + // Info Popover shows after request responded to + let range = cx.lsp_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some new docs".to_string(), + }), + range: Some(range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + + cx.background_executor.run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }); + } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx.add_window(|cx| Editor::single_line(cx)); + editor + .update(cx, |editor, cx| { + let style = editor.style.clone().unwrap(); + + struct Row { + blocks: Vec, + expected_marked_text: String, + expected_styles: Vec, + } + + let rows = &[ + // Strong emphasis + Row { + blocks: vec![HoverBlock { + text: "one **two** three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }], + }, + // Links + Row { + blocks: vec![HoverBlock { + text: "one [two](https://the-url) three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Lists + Row { + blocks: vec![HoverBlock { + text: " + lists: + * one + - a + - b + * two + - [c](https://the-url) + - d" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + lists: + - one + - a + - b + - two + - «c» + - d" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Multi-paragraph list items + Row { + blocks: vec![HoverBlock { + text: " + * one two + three + + * four five + * six seven + eight + + nine + * ten + * six" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + - one two three + - four five + - six seven eight + + nine + - ten + - six" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + ]; + + for Row { + blocks, + expected_marked_text, + expected_styles, + } in &rows[0..] + { + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + + let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); + let expected_highlights = ranges + .into_iter() + .zip(expected_styles.iter().cloned()) + .collect::>(); + assert_eq!( + rendered.text, expected_text, + "wrong text for input {blocks:?}" + ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect(); + + assert_eq!( + rendered_highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + }) + .unwrap(); + } + + #[gpui::test] + async fn test_hover_inlay_label_parts(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 cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.background_executor.run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len()..": ".len() + new_type_label.len(), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } +} diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index d36762f3955e126200d7025002d543e9744bce79..092882573c59961dc9e6cba6ee65aa022367107d 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -5,7 +5,7 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, SelectPhase, }; -use gpui::{Task, ViewContext}; +use gpui::{px, Task, ViewContext}; use language::{Bias, ToOffset}; use lsp::LanguageServerId; use project::{ @@ -13,6 +13,7 @@ use project::{ ResolveState, }; use std::ops::Range; +use theme::ActiveTheme as _; use util::TryFutureExt; #[derive(Debug, Default)] @@ -485,40 +486,45 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // todo!() - // // Highlight symbol using theme link definition highlight style - // let style = theme::current(cx).editor.link_definition; - // let highlight_range = - // symbol_range.unwrap_or_else(|| match &trigger_point { - // TriggerPoint::Text(trigger_anchor) => { - // let snapshot = &snapshot.buffer_snapshot; - // // If no symbol range returned from language server, use the surrounding word. - // let (offset_range, _) = - // snapshot.surrounding_word(*trigger_anchor); - // RangeInEditor::Text( - // snapshot.anchor_before(offset_range.start) - // ..snapshot.anchor_after(offset_range.end), - // ) - // } - // TriggerPoint::InlayHint(highlight, _, _) => { - // RangeInEditor::Inlay(highlight.clone()) - // } - // }); - - // match highlight_range { - // RangeInEditor::Text(text_range) => this - // .highlight_text::( - // vec![text_range], - // style, - // cx, - // ), - // RangeInEditor::Inlay(highlight) => this - // .highlight_inlays::( - // vec![highlight], - // style, - // cx, - // ), - // } + let style = gpui::HighlightStyle { + underline: Some(gpui::UnderlineStyle { + thickness: px(1.), + ..Default::default() + }), + color: Some(gpui::red()), + ..Default::default() + }; + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + RangeInEditor::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(highlight, _, _) => { + RangeInEditor::Inlay(highlight.clone()) + } + }); + + match highlight_range { + RangeInEditor::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + RangeInEditor::Inlay(highlight) => this + .highlight_inlays::( + vec![highlight], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index bcf41f135ba44b0562d1351901a452b04a1ef10b..6542ace5fbe057d8d8fb65cfa1f615ebeb3d89c1 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -595,31 +595,32 @@ impl<'a> MutableSelectionsCollection<'a> { self.select(selections) } - pub fn select_anchor_ranges>>(&mut self, ranges: I) { - todo!() - // let buffer = self.buffer.read(self.cx).snapshot(self.cx); - // let selections = ranges - // .into_iter() - // .map(|range| { - // let mut start = range.start; - // let mut end = range.end; - // let reversed = if start.cmp(&end, &buffer).is_gt() { - // mem::swap(&mut start, &mut end); - // true - // } else { - // false - // }; - // Selection { - // id: post_inc(&mut self.collection.next_selection_id), - // start, - // end, - // reversed, - // goal: SelectionGoal::None, - // } - // }) - // .collect::>(); - - // self.select_anchors(selections) + pub fn select_anchor_ranges(&mut self, ranges: I) + where + I: IntoIterator>, + { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start.cmp(&end, &buffer).is_gt() { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } + }) + .collect::>(); + self.select_anchors(selections) } pub fn new_selection_id(&mut self) -> usize { diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index ec37c57f2c9656a978e289cbb33f8580004f98f9..4f6e157e4e0cbf27831b578aeeb25860cbc8e632 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -27,7 +27,7 @@ pub fn marked_display_snapshot( let (unmarked_text, markers) = marked_text_offsets(text); let font = cx.text_style().font(); - let font_size: Pixels = 14.into(); + let font_size: Pixels = 14usize.into(); let buffer = MultiBuffer::build_simple(&unmarked_text, cx); let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); diff --git a/crates/feature_flags2/src/feature_flags2.rs b/crates/feature_flags2/src/feature_flags2.rs index 23167796ecd8a50cc2a5221952beb72486d74025..065d06f96d1b1765828329464ddb149da371d63b 100644 --- a/crates/feature_flags2/src/feature_flags2.rs +++ b/crates/feature_flags2/src/feature_flags2.rs @@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt { impl FeatureFlagViewExt for ViewContext<'_, V> where - V: 'static + Send + Sync, + V: 'static, { fn observe_flag(&mut self, callback: F) -> Subscription where - F: Fn(bool, &mut V, &mut ViewContext) + Send + Sync + 'static, + F: Fn(bool, &mut V, &mut ViewContext) + 'static, { self.observe_global::(move |v, cx| { let feature_flags = cx.global::(); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index b7a4a387ab63a8592992c538e606c6e0d915290f..156b062df85f2c1fed4aa30fda7e41aa507297a4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -518,6 +518,7 @@ impl PickerDelegate for FileFinderDelegate { } fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + let raw_query = raw_query.trim(); if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); @@ -539,7 +540,6 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - let raw_query = &raw_query; let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { Ok::<_, std::convert::Infallible>(FileSearchQuery { raw_query: raw_query.to_owned(), @@ -735,6 +735,7 @@ mod tests { cx.dispatch_action(window.into(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder .update(cx, |finder, cx| { finder.delegate_mut().update_matches("bna".to_string(), cx) @@ -743,7 +744,6 @@ mod tests { finder.read_with(cx, |finder, _| { assert_eq!(finder.delegate().matches.len(), 2); }); - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); cx.dispatch_action(window.into(), SelectNext); cx.dispatch_action(window.into(), Confirm); @@ -762,6 +762,49 @@ mod tests { "bandana" ); }); + + for bandana_query in [ + "bandana", + " bandana", + "bandana ", + " bandana ", + " ndan ", + " band ", + ] { + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(bandana_query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + assert_eq!( + finder.delegate().matches.len(), + 1, + "Wrong number of matches for bandana query '{bandana_query}'" + ); + }); + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + assert_eq!( + active_item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .title(cx), + "bandana", + "Wrong match for bandana query '{bandana_query}'" + ); + }); + } } #[gpui::test] diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 873054a68c77c81ecac723b2ad1169ef9eca3775..7a00b2644a577ca6b54045b18bd9e16404e7acd0 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - IntoElement, Manager, Model, ParentElement, Render, Styled, Task, View, ViewContext, - VisualContext, WeakView, + actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model, + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -16,8 +15,7 @@ use std::{ }, }; use text::Point; -use theme::ActiveTheme; -use ui::{v_stack, HighlightedLabel, StyledExt}; +use ui::{v_stack, HighlightedLabel, ListItem}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; @@ -111,7 +109,7 @@ impl FileFinder { } } -impl EventEmitter for FileFinder {} +impl EventEmitter for FileFinder {} impl FocusableView for FileFinder { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) @@ -530,7 +528,7 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { - type ListItem = Div; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Search project files...".into() @@ -554,6 +552,7 @@ impl PickerDelegate for FileFinderDelegate { raw_query: String, cx: &mut ViewContext>, ) -> Task<()> { + let raw_query = raw_query.trim(); if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); @@ -575,7 +574,6 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - let raw_query = &raw_query; let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { Ok::<_, std::convert::Infallible>(FileSearchQuery { raw_query: raw_query.to_owned(), @@ -689,9 +687,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - finder - .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss)) - .ok()?; + finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?; Some(()) }) @@ -702,7 +698,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } @@ -711,30 +707,22 @@ impl PickerDelegate for FileFinderDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { + ) -> Option { let path_match = self .matches .get(ix) .expect("Invalid matches state: no element for index {ix}"); - let theme = cx.theme(); - let colors = theme.colors(); let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( + Some( + ListItem::new(ix).inset(true).selected(selected).child( v_stack() .child(HighlightedLabel::new(file_name, file_name_positions)) .child(HighlightedLabel::new(full_path, full_path_positions)), - ) + ), + ) } } @@ -778,18 +766,49 @@ mod tests { let (picker, workspace, cx) = build_find_picker(project, cx); cx.simulate_input("bna"); - picker.update(cx, |picker, _| { assert_eq!(picker.delegate.matches.len(), 2); }); - cx.dispatch_action(SelectNext); cx.dispatch_action(Confirm); - cx.read(|cx| { let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); assert_eq!(active_editor.read(cx).title(cx), "bandana"); }); + + for bandana_query in [ + "bandana", + " bandana", + "bandana ", + " bandana ", + " ndan ", + " band ", + ] { + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(bandana_query.to_string(), cx) + }) + .await; + picker.update(cx, |picker, _| { + assert_eq!( + picker.delegate.matches.len(), + 1, + "Wrong number of matches for bandana query '{bandana_query}'" + ); + }); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!( + active_editor.read(cx).title(cx), + "bandana", + "Wrong match for bandana query '{bandana_query}'" + ); + }); + } } #[gpui::test] diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 61f5742750a08ea35ffec2592c526e47f8b0a377..5ad95c1f6ea6cf0fb49e185a044d8be7eaf383fe 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,13 +1,13 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager, - Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, + WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, v_stack, Color, Label, StyledExt}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::Workspace; actions!(Toggle); @@ -25,22 +25,24 @@ pub struct GoToLine { impl FocusableView for GoToLine { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.active_editor.focus_handle(cx) + self.line_editor.focus_handle(cx) } } -impl EventEmitter for GoToLine {} +impl EventEmitter for GoToLine {} impl GoToLine { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &Toggle, cx| { - let Some(editor) = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::()) - else { + fn register(editor: &mut Editor, cx: &mut ViewContext) { + let handle = cx.view().downgrade(); + editor.register_action(move |_: &Toggle, cx| { + let Some(editor) = handle.upgrade() else { return; }; - - workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); + let Some(workspace) = editor.read(cx).workspace() else { + return; + }; + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); + }) }); } @@ -88,7 +90,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss), + editor::EditorEvent::Blurred => cx.emit(DismissEvent), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -123,7 +125,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +142,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/gpui2/build.rs b/crates/gpui2/build.rs index 6e8a0868b969c7a85627fd974f0f1bde793eb587..24e493cb812d6e7478b266cc621eea7cbc77b051 100644 --- a/crates/gpui2/build.rs +++ b/crates/gpui2/build.rs @@ -65,6 +65,8 @@ fn generate_shader_bindings() -> PathBuf { "MonochromeSprite".into(), "PolychromeSprite".into(), "PathSprite".into(), + "SurfaceInputIndex".into(), + "SurfaceBounds".into(), ]); config.no_includes = true; config.enumeration.prefix_with_name = true; diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 958eaabdb83076dcd8114ba1f06ae2ac4fc6c50b..03ef2d2281876ca101e210b1d06491413e1ce027 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -162,6 +162,7 @@ macro_rules! actions { ( $name:ident ) => { #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)] + #[serde(crate = "gpui::serde")] pub struct $name; }; diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 617c0b5600742a07029863468b785376c1d53224..94a7d3be0b8b2cf239d29f1a29d7b6bb0b7d2bbf 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -520,6 +520,10 @@ impl AppContext { self.platform.should_auto_hide_scrollbars() } + pub fn restart(&self) { + self.platform.restart() + } + pub(crate) fn push_effect(&mut self, effect: Effect) { match &effect { Effect::Notify { emitter } => { @@ -580,7 +584,7 @@ impl AppContext { .windows .iter() .filter_map(|(_, window)| { - let window = window.as_ref().unwrap(); + let window = window.as_ref()?; if window.dirty { Some(window.handle.clone()) } else { @@ -1049,7 +1053,9 @@ impl Context for AppContext { let root_view = window.root_view.clone().unwrap(); let result = update(root_view, &mut WindowContext::new(cx, &mut window)); - if !window.removed { + if window.removed { + cx.windows.remove(handle.id); + } else { cx.windows .get_mut(handle.id) .ok_or_else(|| anyhow!("window not found"))? diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index cc3b0ace57b37d639656ef06fbc5a2c2344b7877..92ccc118f12826076621629a470dfda5c6a545d1 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -1,7 +1,7 @@ use crate::{ - AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView, - ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext, - VisualContext, WindowContext, WindowHandle, + AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent, + FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, + ViewContext, VisualContext, WindowContext, WindowHandle, }; use anyhow::{anyhow, Context as _}; use derive_more::{Deref, DerefMut}; @@ -325,8 +325,7 @@ impl VisualContext for AsyncWindowContext { where V: crate::ManagedView, { - self.window.update(self, |_, cx| { - view.update(cx, |_, cx| cx.emit(Manager::Dismiss)) - }) + self.window + .update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent))) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 2bd3a069caa28c22deb84c156ee590c14029d67d..9637720a67e78ba6f735534f924b4028d8d8c75f 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { { self.window .update(self.cx, |_, cx| { - view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss)) + view.update(cx, |_, cx| cx.emit(crate::DismissEvent)) }) .unwrap() } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index b18ffb8ca68ceadad27692bc4ef4ba403a7ce3dd..3c8f678b89dca4e0d7968a9b94e2298140324479 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -111,7 +111,7 @@ pub struct Component { pub struct CompositeElementState { rendered_element: Option<::Element>, - rendered_element_state: <::Element as Element>::State, + rendered_element_state: Option<<::Element as Element>::State>, } impl Component { @@ -131,20 +131,40 @@ impl Element for Component { cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let mut element = self.component.take().unwrap().render(cx).into_element(); - let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx); - let state = CompositeElementState { - rendered_element: Some(element), - rendered_element_state: state, - }; - (layout_id, state) + if let Some(element_id) = element.element_id() { + let layout_id = + cx.with_element_state(element_id, |state, cx| element.layout(state, cx)); + let state = CompositeElementState { + rendered_element: Some(element), + rendered_element_state: None, + }; + (layout_id, state) + } else { + let (layout_id, state) = + element.layout(state.and_then(|s| s.rendered_element_state), cx); + let state = CompositeElementState { + rendered_element: Some(element), + rendered_element_state: Some(state), + }; + (layout_id, state) + } } fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { - state - .rendered_element - .take() - .unwrap() - .paint(bounds, &mut state.rendered_element_state, cx); + let element = state.rendered_element.take().unwrap(); + if let Some(element_id) = element.element_id() { + cx.with_element_state(element_id, |element_state, cx| { + let mut element_state = element_state.unwrap(); + element.paint(bounds, &mut element_state, cx); + ((), element_state) + }); + } else { + element.paint( + bounds, + &mut state.rendered_element_state.as_mut().unwrap(), + cx, + ); + } } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 406f2ea31179d265202b7bd381c3b8b782b813c9..ced0a4767cc988d58ea49dcc6b3f3c7a78b34b4f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -3,7 +3,8 @@ use crate::{ BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, - SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, + SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, + WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -11,6 +12,7 @@ use smallvec::SmallVec; use std::{ any::{Any, TypeId}, cell::RefCell, + cmp::Ordering, fmt::Debug, mem, rc::Rc, @@ -84,7 +86,7 @@ pub trait InteractiveElement: Sized + Element { move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button - && bounds.contains_point(&event.position) + && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } @@ -99,7 +101,7 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } }, @@ -117,7 +119,7 @@ pub trait InteractiveElement: Sized + Element { .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button - && bounds.contains_point(&event.position) + && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } @@ -132,7 +134,7 @@ pub trait InteractiveElement: Sized + Element { self.interactivity() .mouse_up_listeners .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx) } })); @@ -145,7 +147,8 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { + if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx) + { (listener)(event, cx) } }, @@ -163,7 +166,7 @@ pub trait InteractiveElement: Sized + Element { .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Capture && event.button == button - && !bounds.contains_point(&event.position) + && !bounds.visibly_contains(&event.position, cx) { (listener)(event, cx); } @@ -177,7 +180,7 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().mouse_move_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx); } }, @@ -191,7 +194,7 @@ pub trait InteractiveElement: Sized + Element { ) -> Self { self.interactivity().scroll_wheel_listeners.push(Box::new( move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { (listener)(event, cx); } }, @@ -355,6 +358,11 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.interactivity().scroll_handle = Some(scroll_handle.clone()); + self + } + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -526,15 +534,15 @@ pub type FocusListeners = SmallVec<[FocusListener; 2]>; pub type FocusListener = Box; pub type MouseDownListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type MouseUpListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type MouseMoveListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type ScrollWheelListener = - Box, DispatchPhase, &mut WindowContext) + 'static>; + Box; pub type ClickListener = Box; @@ -624,6 +632,26 @@ impl Element for Div { let mut child_max = Point::default(); let content_size = if element_state.child_layout_ids.is_empty() { bounds.size + } else if let Some(scroll_handle) = self.interactivity.scroll_handle.as_ref() { + let mut state = scroll_handle.0.borrow_mut(); + state.child_bounds = Vec::with_capacity(element_state.child_layout_ids.len()); + state.bounds = bounds; + let requested = state.requested_scroll_top.take(); + + for (ix, child_layout_id) in element_state.child_layout_ids.iter().enumerate() { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + state.child_bounds.push(child_bounds); + + if let Some(requested) = requested.as_ref() { + if requested.0 == ix { + *state.offset.borrow_mut() = + bounds.origin - (child_bounds.origin - point(px(0.), requested.1)); + } + } + } + (child_max - child_min).into() } else { for child_layout_id in &element_state.child_layout_ids { let child_bounds = cx.layout_bounds(*child_layout_id); @@ -694,6 +722,7 @@ pub struct Interactivity { pub key_context: KeyContext, pub focusable: bool, pub tracked_focus_handle: Option, + pub scroll_handle: Option, pub focus_listeners: FocusListeners, pub group: Option, pub base_style: StyleRefinement, @@ -719,6 +748,18 @@ pub struct Interactivity { pub tooltip_builder: Option, } +#[derive(Clone)] +pub struct InteractiveBounds { + pub bounds: Bounds, + pub stacking_order: StackingOrder, +} + +impl InteractiveBounds { + pub fn visibly_contains(&self, point: &Point, cx: &WindowContext) -> bool { + self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order) + } +} + impl Interactivity { pub fn layout( &mut self, @@ -740,6 +781,10 @@ impl Interactivity { }); } + if let Some(scroll_handle) = self.scroll_handle.as_ref() { + element_state.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); + } + let style = self.compute_style(None, &mut element_state, cx); let layout_id = f(style, cx); (layout_id, element_state) @@ -755,34 +800,52 @@ impl Interactivity { ) { let style = self.compute_style(Some(bounds), element_state, cx); + if style + .background + .as_ref() + .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + { + cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds)) + } + + let interactive_bounds = Rc::new(InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }); + if let Some(mouse_cursor) = style.mouse_cursor { - let hovered = bounds.contains_point(&cx.mouse_position()); + let mouse_position = &cx.mouse_position(); + let hovered = interactive_bounds.visibly_contains(mouse_position, cx); if hovered { cx.set_cursor_style(mouse_cursor); } } for listener in self.mouse_down_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } for listener in self.mouse_up_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } for listener in self.mouse_move_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } for listener in self.scroll_wheel_listeners.drain(..) { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - listener(event, &bounds, phase, cx); + listener(event, &*interactive_bounds, phase, cx); }) } @@ -803,8 +866,9 @@ impl Interactivity { } if self.hover_style.is_some() - || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) + || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() { + let bounds = bounds.intersect(&cx.content_mask().bounds); let hovered = bounds.contains_point(&cx.mouse_position()); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { @@ -817,8 +881,11 @@ impl Interactivity { if cx.active_drag.is_some() { let drop_listeners = mem::take(&mut self.drop_listeners); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, &cx) + { if let Some(drag_state_type) = cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) { @@ -847,6 +914,7 @@ impl Interactivity { if let Some(mouse_down) = mouse_down { if let Some(drag_listener) = drag_listener { let active_state = element_state.clicked_state.clone(); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if cx.active_drag.is_some() { @@ -854,7 +922,7 @@ impl Interactivity { cx.notify(); } } else if phase == DispatchPhase::Bubble - && bounds.contains_point(&event.position) + && interactive_bounds.visibly_contains(&event.position, cx) && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD { *active_state.borrow_mut() = ElementClickedState::default(); @@ -867,8 +935,11 @@ impl Interactivity { }); } + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { let mouse_click = ClickEvent { down: mouse_down.clone(), up: event.clone(), @@ -881,8 +952,11 @@ impl Interactivity { cx.notify(); }); } else { + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { *pending_mouse_down.borrow_mut() = Some(event.clone()); cx.notify(); } @@ -893,13 +967,14 @@ impl Interactivity { if let Some(hover_listener) = self.hover_listener.take() { let was_hovered = element_state.hover_state.clone(); let has_mouse_down = element_state.pending_mouse_down.clone(); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - let is_hovered = - bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none(); + let is_hovered = interactive_bounds.visibly_contains(&event.position, cx) + && has_mouse_down.borrow().is_none(); let mut was_hovered = was_hovered.borrow_mut(); if is_hovered != was_hovered.clone() { @@ -914,14 +989,15 @@ impl Interactivity { if let Some(tooltip_builder) = self.tooltip_builder.take() { let active_tooltip = element_state.active_tooltip.clone(); let pending_mouse_down = element_state.pending_mouse_down.clone(); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - let is_hovered = - bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none(); + let is_hovered = interactive_bounds.visibly_contains(&event.position, cx) + && pending_mouse_down.borrow().is_none(); if !is_hovered { active_tooltip.borrow_mut().take(); return; @@ -979,11 +1055,12 @@ impl Interactivity { .group_active_style .as_ref() .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble { let group = active_group_bounds .map_or(false, |bounds| bounds.contains_point(&down.position)); - let element = bounds.contains_point(&down.position); + let element = interactive_bounds.visibly_contains(&down.position, cx); if group || element { *active_state.borrow_mut() = ElementClickedState { group, element }; cx.notify(); @@ -1000,9 +1077,12 @@ impl Interactivity { .clone(); let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); + let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&event.position, cx) + { let mut scroll_offset = scroll_offset.borrow_mut(); let old_scroll_offset = *scroll_offset; let delta = event.delta.pixel_delta(line_height); @@ -1093,19 +1173,22 @@ impl Interactivity { let mouse_position = cx.mouse_position(); if let Some(group_hover) = self.group_hover_style.as_ref() { if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains_point(&mouse_position) { + if group_bounds.contains_point(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) + { style.refine(&group_hover.style); } } } - // if self.hover_style.is_some() { - if bounds.contains_point(&mouse_position) { - // eprintln!("div hovered {bounds:?} {mouse_position:?}"); - style.refine(&self.hover_style); - } else { - // eprintln!("div NOT hovered {bounds:?} {mouse_position:?}"); + if self.hover_style.is_some() { + if bounds + .intersect(&cx.content_mask().bounds) + .contains_point(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) + { + style.refine(&self.hover_style); + } } - // } if let Some(drag) = cx.active_drag.take() { for (state_type, group_drag_style) in &self.group_drag_over_styles { @@ -1120,7 +1203,9 @@ impl Interactivity { for (state_type, drag_over_style) in &self.drag_over_styles { if *state_type == drag.view.entity_type() - && bounds.contains_point(&mouse_position) + && bounds + .intersect(&cx.content_mask().bounds) + .contains_point(&mouse_position) { style.refine(drag_over_style); } @@ -1152,6 +1237,7 @@ impl Default for Interactivity { key_context: KeyContext::default(), focusable: false, tracked_focus_handle: None, + scroll_handle: None, focus_listeners: SmallVec::default(), // scroll_offset: Point::default(), group: None, @@ -1375,3 +1461,83 @@ where self.element.children_mut() } } + +#[derive(Default)] +struct ScrollHandleState { + // not great to have the nested rc's... + offset: Rc>>, + bounds: Bounds, + child_bounds: Vec>, + requested_scroll_top: Option<(usize, Pixels)>, +} + +#[derive(Clone)] +pub struct ScrollHandle(Rc>); + +impl ScrollHandle { + pub fn new() -> Self { + Self(Rc::default()) + } + + pub fn offset(&self) -> Point { + self.0.borrow().offset.borrow().clone() + } + + pub fn top_item(&self) -> usize { + let state = self.0.borrow(); + let top = state.bounds.top() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if top < bounds.top() { + Ordering::Greater + } else if top > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + pub fn bounds_for_item(&self, ix: usize) -> Option> { + self.0.borrow().child_bounds.get(ix).cloned() + } + + /// scroll_to_item scrolls the minimal amount to ensure that the item is + /// fully visible + pub fn scroll_to_item(&self, ix: usize) { + let state = self.0.borrow(); + + let Some(bounds) = state.child_bounds.get(ix) else { + return; + }; + + let scroll_offset = state.offset.borrow().y; + + if bounds.top() + scroll_offset < state.bounds.top() { + state.offset.borrow_mut().y = state.bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset > state.bounds.bottom() { + state.offset.borrow_mut().y = state.bounds.bottom() - bounds.bottom(); + } + } + + pub fn logical_scroll_top(&self) -> (usize, Pixels) { + let ix = self.top_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.top() + state.offset.borrow().y - state.bounds.top(), + ) + } else { + (ix, px(0.)) + } + } + + pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) { + self.0.borrow_mut().requested_scroll_top = Some((ix, px)); + } +} diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index f7dcd7ab82b35b25f4824dd191831acd183f9569..f6aae2de66aebb7bc894bd63a8d9a85a6e74e089 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,30 +1,67 @@ +use std::sync::Arc; + use crate::{ - Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, - LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, + point, size, Bounds, DevicePixels, Element, ImageData, InteractiveElement, + InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size, + StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; +use media::core_video::CVImageBuffer; use util::ResultExt; +#[derive(Clone, Debug)] +pub enum ImageSource { + /// Image content will be loaded from provided URI at render time. + Uri(SharedString), + Data(Arc), + Surface(CVImageBuffer), +} + +impl From for ImageSource { + fn from(value: SharedString) -> Self { + Self::Uri(value) + } +} + +impl From<&'static str> for ImageSource { + fn from(uri: &'static str) -> Self { + Self::Uri(uri.into()) + } +} + +impl From for ImageSource { + fn from(uri: String) -> Self { + Self::Uri(uri.into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Data(value) + } +} + +impl From for ImageSource { + fn from(value: CVImageBuffer) -> Self { + Self::Surface(value) + } +} + pub struct Img { interactivity: Interactivity, - uri: Option, + source: ImageSource, grayscale: bool, } -pub fn img() -> Img { +pub fn img(source: impl Into) -> Img { Img { interactivity: Interactivity::default(), - uri: None, + source: source.into(), grayscale: false, } } impl Img { - pub fn uri(mut self, uri: impl Into) -> Self { - self.uri = Some(uri.into()); - self - } - pub fn grayscale(mut self, grayscale: bool) -> Self { self.grayscale = grayscale; self @@ -39,9 +76,8 @@ impl Element for Img { element_state: Option, cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - self.interactivity.layout(element_state, cx, |style, cx| { - cx.request_layout(&style, None) - }) + self.interactivity + .layout(element_state, cx, |style, cx| cx.request_layout(&style, [])) } fn paint( @@ -56,31 +92,43 @@ impl Element for Img { element_state, cx, |style, _scroll_offset, cx| { - let corner_radii = style.corner_radii; - - if let Some(uri) = self.uri.clone() { - // eprintln!(">>> image_cache.get({uri}"); - let image_future = cx.image_cache.get(uri.clone()); - // eprintln!("<<< image_cache.get({uri}"); - if let Some(data) = image_future - .clone() - .now_or_never() - .and_then(|result| result.ok()) - { - let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - cx.paint_image(bounds, corner_radii, data, self.grayscale) - .log_err() - }); - } else { - cx.spawn(|mut cx| async move { - if image_future.await.ok().is_some() { - cx.on_next_frame(|cx| cx.notify()); + let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size()); + cx.with_z_index(1, |cx| { + match self.source { + ImageSource::Uri(uri) => { + let image_future = cx.image_cache.get(uri.clone()); + if let Some(data) = image_future + .clone() + .now_or_never() + .and_then(|result| result.ok()) + { + let new_bounds = preserve_aspect_ratio(bounds, data.size()); + cx.paint_image(new_bounds, corner_radii, data, self.grayscale) + .log_err(); + } else { + cx.spawn(|mut cx| async move { + if image_future.await.ok().is_some() { + cx.on_next_frame(|cx| cx.notify()); + } + }) + .detach(); } - }) - .detach() - } - } + } + + ImageSource::Data(data) => { + let new_bounds = preserve_aspect_ratio(bounds, data.size()); + cx.paint_image(new_bounds, corner_radii, data, self.grayscale) + .log_err(); + } + + ImageSource::Surface(surface) => { + let size = size(surface.width().into(), surface.height().into()); + let new_bounds = preserve_aspect_ratio(bounds, size); + // TODO: Add support for corner_radii and grayscale. + cx.paint_surface(new_bounds, surface); + } + }; + }); }, ) } @@ -109,3 +157,29 @@ impl InteractiveElement for Img { &mut self.interactivity } } + +fn preserve_aspect_ratio(bounds: Bounds, image_size: Size) -> Bounds { + let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension))); + let image_ratio = image_size.width / image_size.height; + let bounds_ratio = bounds.size.width / bounds.size.height; + + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } +} diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index d8aad4a42f9413f60e05d59bfab5d0606d4dd5d2..764bdfabcd6695d3ee0b4dd71cc51e472567c09e 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -144,9 +144,11 @@ impl Element for Overlay { } cx.with_element_offset(desired.origin - bounds.origin, |cx| { - for child in self.children { - child.paint(cx); - } + cx.break_content_mask(|cx| { + for child in self.children { + child.paint(cx); + } + }) }) } } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index f4745836a5fc7824864ff5083bcd2a0cc10942a5..aebec237c726ca6d693e471f87e8d9b093e5e46a 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -287,7 +287,9 @@ impl TextState { pub struct InteractiveText { element_id: ElementId, text: StyledText, - click_listener: Option)>>, + click_listener: + Option], InteractiveTextClickEvent, &mut WindowContext<'_>)>>, + clickable_ranges: Vec>, } struct InteractiveTextClickEvent { @@ -306,6 +308,7 @@ impl InteractiveText { element_id: id.into(), text, click_listener: None, + clickable_ranges: Vec::new(), } } @@ -314,7 +317,7 @@ impl InteractiveText { ranges: Vec>, listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, ) -> Self { - self.click_listener = Some(Box::new(move |event, cx| { + self.click_listener = Some(Box::new(move |ranges, event, cx| { for (range_ix, range) in ranges.iter().enumerate() { if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) { @@ -322,6 +325,7 @@ impl InteractiveText { } } })); + self.clickable_ranges = ranges; self } } @@ -356,6 +360,19 @@ impl Element for InteractiveText { fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { if let Some(click_listener) = self.click_listener { + if let Some(ix) = state + .text_state + .index_for_position(bounds, cx.mouse_position()) + { + if self + .clickable_ranges + .iter() + .any(|range| range.contains(&ix)) + { + cx.set_cursor_style(crate::CursorStyle::PointingHand) + } + } + let text_state = state.text_state.clone(); let mouse_down = state.mouse_down_index.clone(); if let Some(mouse_down_index) = mouse_down.get() { @@ -365,6 +382,7 @@ impl Element for InteractiveText { text_state.index_for_position(bounds, event.position) { click_listener( + &self.clickable_ranges, InteractiveTextClickEvent { mouse_down_index, mouse_up_index, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 1ddddf9c783fd5fbca5cdee60a5860f9e2a9c29d..2d5a46f3d99ef4887714b905f24cfbd29d5410c5 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -9,7 +9,7 @@ use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, -/// uniform_list will only render the visibile subset of items. +/// uniform_list will only render the visible subset of items. pub fn uniform_list( view: View, id: I, @@ -173,7 +173,7 @@ impl Element for UniformList { let item_size = element_state.item_size; let content_size = Size { width: padded_bounds.size.width, - height: item_size.height * self.item_count, + height: item_size.height * self.item_count + padding.top + padding.bottom, }; let shared_scroll_offset = element_state @@ -221,9 +221,7 @@ impl Element for UniformList { let items = (self.render_items)(visible_range.clone(), cx); cx.with_z_index(1, |cx| { - let content_mask = ContentMask { - bounds: padded_bounds, - }; + let content_mask = ContentMask { bounds }; cx.with_content_mask(Some(content_mask), |cx| { for (item, ix) in items.into_iter().zip(visible_range) { let item_origin = padded_bounds.origin diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index e1f039e309466bcd07e867551b2e082b27c6a186..d32c2e849be6151e6a1909405ee96c9ff838cca3 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -740,7 +740,7 @@ impl Copy for Corners where T: Copy + Clone + Default + Debug {} Deserialize, )] #[repr(transparent)] -pub struct Pixels(pub(crate) f32); +pub struct Pixels(pub f32); impl std::ops::Div for Pixels { type Output = f32; @@ -905,6 +905,12 @@ impl From for usize { } } +impl From for Pixels { + fn from(pixels: usize) -> Self { + Pixels(pixels as f32) + } +} + #[derive( Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign, )] @@ -959,6 +965,18 @@ impl From for DevicePixels { } } +impl From for usize { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as usize + } +} + +impl From for DevicePixels { + fn from(device_pixels: usize) -> Self { + DevicePixels(device_pixels as i32) + } +} + #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] #[repr(transparent)] pub struct ScaledPixels(pub(crate) f32); @@ -1034,7 +1052,7 @@ impl sqlez::bindable::Bind for GlobalPixels { } #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] -pub struct Rems(f32); +pub struct Rems(pub f32); impl Mul for Rems { type Output = Pixels; diff --git a/crates/gpui2/src/platform/mac/metal_renderer.rs b/crates/gpui2/src/platform/mac/metal_renderer.rs index 0631c75de5e222e43f780f29fb9844f5e96c099c..19afb503324907a0b84bcac7722fc71ead05451d 100644 --- a/crates/gpui2/src/platform/mac/metal_renderer.rs +++ b/crates/gpui2/src/platform/mac/metal_renderer.rs @@ -1,7 +1,7 @@ use crate::{ point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, - Quad, ScaledPixels, Scene, Shadow, Size, Underline, + Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, }; use cocoa::{ base::{NO, YES}, @@ -9,6 +9,9 @@ use cocoa::{ quartzcore::AutoresizingMask, }; use collections::HashMap; +use core_foundation::base::TCFType; +use foreign_types::ForeignType; +use media::core_video::CVMetalTextureCache; use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; use smallvec::SmallVec; @@ -27,9 +30,11 @@ pub(crate) struct MetalRenderer { underlines_pipeline_state: metal::RenderPipelineState, monochrome_sprites_pipeline_state: metal::RenderPipelineState, polychrome_sprites_pipeline_state: metal::RenderPipelineState, + surfaces_pipeline_state: metal::RenderPipelineState, unit_vertices: metal::Buffer, instances: metal::Buffer, sprite_atlas: Arc, + core_video_texture_cache: CVMetalTextureCache, } impl MetalRenderer { @@ -143,6 +148,14 @@ impl MetalRenderer { "polychrome_sprite_fragment", MTLPixelFormat::BGRA8Unorm, ); + let surfaces_pipeline_state = build_pipeline_state( + &device, + &library, + "surfaces", + "surface_vertex", + "surface_fragment", + MTLPixelFormat::BGRA8Unorm, + ); let command_queue = device.new_command_queue(); let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); @@ -157,9 +170,11 @@ impl MetalRenderer { underlines_pipeline_state, monochrome_sprites_pipeline_state, polychrome_sprites_pipeline_state, + surfaces_pipeline_state, unit_vertices, instances, sprite_atlas, + core_video_texture_cache: CVMetalTextureCache::new(device.as_ptr()).unwrap(), } } @@ -268,6 +283,14 @@ impl MetalRenderer { command_encoder, ); } + PrimitiveBatch::Surfaces(surfaces) => { + self.draw_surfaces( + surfaces, + &mut instance_offset, + viewport_size, + command_encoder, + ); + } } } @@ -793,6 +816,102 @@ impl MetalRenderer { ); *offset = next_offset; } + + fn draw_surfaces( + &mut self, + surfaces: &[Surface], + offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) { + command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state); + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + for surface in surfaces { + let texture_size = size( + DevicePixels::from(surface.image_buffer.width() as i32), + DevicePixels::from(surface.image_buffer.height() as i32), + ); + + assert_eq!( + surface.image_buffer.pixel_format_type(), + media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ); + + let y_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + MTLPixelFormat::R8Unorm, + surface.image_buffer.plane_width(0), + surface.image_buffer.plane_height(0), + 0, + ) + .unwrap(); + let cb_cr_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + MTLPixelFormat::RG8Unorm, + surface.image_buffer.plane_width(1), + surface.image_buffer.plane_height(1), + 1, + ) + .unwrap(); + + align_offset(offset); + let next_offset = *offset + mem::size_of::(); + assert!( + next_offset <= INSTANCE_BUFFER_SIZE, + "instance buffer exhausted" + ); + + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Surfaces as u64, + Some(&self.instances), + *offset as u64, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::TextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_texture( + SurfaceInputIndex::YTexture as u64, + Some(y_texture.as_texture_ref()), + ); + command_encoder.set_fragment_texture( + SurfaceInputIndex::CbCrTexture as u64, + Some(cb_cr_texture.as_texture_ref()), + ); + + unsafe { + let buffer_contents = + (self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds; + ptr::write( + buffer_contents, + SurfaceBounds { + bounds: surface.bounds, + content_mask: surface.content_mask.clone(), + }, + ); + } + + command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); + *offset = next_offset; + } + } } fn build_pipeline_state( @@ -898,6 +1017,16 @@ enum SpriteInputIndex { AtlasTexture = 4, } +#[repr(C)] +enum SurfaceInputIndex { + Vertices = 0, + Surfaces = 1, + ViewportSize = 2, + TextureSize = 3, + YTexture = 4, + CbCrTexture = 5, +} + #[repr(C)] enum PathRasterizationInputIndex { Vertices = 0, @@ -911,3 +1040,10 @@ pub struct PathSprite { pub color: Hsla, pub tile: AtlasTile, } + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct SurfaceBounds { + pub bounds: Bounds, + pub content_mask: ContentMask, +} diff --git a/crates/gpui2/src/platform/mac/shaders.metal b/crates/gpui2/src/platform/mac/shaders.metal index 4def1c33b85a430d051376322dca47de93b9e70c..aba01b9d5b059da1c1df55c0a01120d8be10775b 100644 --- a/crates/gpui2/src/platform/mac/shaders.metal +++ b/crates/gpui2/src/platform/mac/shaders.metal @@ -469,6 +469,58 @@ fragment float4 path_sprite_fragment( return color; } +struct SurfaceVertexOutput { + float4 position [[position]]; + float2 texture_position; + float clip_distance [[clip_distance]][4]; +}; + +struct SurfaceFragmentInput { + float4 position [[position]]; + float2 texture_position; +}; + +vertex SurfaceVertexOutput surface_vertex( + uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]], + constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]], + constant Size_DevicePixels *viewport_size + [[buffer(SurfaceInputIndex_ViewportSize)]], + constant Size_DevicePixels *texture_size + [[buffer(SurfaceInputIndex_TextureSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + SurfaceBounds surface = surfaces[surface_id]; + float4 device_position = + to_device_position(unit_vertex, surface.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds, + surface.content_mask.bounds); + // We are going to copy the whole texture, so the texture position corresponds + // to the current vertex of the unit triangle. + float2 texture_position = unit_vertex; + return SurfaceVertexOutput{ + device_position, + texture_position, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]], + texture2d y_texture + [[texture(SurfaceInputIndex_YTexture)]], + texture2d cb_cr_texture + [[texture(SurfaceInputIndex_CbCrTexture)]]) { + constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear); + const float4x4 ycbcrToRGBTransform = + float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), + float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), + float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), + float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); + float4 ycbcr = float4( + y_texture.sample(texture_sampler, input.texture_position).r, + cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0); + + return ycbcrToRGBTransform * ycbcr; +} + float4 hsla_to_rgba(Hsla hsla) { float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range float s = hsla.s; diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index bb3a659a62bb998d191d28e31e796e31ca1eb3fe..5b72c10851ff555b08669d8db96e143509e8ad46 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -683,6 +683,9 @@ impl Drop for MacWindow { this.executor .spawn(async move { unsafe { + // todo!() this panic()s when you click the red close button + // unless should_close returns false. + // (luckliy in zed it always returns false) window.close(); } }) diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index 87e89adfa0e15d18512d2a1a748ea5a212a48489..549260560236ffb43179caf6cb8fce5340c10b7a 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -25,6 +25,7 @@ pub(crate) struct SceneBuilder { underlines: Vec, monochrome_sprites: Vec, polychrome_sprites: Vec, + surfaces: Vec, } impl Default for SceneBuilder { @@ -38,6 +39,7 @@ impl Default for SceneBuilder { underlines: Vec::new(), monochrome_sprites: Vec::new(), polychrome_sprites: Vec::new(), + surfaces: Vec::new(), } } } @@ -120,6 +122,7 @@ impl SceneBuilder { (PrimitiveKind::PolychromeSprite, ix) => { self.polychrome_sprites[ix].order = draw_order as DrawOrder } + (PrimitiveKind::Surface, ix) => self.surfaces[ix].order = draw_order as DrawOrder, } } @@ -129,6 +132,7 @@ impl SceneBuilder { self.underlines.sort_unstable(); self.monochrome_sprites.sort_unstable(); self.polychrome_sprites.sort_unstable(); + self.surfaces.sort_unstable(); Scene { shadows: mem::take(&mut self.shadows), @@ -137,6 +141,7 @@ impl SceneBuilder { underlines: mem::take(&mut self.underlines), monochrome_sprites: mem::take(&mut self.monochrome_sprites), polychrome_sprites: mem::take(&mut self.polychrome_sprites), + surfaces: mem::take(&mut self.surfaces), } } @@ -185,6 +190,10 @@ impl SceneBuilder { sprite.order = layer_id; self.polychrome_sprites.push(sprite); } + Primitive::Surface(mut surface) => { + surface.order = layer_id; + self.surfaces.push(surface); + } } } } @@ -196,6 +205,7 @@ pub(crate) struct Scene { pub underlines: Vec, pub monochrome_sprites: Vec, pub polychrome_sprites: Vec, + pub surfaces: Vec, } impl Scene { @@ -224,6 +234,9 @@ impl Scene { polychrome_sprites: &self.polychrome_sprites, polychrome_sprites_start: 0, polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), + surfaces: &self.surfaces, + surfaces_start: 0, + surfaces_iter: self.surfaces.iter().peekable(), } } } @@ -247,6 +260,9 @@ struct BatchIterator<'a> { polychrome_sprites: &'a [PolychromeSprite], polychrome_sprites_start: usize, polychrome_sprites_iter: Peekable>, + surfaces: &'a [Surface], + surfaces_start: usize, + surfaces_iter: Peekable>, } impl<'a> Iterator for BatchIterator<'a> { @@ -272,6 +288,10 @@ impl<'a> Iterator for BatchIterator<'a> { self.polychrome_sprites_iter.peek().map(|s| s.order), PrimitiveKind::PolychromeSprite, ), + ( + self.surfaces_iter.peek().map(|s| s.order), + PrimitiveKind::Surface, + ), ]; orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); @@ -378,6 +398,21 @@ impl<'a> Iterator for BatchIterator<'a> { sprites: &self.polychrome_sprites[sprites_start..sprites_end], }) } + PrimitiveKind::Surface => { + let surfaces_start = self.surfaces_start; + let mut surfaces_end = surfaces_start; + while self + .surfaces_iter + .next_if(|surface| surface.order <= max_order) + .is_some() + { + surfaces_end += 1; + } + self.surfaces_start = surfaces_end; + Some(PrimitiveBatch::Surfaces( + &self.surfaces[surfaces_start..surfaces_end], + )) + } } } } @@ -391,6 +426,7 @@ pub enum PrimitiveKind { Underline, MonochromeSprite, PolychromeSprite, + Surface, } pub enum Primitive { @@ -400,6 +436,7 @@ pub enum Primitive { Underline(Underline), MonochromeSprite(MonochromeSprite), PolychromeSprite(PolychromeSprite), + Surface(Surface), } impl Primitive { @@ -411,6 +448,7 @@ impl Primitive { Primitive::Underline(underline) => &underline.bounds, Primitive::MonochromeSprite(sprite) => &sprite.bounds, Primitive::PolychromeSprite(sprite) => &sprite.bounds, + Primitive::Surface(surface) => &surface.bounds, } } @@ -422,6 +460,7 @@ impl Primitive { Primitive::Underline(underline) => &underline.content_mask, Primitive::MonochromeSprite(sprite) => &sprite.content_mask, Primitive::PolychromeSprite(sprite) => &sprite.content_mask, + Primitive::Surface(surface) => &surface.content_mask, } } } @@ -440,6 +479,7 @@ pub(crate) enum PrimitiveBatch<'a> { texture_id: AtlasTextureId, sprites: &'a [PolychromeSprite], }, + Surfaces(&'a [Surface]), } #[derive(Default, Debug, Clone, Eq, PartialEq)] @@ -593,6 +633,32 @@ impl From for Primitive { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Surface { + pub order: u32, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub image_buffer: media::core_video::CVImageBuffer, +} + +impl Ord for Surface { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Surface { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(surface: Surface) -> Self { + Primitive::Surface(surface) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct PathId(pub(crate) usize); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 7b39089ae0773295f8cf5d34090acdaa67f10003..76932f28e4a7dbe37434b9d785587b48008b5200 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -8,8 +8,8 @@ use crate::{ MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, - Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, - VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, + UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; @@ -18,6 +18,7 @@ use futures::{ channel::{mpsc, oneshot}, StreamExt, }; +use media::core_video::CVImageBuffer; use parking_lot::RwLock; use slotmap::SlotMap; use smallvec::SmallVec; @@ -39,8 +40,8 @@ use util::ResultExt; /// A global stacking order, which is created by stacking successive z-index values. /// Each z-index will always be interpreted in the context of its parent z-index. -#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)] -pub(crate) struct StackingOrder(pub(crate) SmallVec<[u32; 16]>); +#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)] +pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>); /// Represents the two different phases when dispatching events. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] @@ -193,13 +194,11 @@ pub trait FocusableView: 'static + Render { /// ManagedView is a view (like a Modal, Popover, Menu, etc.) /// where the lifecycle of the view is handled by another view. -pub trait ManagedView: FocusableView + EventEmitter {} +pub trait ManagedView: FocusableView + EventEmitter {} -impl> ManagedView for M {} +impl> ManagedView for M {} -pub enum Manager { - Dismiss, -} +pub struct DismissEvent; // Holds the state for a specific window. pub struct Window { @@ -243,7 +242,8 @@ pub(crate) struct Frame { pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, pub(crate) scene_builder: SceneBuilder, - z_index_stack: StackingOrder, + pub(crate) depth_map: Vec<(StackingOrder, Bounds)>, + pub(crate) z_index_stack: StackingOrder, content_mask_stack: Vec>, element_offset_stack: Vec>, } @@ -257,6 +257,7 @@ impl Frame { focus_listeners: Vec::new(), scene_builder: SceneBuilder::default(), z_index_stack: StackingOrder::default(), + depth_map: Default::default(), content_mask_stack: Vec::new(), element_offset_stack: Vec::new(), } @@ -806,6 +807,32 @@ impl<'a> WindowContext<'a> { result } + /// Called during painting to track which z-index is on top at each pixel position + pub fn add_opaque_layer(&mut self, bounds: Bounds) { + let stacking_order = self.window.current_frame.z_index_stack.clone(); + let depth_map = &mut self.window.current_frame.depth_map; + match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(&level)) { + Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)), + } + } + + /// Returns true if the top-most opaque layer painted over this point was part of the + /// same layer as the given stacking order. + pub fn was_top_layer(&self, point: &Point, level: &StackingOrder) -> bool { + for (stack, bounds) in self.window.previous_frame.depth_map.iter() { + if bounds.contains_point(point) { + return level.starts_with(stack) || stack.starts_with(level); + } + } + + false + } + + /// Called during painting to get the current stacking order. + pub fn stacking_order(&self) -> &StackingOrder { + &self.window.current_frame.z_index_stack + } + /// Paint one or more drop shadows into the scene for the current frame at the current z-index. pub fn paint_shadows( &mut self, @@ -1090,6 +1117,23 @@ impl<'a> WindowContext<'a> { Ok(()) } + /// Paint a surface into the scene for the current frame at the current z-index. + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + let window = &mut *self.window; + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, + Surface { + order: 0, + bounds, + content_mask, + image_buffer, + }, + ); + } + /// Draw pixels to the display for this window based on the contents of its scene. pub(crate) fn draw(&mut self) { let root_view = self.window.root_view.take().unwrap(); @@ -1153,6 +1197,7 @@ impl<'a> WindowContext<'a> { frame.mouse_listeners.values_mut().for_each(Vec::clear); frame.focus_listeners.clear(); frame.dispatch_tree.clear(); + frame.depth_map.clear(); } /// Dispatch a mouse or keyboard event on the window. @@ -1453,13 +1498,15 @@ impl<'a> WindowContext<'a> { } } - pub fn constructor_for( + pub fn handler_for( &self, view: &View, - f: impl Fn(&mut V, &mut ViewContext) -> R + 'static, - ) -> impl Fn(&mut WindowContext) -> R + 'static { - let view = view.clone(); - move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx)) + f: impl Fn(&mut V, &mut ViewContext) + 'static, + ) -> impl Fn(&mut WindowContext) { + let view = view.downgrade(); + move |cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, cx)).ok(); + } } //========== ELEMENT RELATED FUNCTIONS =========== @@ -1517,6 +1564,13 @@ impl<'a> WindowContext<'a> { .set_input_handler(Box::new(input_handler)); } } + + pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { + let mut this = self.to_async(); + self.window + .platform_window + .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true))) + } } impl Context for WindowContext<'_> { @@ -1663,7 +1717,7 @@ impl VisualContext for WindowContext<'_> { where V: ManagedView, { - self.update_view(view, |_, cx| cx.emit(Manager::Dismiss)) + self.update_view(view, |_, cx| cx.emit(DismissEvent)) } } @@ -1752,6 +1806,24 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { } } + /// Invoke the given function with the content mask reset to that + /// of the window. + fn break_content_mask(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + let mask = ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }; + self.window_mut() + .current_frame + .content_mask_stack + .push(mask); + let result = f(self); + self.window_mut().current_frame.content_mask_stack.pop(); + result + } + /// Update the global element offset relative to the current offset. This is used to implement /// scrolling. fn with_element_offset( @@ -1885,23 +1957,6 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { }) } - /// Like `with_element_state`, but for situations where the element_id is optional. If the - /// id is `None`, no state will be retrieved or stored. - fn with_optional_element_state( - &mut self, - element_id: Option, - f: impl FnOnce(Option, &mut Self) -> (R, S), - ) -> R - where - S: 'static, - { - if let Some(element_id) = element_id { - self.with_element_state(element_id, f) - } else { - f(None, self).0 - } - } - /// Obtain the current content mask. fn content_mask(&self) -> ContentMask { self.window() @@ -2349,7 +2404,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: ManagedView, { - self.defer(|_, cx| cx.emit(Manager::Dismiss)) + self.defer(|_, cx| cx.emit(DismissEvent)) } pub fn listener( @@ -2545,7 +2600,7 @@ impl WindowHandle { cx.read_window(self, |root_view, _cx| root_view.clone()) } - pub fn is_active(&self, cx: &WindowContext) -> Option { + pub fn is_active(&self, cx: &AppContext) -> Option { cx.windows .get(self.id) .and_then(|window| window.as_ref().map(|window| window.active)) diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 109d79cf708218345f0ac8705b770f6c4088a846..cf790e803e0b0ed46b72d394967b6fa329c361c6 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -98,9 +98,9 @@ mod tests { ); let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), + "function.special", + "function.async.rust", + "variable.builtin.self", ]; let map = HighlightMap::new(capture_names, &theme); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1d22d7773bad2ca8a2bc666957f0dd78ad05a273..811e54940672ee075993d8cd981c945199409675 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -197,8 +197,12 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { - self.adapter.workspace_configuration(cx) + pub fn workspace_configuration( + &self, + workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { + self.adapter.workspace_configuration(workspace_root, cx) } pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -312,7 +316,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> { futures::future::ready(serde_json::json!({})).boxed() } @@ -1383,7 +1387,7 @@ impl Language { let query = Query::new(self.grammar_mut().ts_language, source)?; let mut override_configs_by_id = HashMap::default(); - for (ix, name) in query.capture_names().iter().enumerate() { + for (ix, name) in query.capture_names().iter().copied().enumerate() { if !name.starts_with('_') { let value = self.config.overrides.remove(name).unwrap_or_default(); for server_name in &value.opt_into_language_servers { @@ -1396,7 +1400,7 @@ impl Language { } } - override_configs_by_id.insert(ix as u32, (name.clone(), value)); + override_configs_by_id.insert(ix as u32, (name.into(), value)); } } diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index bd50608122b80e9dd3ceba0a20d6b29dbb9f07c4..f20f481613eabfffddee791873d78d6383086ade 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; - if highlight_query_capture_names.contains(&name.as_str()) { + if highlight_query_capture_names.contains(&name) { actual_ranges.push(capture.node.byte_range()); } } diff --git a/crates/language2/src/highlight_map.rs b/crates/language2/src/highlight_map.rs index 1421ef672da935a7d2ff540e85591e4ff7d41be1..8e7a35233cf2e702536241099619cb0bff53459e 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -100,9 +100,9 @@ mod tests { }; let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), + "function.special", + "function.async.rust", + "variable.builtin.self", ]; let map = HighlightMap::new(capture_names, &theme); diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 311049f0328dac56df8e04720b7f2c74136234af..8fdf524f69e16a219bb264504051bb96f7e565ad 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -200,8 +200,12 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { - self.adapter.workspace_configuration(cx) + pub fn workspace_configuration( + &self, + workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { + self.adapter.workspace_configuration(workspace_root, cx) } pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -315,7 +319,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> { futures::future::ready(serde_json::json!({})).boxed() } @@ -1391,7 +1395,7 @@ impl Language { let mut override_configs_by_id = HashMap::default(); for (ix, name) in query.capture_names().iter().enumerate() { if !name.starts_with('_') { - let value = self.config.overrides.remove(name).unwrap_or_default(); + let value = self.config.overrides.remove(*name).unwrap_or_default(); for server_name in &value.opt_into_language_servers { if !self .config @@ -1402,7 +1406,7 @@ impl Language { } } - override_configs_by_id.insert(ix as u32, (name.clone(), value)); + override_configs_by_id.insert(ix as u32, (name.to_string(), value)); } } diff --git a/crates/language2/src/syntax_map/syntax_map_tests.rs b/crates/language2/src/syntax_map/syntax_map_tests.rs index bd50608122b80e9dd3ceba0a20d6b29dbb9f07c4..f20f481613eabfffddee791873d78d6383086ade 100644 --- a/crates/language2/src/syntax_map/syntax_map_tests.rs +++ b/crates/language2/src/syntax_map/syntax_map_tests.rs @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; - if highlight_query_capture_names.contains(&name.as_str()) { + if highlight_query_capture_names.contains(&name) { actual_ranges.push(capture.node.byte_range()); } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 98fd81f012f163402af47a840ae765e8760b062b..dc5b63d222d0f1545dfce7eab0748f5fd9c23e2d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -429,8 +429,8 @@ impl LanguageServer { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { - process_id: Default::default(), - root_path: Default::default(), + process_id: None, + root_path: None, root_uri: Some(root_uri.clone()), initialization_options: options, capabilities: ClientCapabilities { @@ -451,12 +451,15 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), + diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + refresh_support: None, + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - ..Default::default() + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -501,7 +504,7 @@ impl LanguageServer { }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - ..Default::default() + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -515,6 +518,20 @@ impl LanguageServer { }), dynamic_registration: Some(false), }), + publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { + related_information: Some(true), + ..Default::default() + }), + formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + on_type_formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + diagnostic: Some(DiagnosticClientCapabilities { + related_document_support: Some(true), + dynamic_registration: None, + }), ..Default::default() }), experimental: Some(json!({ @@ -524,15 +541,15 @@ impl LanguageServer { work_done_progress: Some(true), ..Default::default() }), - ..Default::default() + general: None, }, - trace: Default::default(), + trace: None, workspace_folders: Some(vec![WorkspaceFolder { uri: root_uri, name: Default::default(), }]), - client_info: Default::default(), - locale: Default::default(), + client_info: None, + locale: None, }; let response = self.request::(params).await?; diff --git a/crates/lsp2/src/lsp2.rs b/crates/lsp2/src/lsp2.rs index 356d029c587b57aaaf182bb090d82d42f14123d9..788c424373deca7c1490dd954fa005e0943d8a99 100644 --- a/crates/lsp2/src/lsp2.rs +++ b/crates/lsp2/src/lsp2.rs @@ -434,8 +434,8 @@ impl LanguageServer { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { - process_id: Default::default(), - root_path: Default::default(), + process_id: None, + root_path: None, root_uri: Some(root_uri.clone()), initialization_options: options, capabilities: ClientCapabilities { @@ -456,12 +456,15 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), + diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + refresh_support: None, + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - ..Default::default() + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -503,7 +506,7 @@ impl LanguageServer { }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - ..Default::default() + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -517,6 +520,20 @@ impl LanguageServer { }), dynamic_registration: Some(false), }), + publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { + related_information: Some(true), + ..Default::default() + }), + formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + on_type_formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + diagnostic: Some(DiagnosticClientCapabilities { + related_document_support: Some(true), + dynamic_registration: None, + }), ..Default::default() }), experimental: Some(json!({ @@ -526,15 +543,15 @@ impl LanguageServer { work_done_progress: Some(true), ..Default::default() }), - ..Default::default() + general: None, }, - trace: Default::default(), + trace: None, workspace_folders: Some(vec![WorkspaceFolder { uri: root_uri, name: Default::default(), }]), - client_info: Default::default(), - locale: Default::default(), + client_info: None, + locale: None, }; let response = self.request::(params).await?; diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 2621c58120af8ae3e711637af7a941dcc24af150..ecabdeb71817af4120cd3f9118c66adc85bd91b3 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -73,6 +73,7 @@ impl RealNodeRuntime { let npm_file = node_dir.join("bin/npm"); let result = Command::new(&node_binary) + .env_clear() .arg(npm_file) .arg("--version") .stdin(Stdio::null()) @@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime { } let mut command = Command::new(node_binary); + command.env_clear(); command.env("PATH", env_path); command.arg(npm_file).arg(subcommand); command.args(["--cache".into(), installation_path.join("cache")]); @@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime { &[ name, "--json", - "-fetch-retry-mintimeout", + "--fetch-retry-mintimeout", "2000", - "-fetch-retry-maxtimeout", + "--fetch-retry-maxtimeout", "5000", - "-fetch-timeout", + "--fetch-timeout", "5000", ], ) @@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime { let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); arguments.extend_from_slice(&[ - "-fetch-retry-mintimeout", + "--fetch-retry-mintimeout", "2000", - "-fetch-retry-maxtimeout", + "--fetch-retry-maxtimeout", "5000", - "-fetch-timeout", + "--fetch-timeout", "5000", ]); diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 70a8df21e138e85c6d9cfbc3528fba22667f1405..44056dabd16528b7a1aa28ab2e966b05f2ce0b43 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,8 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, - MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView, + MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, + WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Color, Divider, Label}; @@ -16,7 +17,6 @@ pub struct Picker { pub trait PickerDelegate: Sized + 'static { type ListItem: IntoElement; - fn match_count(&self) -> usize; fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); @@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem; + ) -> Option; } impl FocusableView for Picker { @@ -205,7 +205,6 @@ impl Render for Picker { .when(self.delegate.match_count() > 0, |el| { el.child( v_stack() - .p_1() .grow() .child( uniform_list( @@ -229,7 +228,7 @@ impl Render for Picker { ) }), ) - .child(picker.delegate.render_match( + .children(picker.delegate.render_match( ix, ix == selected_index, cx, @@ -239,7 +238,8 @@ impl Render for Picker { } }, ) - .track_scroll(self.scroll_handle.clone()), + .track_scroll(self.scroll_handle.clone()) + .p_1() ) .max_h_72() .overflow_hidden(), @@ -256,3 +256,22 @@ impl Render for Picker { }) } } + +pub fn simple_picker_match( + selected: bool, + cx: &mut WindowContext, + children: impl FnOnce(&mut WindowContext) -> AnyElement, +) -> AnyElement { + let colors = cx.theme().colors(); + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child((children)(cx)) + .into_any() +} diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index cb9d32d0b05115e1e9b3176c38ed6caf4b98ec49..0886d68747369e0c64e12d43d34ad787c31ddd61 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -13,12 +13,14 @@ use node_runtime::NodeRuntime; use serde::{Deserialize, Serialize}; use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; +#[derive(Clone)] pub enum Prettier { Real(RealPrettier), #[cfg(any(test, feature = "test-support"))] Test(TestPrettier), } +#[derive(Clone)] pub struct RealPrettier { default: bool, prettier_dir: PathBuf, @@ -26,11 +28,13 @@ pub struct RealPrettier { } #[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] pub struct TestPrettier { prettier_dir: PathBuf, default: bool, } +pub const FAIL_THRESHOLD: usize = 4; pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); const PRETTIER_PACKAGE_NAME: &str = "prettier"; diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js index 191431da0b89aaf8c98ae01b004e4fd1af981d43..bf62e538ddee2d29a5e41872f49f2258674cd82e 100644 --- a/crates/prettier/src/prettier_server.js +++ b/crates/prettier/src/prettier_server.js @@ -153,7 +153,10 @@ async function handleMessage(message, prettier) { const { method, id, params } = message; if (method === undefined) { throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); + } else if (method == "initialized") { + return; } + if (id === undefined) { throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); } diff --git a/crates/prettier2/src/prettier2.rs b/crates/prettier2/src/prettier2.rs index a01144ced330fa3bfcb75ac8330c6f526b25dfb1..61bcf9c9b3520993b09ed6110723fa5a7631b1ca 100644 --- a/crates/prettier2/src/prettier2.rs +++ b/crates/prettier2/src/prettier2.rs @@ -13,12 +13,14 @@ use std::{ }; use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; +#[derive(Clone)] pub enum Prettier { Real(RealPrettier), #[cfg(any(test, feature = "test-support"))] Test(TestPrettier), } +#[derive(Clone)] pub struct RealPrettier { default: bool, prettier_dir: PathBuf, @@ -26,11 +28,13 @@ pub struct RealPrettier { } #[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] pub struct TestPrettier { prettier_dir: PathBuf, default: bool, } +pub const FAIL_THRESHOLD: usize = 4; pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); const PRETTIER_PACKAGE_NAME: &str = "prettier"; diff --git a/crates/prettier2/src/prettier_server.js b/crates/prettier2/src/prettier_server.js index 191431da0b89aaf8c98ae01b004e4fd1af981d43..bf62e538ddee2d29a5e41872f49f2258674cd82e 100644 --- a/crates/prettier2/src/prettier_server.js +++ b/crates/prettier2/src/prettier_server.js @@ -153,7 +153,10 @@ async function handleMessage(message, prettier) { const { method, id, params } = message; if (method === undefined) { throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); + } else if (method == "initialized") { + return; } + if (id === undefined) { throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); } diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs new file mode 100644 index 0000000000000000000000000000000000000000..c438f294b689de194583a409d712f48569e0adaa --- /dev/null +++ b/crates/project/src/prettier_support.rs @@ -0,0 +1,758 @@ +use std::{ + ops::ControlFlow, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::Context; +use collections::HashSet; +use fs::Fs; +use futures::{ + future::{self, Shared}, + FutureExt, +}; +use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task}; +use language::{ + language_settings::{Formatter, LanguageSettings}, + Buffer, Language, LanguageServerName, LocalFile, +}; +use lsp::LanguageServerId; +use node_runtime::NodeRuntime; +use prettier::Prettier; +use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt}; + +use crate::{ + Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId, +}; + +pub fn prettier_plugins_for_language( + language: &Language, + language_settings: &LanguageSettings, +) -> Option> { + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return None, + }; + let mut prettier_plugins = None; + if language.prettier_parser_name().is_some() { + prettier_plugins + .get_or_insert_with(|| HashSet::default()) + .extend( + language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.prettier_plugins()), + ) + } + + prettier_plugins +} + +pub(super) async fn format_with_prettier( + project: &ModelHandle, + buffer: &ModelHandle, + cx: &mut AsyncAppContext, +) -> Option { + if let Some((prettier_path, prettier_task)) = project + .update(cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }) + .await + { + match prettier_task.await { + Ok(prettier) => { + let buffer_path = buffer.update(cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + match prettier.format(buffer, buffer_path, cx).await { + Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)), + Err(e) => { + log::error!( + "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}" + ); + } + } + } + Err(e) => project.update(cx, |project, _| { + let instance_to_update = match prettier_path { + Some(prettier_path) => { + log::error!( + "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}" + ); + project.prettier_instances.get_mut(&prettier_path) + } + None => { + log::error!("Default prettier instance failed to spawn: {e:#}"); + match &mut project.default_prettier.prettier { + PrettierInstallation::NotInstalled { .. } => None, + PrettierInstallation::Installed(instance) => Some(instance), + } + } + }; + + if let Some(instance) = instance_to_update { + instance.attempt += 1; + instance.prettier = None; + } + }), + } + } + + None +} + +pub struct DefaultPrettier { + prettier: PrettierInstallation, + installed_plugins: HashSet<&'static str>, +} + +pub enum PrettierInstallation { + NotInstalled { + attempts: usize, + installation_task: Option>>>>, + not_installed_plugins: HashSet<&'static str>, + }, + Installed(PrettierInstance), +} + +pub type PrettierTask = Shared, Arc>>>; + +#[derive(Clone)] +pub struct PrettierInstance { + attempt: usize, + prettier: Option, +} + +impl Default for DefaultPrettier { + fn default() -> Self { + Self { + prettier: PrettierInstallation::NotInstalled { + attempts: 0, + installation_task: None, + not_installed_plugins: HashSet::default(), + }, + installed_plugins: HashSet::default(), + } + } +} + +impl DefaultPrettier { + pub fn instance(&self) -> Option<&PrettierInstance> { + if let PrettierInstallation::Installed(instance) = &self.prettier { + Some(instance) + } else { + None + } + } + + pub fn prettier_task( + &mut self, + node: &Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, + ) -> Option>> { + match &mut self.prettier { + PrettierInstallation::NotInstalled { .. } => { + Some(start_default_prettier(Arc::clone(node), worktree_id, cx)) + } + PrettierInstallation::Installed(existing_instance) => { + existing_instance.prettier_task(node, None, worktree_id, cx) + } + } + } +} + +impl PrettierInstance { + pub fn prettier_task( + &mut self, + node: &Arc, + prettier_dir: Option<&Path>, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, + ) -> Option>> { + if self.attempt > prettier::FAIL_THRESHOLD { + match prettier_dir { + Some(prettier_dir) => log::warn!( + "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting" + ), + None => log::warn!("Default prettier exceeded launch threshold, not starting"), + } + return None; + } + Some(match &self.prettier { + Some(prettier_task) => Task::ready(Ok(prettier_task.clone())), + None => match prettier_dir { + Some(prettier_dir) => { + let new_task = start_prettier( + Arc::clone(node), + prettier_dir.to_path_buf(), + worktree_id, + cx, + ); + self.attempt += 1; + self.prettier = Some(new_task.clone()); + Task::ready(Ok(new_task)) + } + None => { + self.attempt += 1; + let node = Arc::clone(node); + cx.spawn(|project, mut cx| async move { + project + .update(&mut cx, |_, cx| { + start_default_prettier(node, worktree_id, cx) + }) + .await + }) + } + }, + }) + } +} + +fn start_default_prettier( + node: Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> Task> { + cx.spawn(|project, mut cx| async move { + loop { + let installation_task = project.update(&mut cx, |project, _| { + match &project.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_task, .. + } => ControlFlow::Continue(installation_task.clone()), + PrettierInstallation::Installed(default_prettier) => { + ControlFlow::Break(default_prettier.clone()) + } + } + }); + match installation_task { + ControlFlow::Continue(None) => { + anyhow::bail!("Default prettier is not installed and cannot be started") + } + ControlFlow::Continue(Some(installation_task)) => { + log::info!("Waiting for default prettier to install"); + if let Err(e) = installation_task.await { + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { + installation_task, + attempts, + .. + } = &mut project.default_prettier.prettier + { + *installation_task = None; + *attempts += 1; + } + }); + anyhow::bail!( + "Cannot start default prettier due to its installation failure: {e:#}" + ); + } + let new_default_prettier = project.update(&mut cx, |project, cx| { + let new_default_prettier = + start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); + project.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: Some(new_default_prettier.clone()), + }); + new_default_prettier + }); + return Ok(new_default_prettier); + } + ControlFlow::Break(instance) => match instance.prettier { + Some(instance) => return Ok(instance), + None => { + let new_default_prettier = project.update(&mut cx, |project, cx| { + let new_default_prettier = + start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); + project.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: instance.attempt + 1, + prettier: Some(new_default_prettier.clone()), + }); + new_default_prettier + }); + return Ok(new_default_prettier); + } + }, + } + } + }) +} + +fn start_prettier( + node: Arc, + prettier_dir: PathBuf, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> PrettierTask { + cx.spawn(|project, mut cx| async move { + log::info!("Starting prettier at path {prettier_dir:?}"); + let new_server_id = project.update(&mut cx, |project, _| { + project.languages.next_language_server_id() + }); + + let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) + .await + .context("default prettier spawn") + .map(Arc::new) + .map_err(Arc::new)?; + register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); + Ok(new_prettier) + }) + .shared() +} + +fn register_new_prettier( + project: &ModelHandle, + prettier: &Prettier, + worktree_id: Option, + new_server_id: LanguageServerId, + cx: &mut AsyncAppContext, +) { + let prettier_dir = prettier.prettier_dir(); + let is_default = prettier.is_default(); + if is_default { + log::info!("Started default prettier in {prettier_dir:?}"); + } else { + log::info!("Started prettier in {prettier_dir:?}"); + } + if let Some(prettier_server) = prettier.server() { + project.update(cx, |project, cx| { + let name = if is_default { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let worktree_path = worktree_id + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); + let name = match worktree_path { + Some(worktree_path) => { + if prettier_dir == worktree_path.as_ref() { + let name = prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + format!("prettier ({name})") + } else { + let dir_to_display = prettier_dir + .strip_prefix(worktree_path.as_ref()) + .ok() + .unwrap_or(prettier_dir); + format!("prettier ({})", dir_to_display.display()) + } + } + None => format!("prettier ({})", prettier_dir.display()), + }; + LanguageServerName(Arc::from(name)) + }; + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }); + } +} + +async fn install_prettier_packages( + plugins_to_install: HashSet<&'static str>, + node: Arc, +) -> anyhow::Result<()> { + let packages_to_versions = + future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( + |package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node + .npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }, + )) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions + .iter() + .map(|(package, version)| (package.as_str(), version.as_str())) + .collect::>(); + node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) + .await + .context("fetching formatter packages")?; + anyhow::Ok(()) +} + +async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> { + let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); + fs.save( + &prettier_wrapper_path, + &text::Rope::from(prettier::PRETTIER_SERVER_JS), + text::LineEnding::Unix, + ) + .await + .with_context(|| { + format!( + "writing {} file at {prettier_wrapper_path:?}", + prettier::PRETTIER_SERVER_FILE + ) + })?; + Ok(()) +} + +impl Project { + pub fn update_prettier_settings( + &self, + worktree: &ModelHandle, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext<'_, Project>, + ) { + let prettier_config_files = Prettier::CONFIG_FILE_NAMES + .iter() + .map(Path::new) + .collect::>(); + + let prettier_config_file_changed = changes + .iter() + .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) + .filter(|(path, _, _)| { + !path + .components() + .any(|component| component.as_os_str().to_string_lossy() == "node_modules") + }) + .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); + let current_worktree_id = worktree.read(cx).id(); + if let Some((config_path, _, _)) = prettier_config_file_changed { + log::info!( + "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" + ); + let prettiers_to_reload = + self.prettiers_per_worktree + .get(¤t_worktree_id) + .iter() + .flat_map(|prettier_paths| prettier_paths.iter()) + .flatten() + .filter_map(|prettier_path| { + Some(( + current_worktree_id, + Some(prettier_path.clone()), + self.prettier_instances.get(prettier_path)?.clone(), + )) + }) + .chain(self.default_prettier.instance().map(|default_prettier| { + (current_worktree_id, None, default_prettier.clone()) + })) + .collect::>(); + + cx.background() + .spawn(async move { + let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| { + async move { + if let Some(instance) = prettier_instance.prettier { + match instance.await { + Ok(prettier) => { + prettier.clear_cache().log_err().await; + }, + Err(e) => { + match prettier_path { + Some(prettier_path) => log::error!( + "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}" + ), + None => log::error!( + "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}" + ), + } + }, + } + } + } + })) + .await; + }) + .detach(); + } + } + + fn prettier_instance_for_buffer( + &mut self, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Task, PrettierTask)>> { + let buffer = buffer.read(cx); + let buffer_file = buffer.file(); + let Some(buffer_language) = buffer.language() else { + return Task::ready(None); + }; + if buffer_language.prettier_parser_name().is_none() { + return Task::ready(None); + } + + if self.is_local() { + let Some(node) = self.node.as_ref().map(Arc::clone) else { + return Task::ready(None); + }; + match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) + { + Some((worktree_id, buffer_path)) => { + let fs = Arc::clone(&self.fs); + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + return cx.spawn(|project, mut cx| async move { + match cx + .background() + .spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + &buffer_path, + ) + .await + }) + .await + { + Ok(ControlFlow::Break(())) => { + return None; + } + Ok(ControlFlow::Continue(None)) => { + let default_instance = project.update(&mut cx, |project, cx| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(None); + project.default_prettier.prettier_task( + &node, + Some(worktree_id), + cx, + ) + }); + Some((None, default_instance?.log_err().await?)) + } + Ok(ControlFlow::Continue(Some(prettier_dir))) => { + project.update(&mut cx, |project, _| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(Some(prettier_dir.clone())) + }); + if let Some(prettier_task) = + project.update(&mut cx, |project, cx| { + project.prettier_instances.get_mut(&prettier_dir).map( + |existing_instance| { + existing_instance.prettier_task( + &node, + Some(&prettier_dir), + Some(worktree_id), + cx, + ) + }, + ) + }) + { + log::debug!( + "Found already started prettier in {prettier_dir:?}" + ); + return Some(( + Some(prettier_dir), + prettier_task?.await.log_err()?, + )); + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let new_prettier_task = project.update(&mut cx, |project, cx| { + let new_prettier_task = start_prettier( + node, + prettier_dir.clone(), + Some(worktree_id), + cx, + ); + project.prettier_instances.insert( + prettier_dir.clone(), + PrettierInstance { + attempt: 0, + prettier: Some(new_prettier_task.clone()), + }, + ); + new_prettier_task + }); + Some((Some(prettier_dir), new_prettier_task)) + } + Err(e) => { + log::error!("Failed to determine prettier path for buffer: {e:#}"); + return None; + } + } + }); + } + None => { + let new_task = self.default_prettier.prettier_task(&node, None, cx); + return cx + .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) }); + } + } + } else { + return Task::ready(None); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn install_default_prettier( + &mut self, + _worktree: Option, + plugins: HashSet<&'static str>, + _cx: &mut ModelContext, + ) { + // suppress unused code warnings + let _ = install_prettier_packages; + let _ = save_prettier_server_file; + + self.default_prettier.installed_plugins.extend(plugins); + self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); + } + + #[cfg(not(any(test, feature = "test-support")))] + pub fn install_default_prettier( + &mut self, + worktree: Option, + mut new_plugins: HashSet<&'static str>, + cx: &mut ModelContext, + ) { + let Some(node) = self.node.as_ref().cloned() else { + return; + }; + log::info!("Initializing default prettier with plugins {new_plugins:?}"); + let fs = Arc::clone(&self.fs); + let locate_prettier_installation = match worktree.and_then(|worktree_id| { + self.worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path()) + }) { + Some(locate_from) => { + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + cx.background().spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + locate_from.as_ref(), + ) + .await + }) + } + None => Task::ready(Ok(ControlFlow::Continue(None))), + }; + new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); + let mut installation_attempt = 0; + let previous_installation_task = match &mut self.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_task, + attempts, + not_installed_plugins, + } => { + installation_attempt = *attempts; + if installation_attempt > prettier::FAIL_THRESHOLD { + *installation_task = None; + log::warn!( + "Default prettier installation had failed {installation_attempt} times, not attempting again", + ); + return; + } + new_plugins.extend(not_installed_plugins.iter()); + installation_task.clone() + } + PrettierInstallation::Installed { .. } => { + if new_plugins.is_empty() { + return; + } + None + } + }; + + let plugins_to_install = new_plugins.clone(); + let fs = Arc::clone(&self.fs); + let new_installation_task = cx + .spawn(|project, mut cx| async move { + match locate_prettier_installation + .await + .context("locate prettier installation") + .map_err(Arc::new)? + { + ControlFlow::Break(()) => return Ok(()), + ControlFlow::Continue(prettier_path) => { + if prettier_path.is_some() { + new_plugins.clear(); + } + let mut needs_install = false; + if let Some(previous_installation_task) = previous_installation_task { + if let Err(e) = previous_installation_task.await { + log::error!("Failed to install default prettier: {e:#}"); + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier { + *attempts += 1; + new_plugins.extend(not_installed_plugins.iter()); + installation_attempt = *attempts; + needs_install = true; + }; + }); + } + }; + if installation_attempt > prettier::FAIL_THRESHOLD { + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier { + *installation_task = None; + }; + }); + log::warn!( + "Default prettier installation had failed {installation_attempt} times, not attempting again", + ); + return Ok(()); + } + project.update(&mut cx, |project, _| { + new_plugins.retain(|plugin| { + !project.default_prettier.installed_plugins.contains(plugin) + }); + if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier { + not_installed_plugins.retain(|plugin| { + !project.default_prettier.installed_plugins.contains(plugin) + }); + not_installed_plugins.extend(new_plugins.iter()); + } + needs_install |= !new_plugins.is_empty(); + }); + if needs_install { + let installed_plugins = new_plugins.clone(); + cx.background() + .spawn(async move { + save_prettier_server_file(fs.as_ref()).await?; + install_prettier_packages(new_plugins, node).await + }) + .await + .context("prettier & plugins install") + .map_err(Arc::new)?; + log::info!("Initialized prettier with plugins: {installed_plugins:?}"); + project.update(&mut cx, |project, _| { + project.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); + project.default_prettier + .installed_plugins + .extend(installed_plugins); + }); + } + } + } + Ok(()) + }) + .shared(); + self.default_prettier.prettier = PrettierInstallation::NotInstalled { + attempts: installation_attempt, + installation_task: Some(new_installation_task), + not_installed_plugins: plugins_to_install, + }; + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cf3fa547f69338a229d53734172c0e60f74e643c..21d64fe91f8509496c7641225863a86dcd2945ce 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ mod ignore; mod lsp_command; +mod prettier_support; pub mod project_settings; pub mod search; pub mod terminals; @@ -20,7 +21,7 @@ use futures::{ mpsc::{self, UnboundedReceiver}, oneshot, }, - future::{self, try_join_all, Shared}, + future::{try_join_all, Shared}, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; @@ -31,9 +32,7 @@ use gpui::{ }; use itertools::Itertools; use language::{ - language_settings::{ - language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, - }, + language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -54,7 +53,7 @@ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::Mutex; use postage::watch; -use prettier::Prettier; +use prettier_support::{DefaultPrettier, PrettierInstance}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -72,7 +71,7 @@ use std::{ hash::Hash, mem, num::NonZeroU32, - ops::{ControlFlow, Range}, + ops::Range, path::{self, Component, Path, PathBuf}, process::Stdio, str, @@ -85,11 +84,8 @@ use std::{ use terminals::Terminals; use text::Anchor; use util::{ - debug_panic, defer, - http::HttpClient, - merge_json_value_into, - paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, - post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, http::HttpClient, merge_json_value_into, + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -168,16 +164,9 @@ pub struct Project { copilot_log_subscription: Option, current_lsp_settings: HashMap, LspSettings>, node: Option>, - default_prettier: Option, + default_prettier: DefaultPrettier, prettiers_per_worktree: HashMap>>, - prettier_instances: HashMap, Arc>>>>, -} - -struct DefaultPrettier { - instance: Option, Arc>>>>, - installation_process: Option>>>>, - #[cfg(not(any(test, feature = "test-support")))] - installed_plugins: HashSet<&'static str>, + prettier_instances: HashMap, } struct DelayedDebounced { @@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate { http_client: Arc, } +// Currently, formatting operations are represented differently depending on +// whether they come from a language server or an external command. +enum FormatOperation { + Lsp(Vec<(Range, String)>), + External(Diff), + Prettier(Diff), +} + impl FormatTrigger { fn from_proto(value: i32) -> FormatTrigger { match value { @@ -690,7 +687,7 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), node: Some(node), - default_prettier: None, + default_prettier: DefaultPrettier::default(), prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), } @@ -791,7 +788,7 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: settings::get::(cx).lsp.clone(), node: None, - default_prettier: None, + default_prettier: DefaultPrettier::default(), prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; @@ -928,8 +925,19 @@ impl Project { .detach(); } + let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language, settings) in language_formatters_to_check { - self.install_default_formatters(worktree, &language, &settings, cx); + if let Some(plugins) = + prettier_support::prettier_plugins_for_language(&language, &settings) + { + prettier_plugins_by_worktree + .entry(worktree) + .or_insert_with(|| HashSet::default()) + .extend(plugins); + } + } + for (worktree, prettier_plugins) in prettier_plugins_by_worktree { + self.install_default_prettier(worktree, prettier_plugins, cx); } // Start all the newly-enabled language servers. @@ -2633,8 +2641,9 @@ impl Project { }); for (adapter, server) in servers { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(server.root_path(), cx)) + .await; server .notify::( lsp::DidChangeConfigurationParams { @@ -2685,8 +2694,11 @@ impl Project { let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); - - self.install_default_formatters(worktree, &new_language, &settings, cx); + if let Some(prettier_plugins) = + prettier_support::prettier_plugins_for_language(&new_language, &settings) + { + self.install_default_prettier(worktree, prettier_plugins, cx); + }; if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { @@ -2742,7 +2754,7 @@ impl Project { stderr_capture.clone(), language.clone(), adapter.clone(), - worktree_path, + Arc::clone(&worktree_path), ProjectLspAdapterDelegate::new(self, cx), cx, ) { @@ -2765,6 +2777,7 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let result = Self::setup_and_insert_language_server( this, + &worktree_path, override_options, pending_server, adapter.clone(), @@ -2880,6 +2893,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModelHandle, + worktree_path: &Path, override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -2892,6 +2906,7 @@ impl Project { this, override_initialization_options, pending_server, + worktree_path, adapter.clone(), server_id, cx, @@ -2921,11 +2936,14 @@ impl Project { this: WeakModelHandle, override_options: Option, pending_server: PendingLanguageServer, + worktree_path: &Path, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(worktree_path, cx)) + .await; let language_server = pending_server.task.await?; language_server @@ -2953,11 +2971,14 @@ impl Project { language_server .on_request::({ let adapter = adapter.clone(); + let worktree_path = worktree_path.to_path_buf(); move |params, mut cx| { let adapter = adapter.clone(); + let worktree_path = worktree_path.clone(); async move { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(&worktree_path, cx)) + .await; Ok(params .items .into_iter() @@ -4073,8 +4094,6 @@ impl Project { let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; let ensure_final_newline = settings.ensure_final_newline_on_save; - let format_on_save = settings.format_on_save.clone(); - let formatter = settings.formatter.clone(); let tab_size = settings.tab_size; // First, format buffer's whitespace according to the settings. @@ -4099,18 +4118,10 @@ impl Project { buffer.end_transaction(cx) }); - // Currently, formatting operations are represented differently depending on - // whether they come from a language server or an external command. - enum FormatOperation { - Lsp(Vec<(Range, String)>), - External(Diff), - Prettier(Diff), - } - // Apply language-specific formatting using either a language server // or external command. let mut format_operation = None; - match (formatter, format_on_save) { + match (&settings.formatter, &settings.format_on_save) { (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) @@ -4155,46 +4166,11 @@ impl Project { } } (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { - if let Some((prettier_path, prettier_task)) = project - .update(&mut cx, |project, cx| { - project.prettier_instance_for_buffer(buffer, cx) - }).await { - match prettier_task.await - { - Ok(prettier) => { - let buffer_path = buffer.update(&mut cx, |buffer, cx| { - File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) - }); - format_operation = Some(FormatOperation::Prettier( - prettier - .format(buffer, buffer_path, &cx) - .await - .context("formatting via prettier")?, - )); - } - Err(e) => { - project.update(&mut cx, |project, _| { - match &prettier_path { - Some(prettier_path) => { - project.prettier_instances.remove(prettier_path); - }, - None => { - if let Some(default_prettier) = project.default_prettier.as_mut() { - default_prettier.instance = None; - } - }, - } - }); - match &prettier_path { - Some(prettier_path) => { - log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); - }, - None => { - log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); - }, - } - } - } + if let Some(new_operation) = + prettier_support::format_with_prettier(&project, buffer, &mut cx) + .await + { + format_operation = Some(new_operation); } else if let Some((language_server, buffer_abs_path)) = language_server.as_ref().zip(buffer_abs_path.as_ref()) { @@ -4212,48 +4188,13 @@ impl Project { )); } } - (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { - if let Some((prettier_path, prettier_task)) = project - .update(&mut cx, |project, cx| { - project.prettier_instance_for_buffer(buffer, cx) - }).await { - match prettier_task.await - { - Ok(prettier) => { - let buffer_path = buffer.update(&mut cx, |buffer, cx| { - File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) - }); - format_operation = Some(FormatOperation::Prettier( - prettier - .format(buffer, buffer_path, &cx) - .await - .context("formatting via prettier")?, - )); - } - Err(e) => { - project.update(&mut cx, |project, _| { - match &prettier_path { - Some(prettier_path) => { - project.prettier_instances.remove(prettier_path); - }, - None => { - if let Some(default_prettier) = project.default_prettier.as_mut() { - default_prettier.instance = None; - } - }, - } - }); - match &prettier_path { - Some(prettier_path) => { - log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); - }, - None => { - log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); - }, - } - } - } - } + (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(new_operation) = + prettier_support::format_with_prettier(&project, buffer, &mut cx) + .await + { + format_operation = Some(new_operation); + } } }; @@ -6566,85 +6507,6 @@ impl Project { .detach(); } - fn update_prettier_settings( - &self, - worktree: &ModelHandle, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut ModelContext<'_, Project>, - ) { - let prettier_config_files = Prettier::CONFIG_FILE_NAMES - .iter() - .map(Path::new) - .collect::>(); - - let prettier_config_file_changed = changes - .iter() - .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) - .filter(|(path, _, _)| { - !path - .components() - .any(|component| component.as_os_str().to_string_lossy() == "node_modules") - }) - .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); - let current_worktree_id = worktree.read(cx).id(); - if let Some((config_path, _, _)) = prettier_config_file_changed { - log::info!( - "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" - ); - let prettiers_to_reload = self - .prettiers_per_worktree - .get(¤t_worktree_id) - .iter() - .flat_map(|prettier_paths| prettier_paths.iter()) - .flatten() - .filter_map(|prettier_path| { - Some(( - current_worktree_id, - Some(prettier_path.clone()), - self.prettier_instances.get(prettier_path)?.clone(), - )) - }) - .chain(self.default_prettier.iter().filter_map(|default_prettier| { - Some(( - current_worktree_id, - None, - default_prettier.instance.clone()?, - )) - })) - .collect::>(); - - cx.background() - .spawn(async move { - for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { - async move { - prettier_task.await? - .clear_cache() - .await - .with_context(|| { - match prettier_path { - Some(prettier_path) => format!( - "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" - ), - None => format!( - "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update" - ), - } - - }) - .map_err(Arc::new) - } - })) - .await - { - if let Err(e) = task_result { - log::error!("Failed to clear cache for prettier: {e:#}"); - } - } - }) - .detach(); - } - } - pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -6671,9 +6533,15 @@ impl Project { }) } - pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { + pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary { let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in self.diagnostic_summaries(cx) { + for (_, _, path_summary) in + self.diagnostic_summaries(include_ignored, cx) + .filter(|(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) + { summary.error_count += path_summary.error_count; summary.warning_count += path_summary.warning_count; } @@ -6682,6 +6550,7 @@ impl Project { pub fn diagnostic_summaries<'a>( &'a self, + include_ignored: bool, cx: &'a AppContext, ) -> impl Iterator + 'a { self.visible_worktrees(cx).flat_map(move |worktree| { @@ -6692,6 +6561,10 @@ impl Project { .map(move |(path, server_id, summary)| { (ProjectPath { worktree_id, path }, server_id, summary) }) + .filter(move |(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) }) } @@ -8536,446 +8409,6 @@ impl Project { Vec::new() } } - - fn prettier_instance_for_buffer( - &mut self, - buffer: &ModelHandle, - cx: &mut ModelContext, - ) -> Task< - Option<( - Option, - Shared, Arc>>>, - )>, - > { - let buffer = buffer.read(cx); - let buffer_file = buffer.file(); - let Some(buffer_language) = buffer.language() else { - return Task::ready(None); - }; - if buffer_language.prettier_parser_name().is_none() { - return Task::ready(None); - } - - if self.is_local() { - let Some(node) = self.node.as_ref().map(Arc::clone) else { - return Task::ready(None); - }; - match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) - { - Some((worktree_id, buffer_path)) => { - let fs = Arc::clone(&self.fs); - let installed_prettiers = self.prettier_instances.keys().cloned().collect(); - return cx.spawn(|project, mut cx| async move { - match cx - .background() - .spawn(async move { - Prettier::locate_prettier_installation( - fs.as_ref(), - &installed_prettiers, - &buffer_path, - ) - .await - }) - .await - { - Ok(ControlFlow::Break(())) => { - return None; - } - Ok(ControlFlow::Continue(None)) => { - let started_default_prettier = - project.update(&mut cx, |project, _| { - project - .prettiers_per_worktree - .entry(worktree_id) - .or_default() - .insert(None); - project.default_prettier.as_ref().and_then( - |default_prettier| default_prettier.instance.clone(), - ) - }); - match started_default_prettier { - Some(old_task) => return Some((None, old_task)), - None => { - let new_default_prettier = project - .update(&mut cx, |_, cx| { - start_default_prettier(node, Some(worktree_id), cx) - }) - .await; - return Some((None, new_default_prettier)); - } - } - } - Ok(ControlFlow::Continue(Some(prettier_dir))) => { - project.update(&mut cx, |project, _| { - project - .prettiers_per_worktree - .entry(worktree_id) - .or_default() - .insert(Some(prettier_dir.clone())) - }); - if let Some(existing_prettier) = - project.update(&mut cx, |project, _| { - project.prettier_instances.get(&prettier_dir).cloned() - }) - { - log::debug!( - "Found already started prettier in {prettier_dir:?}" - ); - return Some((Some(prettier_dir), existing_prettier)); - } - - log::info!("Found prettier in {prettier_dir:?}, starting."); - let new_prettier_task = project.update(&mut cx, |project, cx| { - let new_prettier_task = start_prettier( - node, - prettier_dir.clone(), - Some(worktree_id), - cx, - ); - project - .prettier_instances - .insert(prettier_dir.clone(), new_prettier_task.clone()); - new_prettier_task - }); - Some((Some(prettier_dir), new_prettier_task)) - } - Err(e) => { - return Some(( - None, - Task::ready(Err(Arc::new( - e.context("determining prettier path"), - ))) - .shared(), - )); - } - } - }); - } - None => { - let started_default_prettier = self - .default_prettier - .as_ref() - .and_then(|default_prettier| default_prettier.instance.clone()); - match started_default_prettier { - Some(old_task) => return Task::ready(Some((None, old_task))), - None => { - let new_task = start_default_prettier(node, None, cx); - return cx.spawn(|_, _| async move { Some((None, new_task.await)) }); - } - } - } - } - } else if self.remote_id().is_some() { - return Task::ready(None); - } else { - Task::ready(Some(( - None, - Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), - ))) - } - } - - #[cfg(any(test, feature = "test-support"))] - fn install_default_formatters( - &mut self, - _worktree: Option, - _new_language: &Language, - _language_settings: &LanguageSettings, - _cx: &mut ModelContext, - ) { - } - - #[cfg(not(any(test, feature = "test-support")))] - fn install_default_formatters( - &mut self, - worktree: Option, - new_language: &Language, - language_settings: &LanguageSettings, - cx: &mut ModelContext, - ) { - match &language_settings.formatter { - Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return, - }; - let Some(node) = self.node.as_ref().cloned() else { - return; - }; - - let mut prettier_plugins = None; - if new_language.prettier_parser_name().is_some() { - prettier_plugins - .get_or_insert_with(|| HashSet::<&'static str>::default()) - .extend( - new_language - .lsp_adapters() - .iter() - .flat_map(|adapter| adapter.prettier_plugins()), - ) - } - let Some(prettier_plugins) = prettier_plugins else { - return; - }; - - let fs = Arc::clone(&self.fs); - let locate_prettier_installation = match worktree.and_then(|worktree_id| { - self.worktree_for_id(worktree_id, cx) - .map(|worktree| worktree.read(cx).abs_path()) - }) { - Some(locate_from) => { - let installed_prettiers = self.prettier_instances.keys().cloned().collect(); - cx.background().spawn(async move { - Prettier::locate_prettier_installation( - fs.as_ref(), - &installed_prettiers, - locate_from.as_ref(), - ) - .await - }) - } - None => Task::ready(Ok(ControlFlow::Break(()))), - }; - let mut plugins_to_install = prettier_plugins; - let previous_installation_process = - if let Some(default_prettier) = &mut self.default_prettier { - plugins_to_install - .retain(|plugin| !default_prettier.installed_plugins.contains(plugin)); - if plugins_to_install.is_empty() { - return; - } - default_prettier.installation_process.clone() - } else { - None - }; - let fs = Arc::clone(&self.fs); - let default_prettier = self - .default_prettier - .get_or_insert_with(|| DefaultPrettier { - instance: None, - installation_process: None, - installed_plugins: HashSet::default(), - }); - default_prettier.installation_process = Some( - cx.spawn(|this, mut cx| async move { - match locate_prettier_installation - .await - .context("locate prettier installation") - .map_err(Arc::new)? - { - ControlFlow::Break(()) => return Ok(()), - ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()), - ControlFlow::Continue(None) => { - let mut needs_install = match previous_installation_process { - Some(previous_installation_process) => { - previous_installation_process.await.is_err() - } - None => true, - }; - this.update(&mut cx, |this, _| { - if let Some(default_prettier) = &mut this.default_prettier { - plugins_to_install.retain(|plugin| { - !default_prettier.installed_plugins.contains(plugin) - }); - needs_install |= !plugins_to_install.is_empty(); - } - }); - if needs_install { - let installed_plugins = plugins_to_install.clone(); - cx.background() - .spawn(async move { - install_default_prettier(plugins_to_install, node, fs).await - }) - .await - .context("prettier & plugins install") - .map_err(Arc::new)?; - this.update(&mut cx, |this, _| { - let default_prettier = - this.default_prettier - .get_or_insert_with(|| DefaultPrettier { - instance: None, - installation_process: Some( - Task::ready(Ok(())).shared(), - ), - installed_plugins: HashSet::default(), - }); - default_prettier.instance = None; - default_prettier.installed_plugins.extend(installed_plugins); - }); - } - } - } - Ok(()) - }) - .shared(), - ); - } -} - -fn start_default_prettier( - node: Arc, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, -) -> Task, Arc>>>> { - cx.spawn(|project, mut cx| async move { - loop { - let default_prettier_installing = project.update(&mut cx, |project, _| { - project - .default_prettier - .as_ref() - .and_then(|default_prettier| default_prettier.installation_process.clone()) - }); - match default_prettier_installing { - Some(installation_task) => { - if installation_task.await.is_ok() { - break; - } - } - None => break, - } - } - - project.update(&mut cx, |project, cx| { - match project - .default_prettier - .as_mut() - .and_then(|default_prettier| default_prettier.instance.as_mut()) - { - Some(default_prettier) => default_prettier.clone(), - None => { - let new_default_prettier = - start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); - project - .default_prettier - .get_or_insert_with(|| DefaultPrettier { - instance: None, - installation_process: None, - #[cfg(not(any(test, feature = "test-support")))] - installed_plugins: HashSet::default(), - }) - .instance = Some(new_default_prettier.clone()); - new_default_prettier - } - } - }) - }) -} - -fn start_prettier( - node: Arc, - prettier_dir: PathBuf, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, -) -> Shared, Arc>>> { - cx.spawn(|project, mut cx| async move { - let new_server_id = project.update(&mut cx, |project, _| { - project.languages.next_language_server_id() - }); - let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) - .await - .context("default prettier spawn") - .map(Arc::new) - .map_err(Arc::new)?; - register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); - Ok(new_prettier) - }) - .shared() -} - -fn register_new_prettier( - project: &ModelHandle, - prettier: &Prettier, - worktree_id: Option, - new_server_id: LanguageServerId, - cx: &mut AsyncAppContext, -) { - let prettier_dir = prettier.prettier_dir(); - let is_default = prettier.is_default(); - if is_default { - log::info!("Started default prettier in {prettier_dir:?}"); - } else { - log::info!("Started prettier in {prettier_dir:?}"); - } - if let Some(prettier_server) = prettier.server() { - project.update(cx, |project, cx| { - let name = if is_default { - LanguageServerName(Arc::from("prettier (default)")) - } else { - let worktree_path = worktree_id - .and_then(|id| project.worktree_for_id(id, cx)) - .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); - let name = match worktree_path { - Some(worktree_path) => { - if prettier_dir == worktree_path.as_ref() { - let name = prettier_dir - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or_default(); - format!("prettier ({name})") - } else { - let dir_to_display = prettier_dir - .strip_prefix(worktree_path.as_ref()) - .ok() - .unwrap_or(prettier_dir); - format!("prettier ({})", dir_to_display.display()) - } - } - None => format!("prettier ({})", prettier_dir.display()), - }; - LanguageServerName(Arc::from(name)) - }; - project - .supplementary_language_servers - .insert(new_server_id, (name, Arc::clone(prettier_server))); - cx.emit(Event::LanguageServerAdded(new_server_id)); - }); - } -} - -#[cfg(not(any(test, feature = "test-support")))] -async fn install_default_prettier( - plugins_to_install: HashSet<&'static str>, - node: Arc, - fs: Arc, -) -> anyhow::Result<()> { - let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); - // method creates parent directory if it doesn't exist - fs.save( - &prettier_wrapper_path, - &text::Rope::from(prettier::PRETTIER_SERVER_JS), - text::LineEnding::Unix, - ) - .await - .with_context(|| { - format!( - "writing {} file at {prettier_wrapper_path:?}", - prettier::PRETTIER_SERVER_FILE - ) - })?; - - let packages_to_versions = - future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( - |package_name| async { - let returned_package_name = package_name.to_string(); - let latest_version = node - .npm_package_latest_version(package_name) - .await - .with_context(|| { - format!("fetching latest npm version for package {returned_package_name}") - })?; - anyhow::Ok((returned_package_name, latest_version)) - }, - )) - .await - .context("fetching latest npm versions")?; - - log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); - let borrowed_packages = packages_to_versions - .iter() - .map(|(package, version)| (package.as_str(), version.as_str())) - .collect::>(); - node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) - .await - .context("fetching formatter packages")?; - anyhow::Ok(()) } fn subscribe_for_copilot_events( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 264c1ff7b54fa52dbffd87545603736704a5f932..5d061b868fb37dd730d09ea184fd3f7a91d447be 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -806,7 +806,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { +async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -814,7 +814,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { "/root", json!({ "dir": { + ".git": { + "HEAD": "ref: refs/heads/main", + }, + ".gitignore": "b.rs", "a.rs": "let a = 1;", + "b.rs": "let b = 2;", }, "other.rs": "let b = c;" }), @@ -822,6 +827,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root/dir", true, cx) + }) + .await + .unwrap(); + let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id()); let (worktree, _) = project .update(cx, |project, cx| { @@ -829,12 +841,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); + let other_worktree_id = worktree.read_with(cx, |tree, _| tree.id()); + let server_id = LanguageServerId(0); project.update(cx, |project, cx| { project .update_diagnostics( - LanguageServerId(0), + server_id, + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/root/dir/b.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "unused variable 'b'".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + project + .update_diagnostics( + server_id, lsp::PublishDiagnosticsParams { uri: Url::from_file_path("/root/other.rs").unwrap(), version: None, @@ -851,11 +881,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .unwrap(); }); - let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + let main_ignored_buffer = project + .update(cx, |project, cx| { + project.open_buffer((main_worktree_id, "b.rs"), cx) + }) .await .unwrap(); - buffer.read_with(cx, |buffer, _| { + main_ignored_buffer.read_with(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let ", None), + ("b", Some(DiagnosticSeverity::ERROR)), + (" = 2;", None), + ], + "Gigitnored buffers should still get in-buffer diagnostics", + ); + }); + let other_buffer = project + .update(cx, |project, cx| { + project.open_buffer((other_worktree_id, ""), cx) + }) + .await + .unwrap(); + other_buffer.read_with(cx, |buffer, _| { let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); assert_eq!( chunks @@ -866,13 +919,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ("let b = ", None), ("c", Some(DiagnosticSeverity::ERROR)), (";", None), - ] + ], + "Buffers from hidden projects should still get in-buffer diagnostics" ); }); project.read_with(cx, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).next(), None); - assert_eq!(project.diagnostic_summary(cx).error_count, 0); + assert_eq!(project.diagnostic_summaries(false, cx).next(), None); + assert_eq!( + project.diagnostic_summaries(true, cx).collect::>(), + vec![( + ProjectPath { + worktree_id: main_worktree_id, + path: Arc::from(Path::new("b.rs")), + }, + server_id, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + } + )] + ); + assert_eq!(project.diagnostic_summary(false, cx).error_count, 0); + assert_eq!(project.diagnostic_summary(true, cx).error_count, 1); }); } @@ -1145,7 +1214,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.read_with(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 1, warning_count: 0, @@ -1171,7 +1240,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.read_with(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 0, warning_count: 0, @@ -1763,7 +1832,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC .unwrap(); assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 2, warning_count: 0, diff --git a/crates/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs index 94c277db1e0277c56493837d45e37a9c0b38e4c0..a2de52b21ae277db5d15863e2adc283bd08c7b78 100644 --- a/crates/project2/src/lsp_command.rs +++ b/crates/project2/src/lsp_command.rs @@ -717,8 +717,9 @@ async fn location_links_from_lsp( })? .await?; - buffer.update(&mut cx, |origin_buffer, cx| { + cx.update(|cx| { let origin_location = origin_range.map(|origin_range| { + let origin_buffer = buffer.read(cx); let origin_start = origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); let origin_end = diff --git a/crates/project2/src/prettier_support.rs b/crates/project2/src/prettier_support.rs new file mode 100644 index 0000000000000000000000000000000000000000..c176c79a91f2e76ae84c6fcb25d95fd9b8568106 --- /dev/null +++ b/crates/project2/src/prettier_support.rs @@ -0,0 +1,772 @@ +use std::{ + ops::ControlFlow, + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::Context; +use collections::HashSet; +use fs::Fs; +use futures::{ + future::{self, Shared}, + FutureExt, +}; +use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel}; +use language::{ + language_settings::{Formatter, LanguageSettings}, + Buffer, Language, LanguageServerName, LocalFile, +}; +use lsp::LanguageServerId; +use node_runtime::NodeRuntime; +use prettier::Prettier; +use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt}; + +use crate::{ + Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId, +}; + +pub fn prettier_plugins_for_language( + language: &Language, + language_settings: &LanguageSettings, +) -> Option> { + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return None, + }; + let mut prettier_plugins = None; + if language.prettier_parser_name().is_some() { + prettier_plugins + .get_or_insert_with(|| HashSet::default()) + .extend( + language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.prettier_plugins()), + ) + } + + prettier_plugins +} + +pub(super) async fn format_with_prettier( + project: &WeakModel, + buffer: &Model, + cx: &mut AsyncAppContext, +) -> Option { + if let Some((prettier_path, prettier_task)) = project + .update(cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }) + .ok()? + .await + { + match prettier_task.await { + Ok(prettier) => { + let buffer_path = buffer + .update(cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }) + .ok()?; + match prettier.format(buffer, buffer_path, cx).await { + Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)), + Err(e) => { + log::error!( + "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}" + ); + } + } + } + Err(e) => project + .update(cx, |project, _| { + let instance_to_update = match prettier_path { + Some(prettier_path) => { + log::error!( + "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}" + ); + project.prettier_instances.get_mut(&prettier_path) + } + None => { + log::error!("Default prettier instance failed to spawn: {e:#}"); + match &mut project.default_prettier.prettier { + PrettierInstallation::NotInstalled { .. } => None, + PrettierInstallation::Installed(instance) => Some(instance), + } + } + }; + + if let Some(instance) = instance_to_update { + instance.attempt += 1; + instance.prettier = None; + } + }) + .ok()?, + } + } + + None +} + +pub struct DefaultPrettier { + prettier: PrettierInstallation, + installed_plugins: HashSet<&'static str>, +} + +pub enum PrettierInstallation { + NotInstalled { + attempts: usize, + installation_task: Option>>>>, + not_installed_plugins: HashSet<&'static str>, + }, + Installed(PrettierInstance), +} + +pub type PrettierTask = Shared, Arc>>>; + +#[derive(Clone)] +pub struct PrettierInstance { + attempt: usize, + prettier: Option, +} + +impl Default for DefaultPrettier { + fn default() -> Self { + Self { + prettier: PrettierInstallation::NotInstalled { + attempts: 0, + installation_task: None, + not_installed_plugins: HashSet::default(), + }, + installed_plugins: HashSet::default(), + } + } +} + +impl DefaultPrettier { + pub fn instance(&self) -> Option<&PrettierInstance> { + if let PrettierInstallation::Installed(instance) = &self.prettier { + Some(instance) + } else { + None + } + } + + pub fn prettier_task( + &mut self, + node: &Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, + ) -> Option>> { + match &mut self.prettier { + PrettierInstallation::NotInstalled { .. } => { + Some(start_default_prettier(Arc::clone(node), worktree_id, cx)) + } + PrettierInstallation::Installed(existing_instance) => { + existing_instance.prettier_task(node, None, worktree_id, cx) + } + } + } +} + +impl PrettierInstance { + pub fn prettier_task( + &mut self, + node: &Arc, + prettier_dir: Option<&Path>, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, + ) -> Option>> { + if self.attempt > prettier::FAIL_THRESHOLD { + match prettier_dir { + Some(prettier_dir) => log::warn!( + "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting" + ), + None => log::warn!("Default prettier exceeded launch threshold, not starting"), + } + return None; + } + Some(match &self.prettier { + Some(prettier_task) => Task::ready(Ok(prettier_task.clone())), + None => match prettier_dir { + Some(prettier_dir) => { + let new_task = start_prettier( + Arc::clone(node), + prettier_dir.to_path_buf(), + worktree_id, + cx, + ); + self.attempt += 1; + self.prettier = Some(new_task.clone()); + Task::ready(Ok(new_task)) + } + None => { + self.attempt += 1; + let node = Arc::clone(node); + cx.spawn(|project, mut cx| async move { + project + .update(&mut cx, |_, cx| { + start_default_prettier(node, worktree_id, cx) + })? + .await + }) + } + }, + }) + } +} + +fn start_default_prettier( + node: Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> Task> { + cx.spawn(|project, mut cx| async move { + loop { + let installation_task = project.update(&mut cx, |project, _| { + match &project.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_task, .. + } => ControlFlow::Continue(installation_task.clone()), + PrettierInstallation::Installed(default_prettier) => { + ControlFlow::Break(default_prettier.clone()) + } + } + })?; + match installation_task { + ControlFlow::Continue(None) => { + anyhow::bail!("Default prettier is not installed and cannot be started") + } + ControlFlow::Continue(Some(installation_task)) => { + log::info!("Waiting for default prettier to install"); + if let Err(e) = installation_task.await { + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { + installation_task, + attempts, + .. + } = &mut project.default_prettier.prettier + { + *installation_task = None; + *attempts += 1; + } + })?; + anyhow::bail!( + "Cannot start default prettier due to its installation failure: {e:#}" + ); + } + let new_default_prettier = project.update(&mut cx, |project, cx| { + let new_default_prettier = + start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); + project.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: Some(new_default_prettier.clone()), + }); + new_default_prettier + })?; + return Ok(new_default_prettier); + } + ControlFlow::Break(instance) => match instance.prettier { + Some(instance) => return Ok(instance), + None => { + let new_default_prettier = project.update(&mut cx, |project, cx| { + let new_default_prettier = + start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); + project.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: instance.attempt + 1, + prettier: Some(new_default_prettier.clone()), + }); + new_default_prettier + })?; + return Ok(new_default_prettier); + } + }, + } + } + }) +} + +fn start_prettier( + node: Arc, + prettier_dir: PathBuf, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, +) -> PrettierTask { + cx.spawn(|project, mut cx| async move { + log::info!("Starting prettier at path {prettier_dir:?}"); + let new_server_id = project.update(&mut cx, |project, _| { + project.languages.next_language_server_id() + })?; + + let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) + .await + .context("default prettier spawn") + .map(Arc::new) + .map_err(Arc::new)?; + register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); + Ok(new_prettier) + }) + .shared() +} + +fn register_new_prettier( + project: &WeakModel, + prettier: &Prettier, + worktree_id: Option, + new_server_id: LanguageServerId, + cx: &mut AsyncAppContext, +) { + let prettier_dir = prettier.prettier_dir(); + let is_default = prettier.is_default(); + if is_default { + log::info!("Started default prettier in {prettier_dir:?}"); + } else { + log::info!("Started prettier in {prettier_dir:?}"); + } + if let Some(prettier_server) = prettier.server() { + project + .update(cx, |project, cx| { + let name = if is_default { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let worktree_path = worktree_id + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); + let name = match worktree_path { + Some(worktree_path) => { + if prettier_dir == worktree_path.as_ref() { + let name = prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + format!("prettier ({name})") + } else { + let dir_to_display = prettier_dir + .strip_prefix(worktree_path.as_ref()) + .ok() + .unwrap_or(prettier_dir); + format!("prettier ({})", dir_to_display.display()) + } + } + None => format!("prettier ({})", prettier_dir.display()), + }; + LanguageServerName(Arc::from(name)) + }; + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }) + .ok(); + } +} + +async fn install_prettier_packages( + plugins_to_install: HashSet<&'static str>, + node: Arc, +) -> anyhow::Result<()> { + let packages_to_versions = + future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( + |package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node + .npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }, + )) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions + .iter() + .map(|(package, version)| (package.as_str(), version.as_str())) + .collect::>(); + node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) + .await + .context("fetching formatter packages")?; + anyhow::Ok(()) +} + +async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> { + let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); + fs.save( + &prettier_wrapper_path, + &text::Rope::from(prettier::PRETTIER_SERVER_JS), + text::LineEnding::Unix, + ) + .await + .with_context(|| { + format!( + "writing {} file at {prettier_wrapper_path:?}", + prettier::PRETTIER_SERVER_FILE + ) + })?; + Ok(()) +} + +impl Project { + pub fn update_prettier_settings( + &self, + worktree: &Model, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext<'_, Project>, + ) { + let prettier_config_files = Prettier::CONFIG_FILE_NAMES + .iter() + .map(Path::new) + .collect::>(); + + let prettier_config_file_changed = changes + .iter() + .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) + .filter(|(path, _, _)| { + !path + .components() + .any(|component| component.as_os_str().to_string_lossy() == "node_modules") + }) + .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); + let current_worktree_id = worktree.read(cx).id(); + if let Some((config_path, _, _)) = prettier_config_file_changed { + log::info!( + "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" + ); + let prettiers_to_reload = + self.prettiers_per_worktree + .get(¤t_worktree_id) + .iter() + .flat_map(|prettier_paths| prettier_paths.iter()) + .flatten() + .filter_map(|prettier_path| { + Some(( + current_worktree_id, + Some(prettier_path.clone()), + self.prettier_instances.get(prettier_path)?.clone(), + )) + }) + .chain(self.default_prettier.instance().map(|default_prettier| { + (current_worktree_id, None, default_prettier.clone()) + })) + .collect::>(); + + cx.background_executor() + .spawn(async move { + let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| { + async move { + if let Some(instance) = prettier_instance.prettier { + match instance.await { + Ok(prettier) => { + prettier.clear_cache().log_err().await; + }, + Err(e) => { + match prettier_path { + Some(prettier_path) => log::error!( + "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}" + ), + None => log::error!( + "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}" + ), + } + }, + } + } + } + })) + .await; + }) + .detach(); + } + } + + fn prettier_instance_for_buffer( + &mut self, + buffer: &Model, + cx: &mut ModelContext, + ) -> Task, PrettierTask)>> { + let buffer = buffer.read(cx); + let buffer_file = buffer.file(); + let Some(buffer_language) = buffer.language() else { + return Task::ready(None); + }; + if buffer_language.prettier_parser_name().is_none() { + return Task::ready(None); + } + + if self.is_local() { + let Some(node) = self.node.as_ref().map(Arc::clone) else { + return Task::ready(None); + }; + match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) + { + Some((worktree_id, buffer_path)) => { + let fs = Arc::clone(&self.fs); + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + return cx.spawn(|project, mut cx| async move { + match cx + .background_executor() + .spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + &buffer_path, + ) + .await + }) + .await + { + Ok(ControlFlow::Break(())) => { + return None; + } + Ok(ControlFlow::Continue(None)) => { + let default_instance = project + .update(&mut cx, |project, cx| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(None); + project.default_prettier.prettier_task( + &node, + Some(worktree_id), + cx, + ) + }) + .ok()?; + Some((None, default_instance?.log_err().await?)) + } + Ok(ControlFlow::Continue(Some(prettier_dir))) => { + project + .update(&mut cx, |project, _| { + project + .prettiers_per_worktree + .entry(worktree_id) + .or_default() + .insert(Some(prettier_dir.clone())) + }) + .ok()?; + if let Some(prettier_task) = project + .update(&mut cx, |project, cx| { + project.prettier_instances.get_mut(&prettier_dir).map( + |existing_instance| { + existing_instance.prettier_task( + &node, + Some(&prettier_dir), + Some(worktree_id), + cx, + ) + }, + ) + }) + .ok()? + { + log::debug!( + "Found already started prettier in {prettier_dir:?}" + ); + return Some(( + Some(prettier_dir), + prettier_task?.await.log_err()?, + )); + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let new_prettier_task = project + .update(&mut cx, |project, cx| { + let new_prettier_task = start_prettier( + node, + prettier_dir.clone(), + Some(worktree_id), + cx, + ); + project.prettier_instances.insert( + prettier_dir.clone(), + PrettierInstance { + attempt: 0, + prettier: Some(new_prettier_task.clone()), + }, + ); + new_prettier_task + }) + .ok()?; + Some((Some(prettier_dir), new_prettier_task)) + } + Err(e) => { + log::error!("Failed to determine prettier path for buffer: {e:#}"); + return None; + } + } + }); + } + None => { + let new_task = self.default_prettier.prettier_task(&node, None, cx); + return cx + .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) }); + } + } + } else { + return Task::ready(None); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn install_default_prettier( + &mut self, + _worktree: Option, + plugins: HashSet<&'static str>, + _cx: &mut ModelContext, + ) { + // suppress unused code warnings + let _ = install_prettier_packages; + let _ = save_prettier_server_file; + + self.default_prettier.installed_plugins.extend(plugins); + self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); + } + + #[cfg(not(any(test, feature = "test-support")))] + pub fn install_default_prettier( + &mut self, + worktree: Option, + mut new_plugins: HashSet<&'static str>, + cx: &mut ModelContext, + ) { + let Some(node) = self.node.as_ref().cloned() else { + return; + }; + log::info!("Initializing default prettier with plugins {new_plugins:?}"); + let fs = Arc::clone(&self.fs); + let locate_prettier_installation = match worktree.and_then(|worktree_id| { + self.worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path()) + }) { + Some(locate_from) => { + let installed_prettiers = self.prettier_instances.keys().cloned().collect(); + cx.background_executor().spawn(async move { + Prettier::locate_prettier_installation( + fs.as_ref(), + &installed_prettiers, + locate_from.as_ref(), + ) + .await + }) + } + None => Task::ready(Ok(ControlFlow::Continue(None))), + }; + new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); + let mut installation_attempt = 0; + let previous_installation_task = match &mut self.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_task, + attempts, + not_installed_plugins, + } => { + installation_attempt = *attempts; + if installation_attempt > prettier::FAIL_THRESHOLD { + *installation_task = None; + log::warn!( + "Default prettier installation had failed {installation_attempt} times, not attempting again", + ); + return; + } + new_plugins.extend(not_installed_plugins.iter()); + installation_task.clone() + } + PrettierInstallation::Installed { .. } => { + if new_plugins.is_empty() { + return; + } + None + } + }; + + let plugins_to_install = new_plugins.clone(); + let fs = Arc::clone(&self.fs); + let new_installation_task = cx + .spawn(|project, mut cx| async move { + match locate_prettier_installation + .await + .context("locate prettier installation") + .map_err(Arc::new)? + { + ControlFlow::Break(()) => return Ok(()), + ControlFlow::Continue(prettier_path) => { + if prettier_path.is_some() { + new_plugins.clear(); + } + let mut needs_install = false; + if let Some(previous_installation_task) = previous_installation_task { + if let Err(e) = previous_installation_task.await { + log::error!("Failed to install default prettier: {e:#}"); + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier { + *attempts += 1; + new_plugins.extend(not_installed_plugins.iter()); + installation_attempt = *attempts; + needs_install = true; + }; + })?; + } + }; + if installation_attempt > prettier::FAIL_THRESHOLD { + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier { + *installation_task = None; + }; + })?; + log::warn!( + "Default prettier installation had failed {installation_attempt} times, not attempting again", + ); + return Ok(()); + } + project.update(&mut cx, |project, _| { + new_plugins.retain(|plugin| { + !project.default_prettier.installed_plugins.contains(plugin) + }); + if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier { + not_installed_plugins.retain(|plugin| { + !project.default_prettier.installed_plugins.contains(plugin) + }); + not_installed_plugins.extend(new_plugins.iter()); + } + needs_install |= !new_plugins.is_empty(); + })?; + if needs_install { + let installed_plugins = new_plugins.clone(); + cx.background_executor() + .spawn(async move { + save_prettier_server_file(fs.as_ref()).await?; + install_prettier_packages(new_plugins, node).await + }) + .await + .context("prettier & plugins install") + .map_err(Arc::new)?; + log::info!("Initialized prettier with plugins: {installed_plugins:?}"); + project.update(&mut cx, |project, _| { + project.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); + project.default_prettier + .installed_plugins + .extend(installed_plugins); + })?; + } + } + } + Ok(()) + }) + .shared(); + self.default_prettier.prettier = PrettierInstallation::NotInstalled { + attempts: installation_attempt, + installation_task: Some(new_installation_task), + not_installed_plugins: plugins_to_install, + }; + } +} diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 856c280ac02e8971ae54e7cbf583267f0e0ff1ee..12940dd2c427f8872fd95c9a1c66a5535738463e 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -1,5 +1,6 @@ mod ignore; mod lsp_command; +mod prettier_support; pub mod project_settings; pub mod search; pub mod terminals; @@ -20,7 +21,7 @@ use futures::{ mpsc::{self, UnboundedReceiver}, oneshot, }, - future::{self, try_join_all, Shared}, + future::{try_join_all, Shared}, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; @@ -31,9 +32,7 @@ use gpui::{ }; use itertools::Itertools; use language::{ - language_settings::{ - language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, - }, + language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -54,7 +53,7 @@ use lsp_command::*; use node_runtime::NodeRuntime; use parking_lot::Mutex; use postage::watch; -use prettier::Prettier; +use prettier_support::{DefaultPrettier, PrettierInstance}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -70,7 +69,7 @@ use std::{ hash::Hash, mem, num::NonZeroU32, - ops::{ControlFlow, Range}, + ops::Range, path::{self, Component, Path, PathBuf}, process::Stdio, str, @@ -83,11 +82,8 @@ use std::{ use terminals::Terminals; use text::Anchor; use util::{ - debug_panic, defer, - http::HttpClient, - merge_json_value_into, - paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, - post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, http::HttpClient, merge_json_value_into, + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -166,16 +162,9 @@ pub struct Project { copilot_log_subscription: Option, current_lsp_settings: HashMap, LspSettings>, node: Option>, - default_prettier: Option, + default_prettier: DefaultPrettier, prettiers_per_worktree: HashMap>>, - prettier_instances: HashMap, Arc>>>>, -} - -struct DefaultPrettier { - instance: Option, Arc>>>>, - installation_process: Option>>>>, - #[cfg(not(any(test, feature = "test-support")))] - installed_plugins: HashSet<&'static str>, + prettier_instances: HashMap, } struct DelayedDebounced { @@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate { http_client: Arc, } +// Currently, formatting operations are represented differently depending on +// whether they come from a language server or an external command. +enum FormatOperation { + Lsp(Vec<(Range, String)>), + External(Diff), + Prettier(Diff), +} + impl FormatTrigger { fn from_proto(value: i32) -> FormatTrigger { match value { @@ -689,7 +686,7 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), node: Some(node), - default_prettier: None, + default_prettier: DefaultPrettier::default(), prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), } @@ -792,7 +789,7 @@ impl Project { copilot_log_subscription: None, current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), node: None, - default_prettier: None, + default_prettier: DefaultPrettier::default(), prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; @@ -965,8 +962,19 @@ impl Project { .detach(); } + let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language, settings) in language_formatters_to_check { - self.install_default_formatters(worktree, &language, &settings, cx); + if let Some(plugins) = + prettier_support::prettier_plugins_for_language(&language, &settings) + { + prettier_plugins_by_worktree + .entry(worktree) + .or_insert_with(|| HashSet::default()) + .extend(plugins); + } + } + for (worktree, prettier_plugins) in prettier_plugins_by_worktree { + self.install_default_prettier(worktree, prettier_plugins, cx); } // Start all the newly-enabled language servers. @@ -2669,8 +2677,9 @@ impl Project { })?; for (adapter, server) in servers { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(server.root_path(), cx))? + .await; server .notify::( lsp::DidChangeConfigurationParams { @@ -2722,8 +2731,11 @@ impl Project { let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); - - self.install_default_formatters(worktree, &new_language, &settings, cx); + if let Some(prettier_plugins) = + prettier_support::prettier_plugins_for_language(&new_language, &settings) + { + self.install_default_prettier(worktree, prettier_plugins, cx); + }; if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { @@ -2779,7 +2791,7 @@ impl Project { stderr_capture.clone(), language.clone(), adapter.clone(), - worktree_path, + Arc::clone(&worktree_path), ProjectLspAdapterDelegate::new(self, cx), cx, ) { @@ -2811,6 +2823,7 @@ impl Project { cx.spawn(move |this, mut cx| async move { let result = Self::setup_and_insert_language_server( this.clone(), + &worktree_path, initialization_options, pending_server, adapter.clone(), @@ -2931,6 +2944,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModel, + worktree_path: &Path, initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -2943,6 +2957,7 @@ impl Project { this.clone(), initialization_options, pending_server, + worktree_path, adapter.clone(), server_id, cx, @@ -2972,11 +2987,14 @@ impl Project { this: WeakModel, initialization_options: Option, pending_server: PendingLanguageServer, + worktree_path: &Path, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(worktree_path, cx))? + .await; let language_server = pending_server.task.await?; language_server @@ -3005,11 +3023,14 @@ impl Project { language_server .on_request::({ let adapter = adapter.clone(); + let worktree_path = worktree_path.to_path_buf(); move |params, cx| { let adapter = adapter.clone(); + let worktree_path = worktree_path.clone(); async move { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(&worktree_path, cx))? + .await; Ok(params .items .into_iter() @@ -4126,7 +4147,8 @@ impl Project { this.buffers_being_formatted .remove(&buffer.read(cx).remote_id()); } - }).ok(); + }) + .ok(); } }); @@ -4138,8 +4160,6 @@ impl Project { let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; let ensure_final_newline = settings.ensure_final_newline_on_save; - let format_on_save = settings.format_on_save.clone(); - let formatter = settings.formatter.clone(); let tab_size = settings.tab_size; // First, format buffer's whitespace according to the settings. @@ -4164,18 +4184,10 @@ impl Project { buffer.end_transaction(cx) })?; - // Currently, formatting operations are represented differently depending on - // whether they come from a language server or an external command. - enum FormatOperation { - Lsp(Vec<(Range, String)>), - External(Diff), - Prettier(Diff), - } - // Apply language-specific formatting using either a language server // or external command. let mut format_operation = None; - match (formatter, format_on_save) { + match (&settings.formatter, &settings.format_on_save) { (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) @@ -4220,46 +4232,11 @@ impl Project { } } (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { - if let Some((prettier_path, prettier_task)) = project - .update(&mut cx, |project, cx| { - project.prettier_instance_for_buffer(buffer, cx) - })?.await { - match prettier_task.await - { - Ok(prettier) => { - let buffer_path = buffer.update(&mut cx, |buffer, cx| { - File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) - })?; - format_operation = Some(FormatOperation::Prettier( - prettier - .format(buffer, buffer_path, &mut cx) - .await - .context("formatting via prettier")?, - )); - } - Err(e) => { - project.update(&mut cx, |project, _| { - match &prettier_path { - Some(prettier_path) => { - project.prettier_instances.remove(prettier_path); - }, - None => { - if let Some(default_prettier) = project.default_prettier.as_mut() { - default_prettier.instance = None; - } - }, - } - })?; - match &prettier_path { - Some(prettier_path) => { - log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); - }, - None => { - log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); - }, - } - } - } + if let Some(new_operation) = + prettier_support::format_with_prettier(&project, buffer, &mut cx) + .await + { + format_operation = Some(new_operation); } else if let Some((language_server, buffer_abs_path)) = language_server.as_ref().zip(buffer_abs_path.as_ref()) { @@ -4277,48 +4254,13 @@ impl Project { )); } } - (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { - if let Some((prettier_path, prettier_task)) = project - .update(&mut cx, |project, cx| { - project.prettier_instance_for_buffer(buffer, cx) - })?.await { - match prettier_task.await - { - Ok(prettier) => { - let buffer_path = buffer.update(&mut cx, |buffer, cx| { - File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) - })?; - format_operation = Some(FormatOperation::Prettier( - prettier - .format(buffer, buffer_path, &mut cx) - .await - .context("formatting via prettier")?, - )); - } - Err(e) => { - project.update(&mut cx, |project, _| { - match &prettier_path { - Some(prettier_path) => { - project.prettier_instances.remove(prettier_path); - }, - None => { - if let Some(default_prettier) = project.default_prettier.as_mut() { - default_prettier.instance = None; - } - }, - } - })?; - match &prettier_path { - Some(prettier_path) => { - log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); - }, - None => { - log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); - }, - } - } - } - } + (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(new_operation) = + prettier_support::format_with_prettier(&project, buffer, &mut cx) + .await + { + format_operation = Some(new_operation); + } } }; @@ -6638,84 +6580,6 @@ impl Project { .detach(); } - fn update_prettier_settings( - &self, - worktree: &Model, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut ModelContext<'_, Project>, - ) { - let prettier_config_files = Prettier::CONFIG_FILE_NAMES - .iter() - .map(Path::new) - .collect::>(); - - let prettier_config_file_changed = changes - .iter() - .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) - .filter(|(path, _, _)| { - !path - .components() - .any(|component| component.as_os_str().to_string_lossy() == "node_modules") - }) - .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); - let current_worktree_id = worktree.read(cx).id(); - if let Some((config_path, _, _)) = prettier_config_file_changed { - log::info!( - "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" - ); - let prettiers_to_reload = self - .prettiers_per_worktree - .get(¤t_worktree_id) - .iter() - .flat_map(|prettier_paths| prettier_paths.iter()) - .flatten() - .filter_map(|prettier_path| { - Some(( - current_worktree_id, - Some(prettier_path.clone()), - self.prettier_instances.get(prettier_path)?.clone(), - )) - }) - .chain(self.default_prettier.iter().filter_map(|default_prettier| { - Some(( - current_worktree_id, - None, - default_prettier.instance.clone()?, - )) - })) - .collect::>(); - - cx.background_executor() - .spawn(async move { - for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { - async move { - prettier_task.await? - .clear_cache() - .await - .with_context(|| { - match prettier_path { - Some(prettier_path) => format!( - "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" - ), - None => format!( - "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update" - ), - } - }) - .map_err(Arc::new) - } - })) - .await - { - if let Err(e) = task_result { - log::error!("Failed to clear cache for prettier: {e:#}"); - } - } - }) - .detach(); - } - } - pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -6742,9 +6606,15 @@ impl Project { }) } - pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { + pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary { let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in self.diagnostic_summaries(cx) { + for (_, _, path_summary) in + self.diagnostic_summaries(include_ignored, cx) + .filter(|(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) + { summary.error_count += path_summary.error_count; summary.warning_count += path_summary.warning_count; } @@ -6753,17 +6623,23 @@ impl Project { pub fn diagnostic_summaries<'a>( &'a self, + include_ignored: bool, cx: &'a AppContext, ) -> impl Iterator + 'a { - self.visible_worktrees(cx).flat_map(move |worktree| { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - worktree - .diagnostic_summaries() - .map(move |(path, server_id, summary)| { - (ProjectPath { worktree_id, path }, server_id, summary) - }) - }) + self.visible_worktrees(cx) + .flat_map(move |worktree| { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + worktree + .diagnostic_summaries() + .map(move |(path, server_id, summary)| { + (ProjectPath { worktree_id, path }, server_id, summary) + }) + }) + .filter(move |(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) } pub fn disk_based_diagnostics_started( @@ -8579,486 +8455,6 @@ impl Project { Vec::new() } } - - fn prettier_instance_for_buffer( - &mut self, - buffer: &Model, - cx: &mut ModelContext, - ) -> Task< - Option<( - Option, - Shared, Arc>>>, - )>, - > { - let buffer = buffer.read(cx); - let buffer_file = buffer.file(); - let Some(buffer_language) = buffer.language() else { - return Task::ready(None); - }; - if buffer_language.prettier_parser_name().is_none() { - return Task::ready(None); - } - - if self.is_local() { - let Some(node) = self.node.as_ref().map(Arc::clone) else { - return Task::ready(None); - }; - match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) - { - Some((worktree_id, buffer_path)) => { - let fs = Arc::clone(&self.fs); - let installed_prettiers = self.prettier_instances.keys().cloned().collect(); - return cx.spawn(|project, mut cx| async move { - match cx - .background_executor() - .spawn(async move { - Prettier::locate_prettier_installation( - fs.as_ref(), - &installed_prettiers, - &buffer_path, - ) - .await - }) - .await - { - Ok(ControlFlow::Break(())) => { - return None; - } - Ok(ControlFlow::Continue(None)) => { - match project.update(&mut cx, |project, _| { - project - .prettiers_per_worktree - .entry(worktree_id) - .or_default() - .insert(None); - project.default_prettier.as_ref().and_then( - |default_prettier| default_prettier.instance.clone(), - ) - }) { - Ok(Some(old_task)) => Some((None, old_task)), - Ok(None) => { - match project.update(&mut cx, |_, cx| { - start_default_prettier(node, Some(worktree_id), cx) - }) { - Ok(new_default_prettier) => { - return Some((None, new_default_prettier.await)) - } - Err(e) => { - Some(( - None, - Task::ready(Err(Arc::new(e.context("project is gone during default prettier startup")))) - .shared(), - )) - } - } - } - Err(e) => Some((None, Task::ready(Err(Arc::new(e.context("project is gone during default prettier checks")))) - .shared())), - } - } - Ok(ControlFlow::Continue(Some(prettier_dir))) => { - match project.update(&mut cx, |project, _| { - project - .prettiers_per_worktree - .entry(worktree_id) - .or_default() - .insert(Some(prettier_dir.clone())); - project.prettier_instances.get(&prettier_dir).cloned() - }) { - Ok(Some(existing_prettier)) => { - log::debug!( - "Found already started prettier in {prettier_dir:?}" - ); - return Some((Some(prettier_dir), existing_prettier)); - } - Err(e) => { - return Some(( - Some(prettier_dir), - Task::ready(Err(Arc::new(e.context("project is gone during custom prettier checks")))) - .shared(), - )) - } - _ => {}, - } - - log::info!("Found prettier in {prettier_dir:?}, starting."); - let new_prettier_task = - match project.update(&mut cx, |project, cx| { - let new_prettier_task = start_prettier( - node, - prettier_dir.clone(), - Some(worktree_id), - cx, - ); - project.prettier_instances.insert( - prettier_dir.clone(), - new_prettier_task.clone(), - ); - new_prettier_task - }) { - Ok(task) => task, - Err(e) => return Some(( - Some(prettier_dir), - Task::ready(Err(Arc::new(e.context("project is gone during custom prettier startup")))) - .shared() - )), - }; - Some((Some(prettier_dir), new_prettier_task)) - } - Err(e) => { - return Some(( - None, - Task::ready(Err(Arc::new( - e.context("determining prettier path"), - ))) - .shared(), - )); - } - } - }); - } - None => { - let started_default_prettier = self - .default_prettier - .as_ref() - .and_then(|default_prettier| default_prettier.instance.clone()); - match started_default_prettier { - Some(old_task) => return Task::ready(Some((None, old_task))), - None => { - let new_task = start_default_prettier(node, None, cx); - return cx.spawn(|_, _| async move { Some((None, new_task.await)) }); - } - } - } - } - } else if self.remote_id().is_some() { - return Task::ready(None); - } else { - Task::ready(Some(( - None, - Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), - ))) - } - } - - #[cfg(any(test, feature = "test-support"))] - fn install_default_formatters( - &mut self, - _: Option, - _: &Language, - _: &LanguageSettings, - _: &mut ModelContext, - ) { - } - - #[cfg(not(any(test, feature = "test-support")))] - fn install_default_formatters( - &mut self, - worktree: Option, - new_language: &Language, - language_settings: &LanguageSettings, - cx: &mut ModelContext, - ) { - match &language_settings.formatter { - Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return, - }; - let Some(node) = self.node.as_ref().cloned() else { - return; - }; - - let mut prettier_plugins = None; - if new_language.prettier_parser_name().is_some() { - prettier_plugins - .get_or_insert_with(|| HashSet::<&'static str>::default()) - .extend( - new_language - .lsp_adapters() - .iter() - .flat_map(|adapter| adapter.prettier_plugins()), - ) - } - let Some(prettier_plugins) = prettier_plugins else { - return; - }; - - let fs = Arc::clone(&self.fs); - let locate_prettier_installation = match worktree.and_then(|worktree_id| { - self.worktree_for_id(worktree_id, cx) - .map(|worktree| worktree.read(cx).abs_path()) - }) { - Some(locate_from) => { - let installed_prettiers = self.prettier_instances.keys().cloned().collect(); - cx.background_executor().spawn(async move { - Prettier::locate_prettier_installation( - fs.as_ref(), - &installed_prettiers, - locate_from.as_ref(), - ) - .await - }) - } - None => Task::ready(Ok(ControlFlow::Break(()))), - }; - let mut plugins_to_install = prettier_plugins; - let previous_installation_process = - if let Some(default_prettier) = &mut self.default_prettier { - plugins_to_install - .retain(|plugin| !default_prettier.installed_plugins.contains(plugin)); - if plugins_to_install.is_empty() { - return; - } - default_prettier.installation_process.clone() - } else { - None - }; - - let fs = Arc::clone(&self.fs); - let default_prettier = self - .default_prettier - .get_or_insert_with(|| DefaultPrettier { - instance: None, - installation_process: None, - installed_plugins: HashSet::default(), - }); - default_prettier.installation_process = Some( - cx.spawn(|this, mut cx| async move { - match locate_prettier_installation - .await - .context("locate prettier installation") - .map_err(Arc::new)? - { - ControlFlow::Break(()) => return Ok(()), - ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()), - ControlFlow::Continue(None) => { - let mut needs_install = match previous_installation_process { - Some(previous_installation_process) => { - previous_installation_process.await.is_err() - } - None => true, - }; - this.update(&mut cx, |this, _| { - if let Some(default_prettier) = &mut this.default_prettier { - plugins_to_install.retain(|plugin| { - !default_prettier.installed_plugins.contains(plugin) - }); - needs_install |= !plugins_to_install.is_empty(); - } - })?; - if needs_install { - let installed_plugins = plugins_to_install.clone(); - cx.background_executor() - .spawn(async move { - install_default_prettier(plugins_to_install, node, fs).await - }) - .await - .context("prettier & plugins install") - .map_err(Arc::new)?; - this.update(&mut cx, |this, _| { - let default_prettier = - this.default_prettier - .get_or_insert_with(|| DefaultPrettier { - instance: None, - installation_process: Some( - Task::ready(Ok(())).shared(), - ), - installed_plugins: HashSet::default(), - }); - default_prettier.instance = None; - default_prettier.installed_plugins.extend(installed_plugins); - })?; - } - } - } - Ok(()) - }) - .shared(), - ); - } -} - -fn start_default_prettier( - node: Arc, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, -) -> Task, Arc>>>> { - cx.spawn(|project, mut cx| async move { - loop { - let default_prettier_installing = match project.update(&mut cx, |project, _| { - project - .default_prettier - .as_ref() - .and_then(|default_prettier| default_prettier.installation_process.clone()) - }) { - Ok(installation) => installation, - Err(e) => { - return Task::ready(Err(Arc::new( - e.context("project is gone during default prettier installation"), - ))) - .shared() - } - }; - match default_prettier_installing { - Some(installation_task) => { - if installation_task.await.is_ok() { - break; - } - } - None => break, - } - } - - match project.update(&mut cx, |project, cx| { - match project - .default_prettier - .as_mut() - .and_then(|default_prettier| default_prettier.instance.as_mut()) - { - Some(default_prettier) => default_prettier.clone(), - None => { - let new_default_prettier = - start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); - project - .default_prettier - .get_or_insert_with(|| DefaultPrettier { - instance: None, - installation_process: None, - #[cfg(not(any(test, feature = "test-support")))] - installed_plugins: HashSet::default(), - }) - .instance = Some(new_default_prettier.clone()); - new_default_prettier - } - } - }) { - Ok(task) => task, - Err(e) => Task::ready(Err(Arc::new( - e.context("project is gone during default prettier startup"), - ))) - .shared(), - } - }) -} - -fn start_prettier( - node: Arc, - prettier_dir: PathBuf, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, -) -> Shared, Arc>>> { - cx.spawn(|project, mut cx| async move { - let new_server_id = project.update(&mut cx, |project, _| { - project.languages.next_language_server_id() - })?; - let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) - .await - .context("default prettier spawn") - .map(Arc::new) - .map_err(Arc::new)?; - register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); - Ok(new_prettier) - }) - .shared() -} - -fn register_new_prettier( - project: &WeakModel, - prettier: &Prettier, - worktree_id: Option, - new_server_id: LanguageServerId, - cx: &mut AsyncAppContext, -) { - let prettier_dir = prettier.prettier_dir(); - let is_default = prettier.is_default(); - if is_default { - log::info!("Started default prettier in {prettier_dir:?}"); - } else { - log::info!("Started prettier in {prettier_dir:?}"); - } - if let Some(prettier_server) = prettier.server() { - project - .update(cx, |project, cx| { - let name = if is_default { - LanguageServerName(Arc::from("prettier (default)")) - } else { - let worktree_path = worktree_id - .and_then(|id| project.worktree_for_id(id, cx)) - .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); - let name = match worktree_path { - Some(worktree_path) => { - if prettier_dir == worktree_path.as_ref() { - let name = prettier_dir - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or_default(); - format!("prettier ({name})") - } else { - let dir_to_display = prettier_dir - .strip_prefix(worktree_path.as_ref()) - .ok() - .unwrap_or(prettier_dir); - format!("prettier ({})", dir_to_display.display()) - } - } - None => format!("prettier ({})", prettier_dir.display()), - }; - LanguageServerName(Arc::from(name)) - }; - project - .supplementary_language_servers - .insert(new_server_id, (name, Arc::clone(prettier_server))); - cx.emit(Event::LanguageServerAdded(new_server_id)); - }) - .ok(); - } -} - -#[cfg(not(any(test, feature = "test-support")))] -async fn install_default_prettier( - plugins_to_install: HashSet<&'static str>, - node: Arc, - fs: Arc, -) -> anyhow::Result<()> { - let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); - // method creates parent directory if it doesn't exist - fs.save( - &prettier_wrapper_path, - &text::Rope::from(prettier::PRETTIER_SERVER_JS), - text::LineEnding::Unix, - ) - .await - .with_context(|| { - format!( - "writing {} file at {prettier_wrapper_path:?}", - prettier::PRETTIER_SERVER_FILE - ) - })?; - - let packages_to_versions = - future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( - |package_name| async { - let returned_package_name = package_name.to_string(); - let latest_version = node - .npm_package_latest_version(package_name) - .await - .with_context(|| { - format!("fetching latest npm version for package {returned_package_name}") - })?; - anyhow::Ok((returned_package_name, latest_version)) - }, - )) - .await - .context("fetching latest npm versions")?; - - log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); - let borrowed_packages = packages_to_versions - .iter() - .map(|(package, version)| (package.as_str(), version.as_str())) - .collect::>(); - node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) - .await - .context("fetching formatter packages")?; - anyhow::Ok(()) } fn subscribe_for_copilot_events( diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 53b2f6ba1fc7cc04489bce5256a0954b9e2bf7ff..4dfb8004e3e644309b89ee31d99f5ac07e05f4b3 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -823,7 +823,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { +async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); @@ -831,7 +831,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { "/root", json!({ "dir": { + ".git": { + "HEAD": "ref: refs/heads/main", + }, + ".gitignore": "b.rs", "a.rs": "let a = 1;", + "b.rs": "let b = 2;", }, "other.rs": "let b = c;" }), @@ -839,6 +844,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root/dir", true, cx) + }) + .await + .unwrap(); + let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id()); let (worktree, _) = project .update(cx, |project, cx| { @@ -846,12 +858,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - let worktree_id = worktree.update(cx, |tree, _| tree.id()); + let other_worktree_id = worktree.update(cx, |tree, _| tree.id()); + let server_id = LanguageServerId(0); project.update(cx, |project, cx| { project .update_diagnostics( - LanguageServerId(0), + server_id, + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/root/dir/b.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "unused variable 'b'".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + project + .update_diagnostics( + server_id, lsp::PublishDiagnosticsParams { uri: Url::from_file_path("/root/other.rs").unwrap(), version: None, @@ -868,11 +898,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .unwrap(); }); - let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + let main_ignored_buffer = project + .update(cx, |project, cx| { + project.open_buffer((main_worktree_id, "b.rs"), cx) + }) .await .unwrap(); - buffer.update(cx, |buffer, _| { + main_ignored_buffer.update(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let ", None), + ("b", Some(DiagnosticSeverity::ERROR)), + (" = 2;", None), + ], + "Gigitnored buffers should still get in-buffer diagnostics", + ); + }); + let other_buffer = project + .update(cx, |project, cx| { + project.open_buffer((other_worktree_id, ""), cx) + }) + .await + .unwrap(); + other_buffer.update(cx, |buffer, _| { let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); assert_eq!( chunks @@ -883,13 +936,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ("let b = ", None), ("c", Some(DiagnosticSeverity::ERROR)), (";", None), - ] + ], + "Buffers from hidden projects should still get in-buffer diagnostics" ); }); project.update(cx, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).next(), None); - assert_eq!(project.diagnostic_summary(cx).error_count, 0); + assert_eq!(project.diagnostic_summaries(false, cx).next(), None); + assert_eq!( + project.diagnostic_summaries(true, cx).collect::>(), + vec![( + ProjectPath { + worktree_id: main_worktree_id, + path: Arc::from(Path::new("b.rs")), + }, + server_id, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + } + )] + ); + assert_eq!(project.diagnostic_summary(false, cx).error_count, 0); + assert_eq!(project.diagnostic_summary(true, cx).error_count, 1); }); } @@ -1162,7 +1231,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 1, warning_count: 0, @@ -1188,7 +1257,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 0, warning_count: 0, @@ -1777,7 +1846,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC .unwrap(); assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 2, warning_count: 0, diff --git a/crates/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs index 9e9a865f3e78d2c563476ef4982286c1cfc60405..82aebe7913133d2aa7f673f21387f2c7a8ad3ca3 100644 --- a/crates/project_panel2/src/file_associations.rs +++ b/crates/project_panel2/src/file_associations.rs @@ -41,56 +41,47 @@ impl FileAssociations { }) } - pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - // FIXME: Associate a type with the languages and have the file's langauge - // override these associations - maybe!({ - let suffix = path.icon_suffix()?; + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations + maybe!({ + let suffix = path.icon_suffix()?; - this.suffixes - .get(suffix) - .and_then(|type_str| this.types.get(type_str)) - .map(|type_config| type_config.icon.clone()) - }) - .or_else(|| this.types.get("default").map(|config| config.icon.clone())) + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) }) - .unwrap_or_else(|| Arc::from("".to_string())) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) } - pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - let key = if expanded { - EXPANDED_DIRECTORY_TYPE - } else { - COLLAPSED_DIRECTORY_TYPE - }; + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + COLLAPSED_DIRECTORY_TYPE + }; - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - }) - .unwrap_or_else(|| Arc::from("".to_string())) + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) } - pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - let key = if expanded { - EXPANDED_CHEVRON_TYPE - } else { - COLLAPSED_CHEVRON_TYPE - }; + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - }) - .unwrap_or_else(|| Arc::from("".to_string())) + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) } } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index d2d30556ebcb1d7864840cb1a72d5e338bd4da58..dc584d52ff520a810b27ecc2a803738491643950 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,10 +8,10 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, - IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, - Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, + actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, + InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, + PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -30,7 +30,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack, IconElement, Label}; +use ui::{v_stack, ContextMenu, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -50,6 +50,7 @@ pub struct ProjectPanel { last_worktree_root_id: Option, expanded_dir_ids: HashMap>, selection: Option, + context_menu: Option<(View, Point, Subscription)>, edit_state: Option, filename_editor: View, clipboard_entry: Option, @@ -232,6 +233,7 @@ impl ProjectPanel { expanded_dir_ids: Default::default(), selection: None, edit_state: None, + context_menu: None, filename_editor, clipboard_entry: None, // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), @@ -367,80 +369,93 @@ impl ProjectPanel { fn deploy_context_menu( &mut self, - _position: Point, - _entry_id: ProjectEntryId, - _cx: &mut ViewContext, + position: Point, + entry_id: ProjectEntryId, + cx: &mut ViewContext, ) { - // todo!() - // let project = self.project.read(cx); + let this = cx.view().clone(); + let project = self.project.read(cx); - // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { - // id - // } else { - // return; - // }; + let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + id + } else { + return; + }; - // self.selection = Some(Selection { - // worktree_id, - // entry_id, - // }); - - // let mut menu_entries = Vec::new(); - // if let Some((worktree, entry)) = self.selected_entry(cx) { - // let is_root = Some(entry) == worktree.root_entry(); - // if !project.is_remote() { - // menu_entries.push(ContextMenuItem::action( - // "Add Folder to Project", - // workspace::AddFolderToProject, - // )); - // if is_root { - // let project = self.project.clone(); - // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { - // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - // })); - // } - // } - // menu_entries.push(ContextMenuItem::action("New File", NewFile)); - // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Cut", Cut)); - // menu_entries.push(ContextMenuItem::action("Copy", Copy)); - // if let Some(clipboard_entry) = self.clipboard_entry { - // if clipboard_entry.worktree_id() == worktree.id() { - // menu_entries.push(ContextMenuItem::action("Paste", Paste)); - // } - // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); - // menu_entries.push(ContextMenuItem::action( - // "Copy Relative Path", - // CopyRelativePath, - // )); - - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::Separator); - // } - // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); - // menu_entries.push(ContextMenuItem::action( - // "Search Inside", - // NewSearchInDirectory, - // )); - // } - - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Rename", Rename)); - // if !is_root { - // menu_entries.push(ContextMenuItem::action("Delete", Delete)); - // } - // } - - // // self.context_menu.update(cx, |menu, cx| { - // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - // // }); - - // cx.notify(); + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + + if let Some((worktree, entry)) = self.selected_entry(cx) { + let is_root = Some(entry) == worktree.root_entry(); + let is_dir = entry.is_dir(); + let worktree_id = worktree.id(); + let is_local = project.is_local(); + + let context_menu = ContextMenu::build(cx, |mut menu, cx| { + if is_local { + menu = menu.action( + "Add Folder to Project", + Box::new(workspace::AddFolderToProject), + cx, + ); + if is_root { + menu = menu.entry( + "Remove from Project", + cx.handler_for(&this, move |this, cx| { + this.project.update(cx, |project, cx| { + project.remove_worktree(worktree_id, cx) + }); + }), + ); + } + } + + menu = menu + .action("New File", Box::new(NewFile), cx) + .action("New Folder", Box::new(NewDirectory), cx) + .separator() + .action("Cut", Box::new(Cut), cx) + .action("Copy", Box::new(Copy), cx); + + if let Some(clipboard_entry) = self.clipboard_entry { + if clipboard_entry.worktree_id() == worktree_id { + menu = menu.action("Paste", Box::new(Paste), cx); + } + } + + menu = menu + .separator() + .action("Copy Path", Box::new(CopyPath), cx) + .action("Copy Relative Path", Box::new(CopyRelativePath), cx) + .separator() + .action("Reveal in Finder", Box::new(RevealInFinder), cx); + + if is_dir { + menu = menu + .action("Open in Terminal", Box::new(OpenInTerminal), cx) + .action("Search Inside", Box::new(NewSearchInDirectory), cx) + } + + menu = menu.separator().action("Rename", Box::new(Rename), cx); + + if !is_root { + menu = menu.action("Delete", Box::new(Delete), cx); + } + + menu + }); + + cx.focus_view(&context_menu); + let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); + } + + cx.notify(); } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { @@ -1268,16 +1283,16 @@ impl ProjectPanel { let icon = match entry.kind { EntryKind::File(_) => { if show_file_icons { - Some(FileAssociations::get_icon(&entry.path, cx)) + FileAssociations::get_icon(&entry.path, cx) } else { None } } _ => { if show_folder_icons { - Some(FileAssociations::get_folder_icon(is_expanded, cx)) + FileAssociations::get_folder_icon(is_expanded, cx) } else { - Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + FileAssociations::get_chevron_icon(is_expanded, cx) } } }; @@ -1334,13 +1349,19 @@ impl ProjectPanel { } } - fn render_entry_visual_element( - details: &EntryDetails, - editor: Option<&View>, - padding: Pixels, + fn render_entry( + &self, + entry_id: ProjectEntryId, + details: EntryDetails, + // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> Div { + ) -> ListItem { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); let theme = cx.theme(); let filename_text_color = details @@ -1353,15 +1374,18 @@ impl ProjectPanel { }) .unwrap_or(theme.status().info); - h_stack() + ListItem::new(entry_id.to_proto() as usize) + .indent_level(details.depth) + .indent_step_size(px(settings.indent_size)) + .selected(is_selected) .child(if let Some(icon) = &details.icon { div().child(IconElement::from_path(icon.to_string())) } else { div() }) .child( - if let (Some(editor), true) = (editor, show_editor) { - div().w_full().child(editor.clone()) + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { + div().h_full().w_full().child(editor.clone()) } else { div() .text_color(filename_text_color) @@ -1369,34 +1393,10 @@ impl ProjectPanel { } .ml_1(), ) - .pl(padding) - } - - fn render_entry( - &self, - entry_id: ProjectEntryId, - details: EntryDetails, - // dragged_entry_destination: &mut Option>, - cx: &mut ViewContext, - ) -> Stateful

{ - let kind = details.kind; - let settings = ProjectPanelSettings::get_global(cx); - const INDENT_SIZE: Pixels = px(16.0); - let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); - let show_editor = details.is_editing && !details.is_processing; - let is_selected = self - .selection - .map_or(false, |selection| selection.entry_id == entry_id); - - Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) - .id(entry_id.to_proto() as usize) - .w_full() - .cursor_pointer() - .when(is_selected, |this| { - this.bg(cx.theme().colors().element_selected) - }) - .hover(|style| style.bg(cx.theme().colors().element_hover)) .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { + if event.down.button == MouseButton::Right { + return; + } if !show_editor { if kind.is_dir() { this.toggle_expanded(entry_id, cx); @@ -1409,12 +1409,9 @@ impl ProjectPanel { } } })) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - }), - ) + .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + })) // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, @@ -1436,6 +1433,7 @@ impl Render for ProjectPanel { div() .id("project-panel") .size_full() + .relative() .key_context("ProjectPanel") .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_prev)) @@ -1479,6 +1477,12 @@ impl Render for ProjectPanel { .size_full() .track_scroll(self.list.clone()), ) + .children(self.context_menu.as_ref().map(|(menu, position, _)| { + overlay() + .position(*position) + .anchor(gpui::AnchorCorner::TopLeft) + .child(menu.clone()) + })) } else { v_stack() .id("empty-project_panel") diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index ad549256f9e0f277ee9c050a12c39d68f7c202ad..b3d600611327f67802db738f57411b322593420d 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -7,23 +7,23 @@ use crate::{ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, }; use collections::HashMap; -use editor::Editor; +use editor::{Editor, EditorMode}; use futures::channel::oneshot; use gpui::{ actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _, - WindowContext, + WeakView, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; -use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; +use ui::{h_stack, Icon, IconButton, IconElement}; use util::ResultExt; use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemLocation, ToolbarItemView, }; #[derive(PartialEq, Clone, Deserialize, Default, Action)] @@ -38,7 +38,7 @@ pub enum Event { } pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) + cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx)) .detach(); } @@ -187,6 +187,7 @@ impl Render for BufferSearchBar { }) .on_action(cx.listener(Self::previous_history_query)) .on_action(cx.listener(Self::next_history_query)) + .on_action(cx.listener(Self::dismiss)) .w_full() .p_1() .child( @@ -213,10 +214,11 @@ impl Render for BufferSearchBar { .child( h_stack() .flex_none() - .child(ButtonGroup::new(vec![ - search_button_for_mode(SearchMode::Text), - search_button_for_mode(SearchMode::Regex), - ])) + .child( + h_stack() + .child(search_button_for_mode(SearchMode::Text)) + .child(search_button_for_mode(SearchMode::Regex)), + ) .when(supported_options.replacement, |this| { this.child(super::toggle_replace_button(self.replace_enabled)) }), @@ -294,9 +296,19 @@ impl ToolbarItemView for BufferSearchBar { } impl BufferSearchBar { - pub fn register(workspace: &mut Workspace) { - workspace.register_action(|workspace, a: &Deploy, cx| { - workspace.active_pane().update(cx, |this, cx| { + pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + if editor.mode() != EditorMode::Full { + return; + }; + + let handle = cx.view().downgrade(); + + editor.register_action(move |a: &Deploy, cx| { + let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else { + return; + }; + + pane.update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| { if let Some(search_bar) = this.item_of_type::() { search_bar.update(cx, |this, cx| { @@ -316,11 +328,16 @@ impl BufferSearchBar { }); }); fn register_action( - workspace: &mut Workspace, + editor: &mut Editor, + handle: WeakView, update: fn(&mut BufferSearchBar, &A, &mut ViewContext), ) { - workspace.register_action(move |workspace, action: &A, cx| { - workspace.active_pane().update(cx, move |this, cx| { + editor.register_action(move |action: &A, cx| { + let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) + else { + return; + }; + pane.update(cx, move |this, cx| { this.toolbar().update(cx, move |this, cx| { if let Some(search_bar) = this.item_of_type::() { search_bar.update(cx, move |this, cx| update(this, action, cx)); @@ -331,49 +348,76 @@ impl BufferSearchBar { }); } - register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { - if this.supported_options().case { - this.toggle_case_sensitive(action, cx); - } - }); - register_action(workspace, |this, action: &ToggleWholeWord, cx| { - if this.supported_options().word { - this.toggle_whole_word(action, cx); - } - }); - register_action(workspace, |this, action: &ToggleReplace, cx| { - if this.supported_options().replacement { - this.toggle_replace(action, cx); - } - }); - register_action(workspace, |this, _: &ActivateRegexMode, cx| { + let handle = cx.view().downgrade(); + register_action( + editor, + handle.clone(), + |this, action: &ToggleCaseSensitive, cx| { + if this.supported_options().case { + this.toggle_case_sensitive(action, cx); + } + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &ToggleWholeWord, cx| { + if this.supported_options().word { + this.toggle_whole_word(action, cx); + } + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &ToggleReplace, cx| { + if this.supported_options().replacement { + this.toggle_replace(action, cx); + } + }, + ); + register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| { if this.supported_options().regex { this.activate_search_mode(SearchMode::Regex, cx); } }); - register_action(workspace, |this, _: &ActivateTextMode, cx| { + register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| { this.activate_search_mode(SearchMode::Text, cx); }); - register_action(workspace, |this, action: &CycleMode, cx| { + register_action(editor, handle.clone(), |this, action: &CycleMode, cx| { if this.supported_options().regex { // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting // cycling. this.cycle_mode(action, cx) } }); - register_action(workspace, |this, action: &SelectNextMatch, cx| { - this.select_next_match(action, cx); - }); - register_action(workspace, |this, action: &SelectPrevMatch, cx| { - this.select_prev_match(action, cx); - }); - register_action(workspace, |this, action: &SelectAllMatches, cx| { - this.select_all_matches(action, cx); - }); - register_action(workspace, |this, _: &editor::Cancel, cx| { + register_action( + editor, + handle.clone(), + |this, action: &SelectNextMatch, cx| { + this.select_next_match(action, cx); + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &SelectPrevMatch, cx| { + this.select_prev_match(action, cx); + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &SelectAllMatches, cx| { + this.select_all_matches(action, cx); + }, + ); + register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| { if !this.dismissed { this.dismiss(&Dismiss, cx); + return; } + cx.propagate(); }); } pub fn new(cx: &mut ViewContext) -> Self { @@ -543,8 +587,7 @@ impl BufferSearchBar { // let style = theme.search.action_button.clone(); - IconButton::new(0, ui::Icon::SelectAll) - .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches))) + IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches)) } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 118d9054e677b3f694c45de6a4f19f6c8d919235..65a4ddfd422abc5700a11a90fd15391e82d8ea78 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -3,7 +3,8 @@ pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext, IntoElement}; pub use mode::SearchMode; use project::search::SearchQuery; -use ui::ButtonVariant; +use ui::prelude::*; +use ui::{ButtonStyle2, Icon, IconButton}; //pub use project_search::{ProjectSearchBar, ProjectSearchView}; // use theme::components::{ // action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, @@ -83,35 +84,35 @@ impl SearchOptions { } pub fn as_button(&self, active: bool) -> impl IntoElement { - ui::IconButton::new(0, self.icon()) + IconButton::new(0, self.icon()) .on_click({ let action = self.to_toggle_action(); move |_, cx| { cx.dispatch_action(action.boxed_clone()); } }) - .variant(ui::ButtonVariant::Ghost) - .when(active, |button| button.variant(ButtonVariant::Filled)) + .style(ButtonStyle2::Subtle) + .when(active, |button| button.style(ButtonStyle2::Filled)) } } fn toggle_replace_button(active: bool) -> impl IntoElement { // todo: add toggle_replace button - ui::IconButton::new(0, ui::Icon::Replace) + IconButton::new(0, Icon::Replace) .on_click(|_, cx| { cx.dispatch_action(Box::new(ToggleReplace)); cx.notify(); }) - .variant(ui::ButtonVariant::Ghost) - .when(active, |button| button.variant(ButtonVariant::Filled)) + .style(ButtonStyle2::Subtle) + .when(active, |button| button.style(ButtonStyle2::Filled)) } fn render_replace_button( action: impl Action + 'static + Send + Sync, - icon: ui::Icon, + icon: Icon, ) -> impl IntoElement { // todo: add tooltip - ui::IconButton::new(0, icon).on_click(move |_, cx| { + IconButton::new(0, icon).on_click(move |_, cx| { cx.dispatch_action(action.boxed_clone()); }) } diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 1a7456f41c462626e5d6bb78e9e4749735e067e6..44ba287d78820db152d9c639e6e7082888ac28d3 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,12 +1,13 @@ -use gpui::{IntoElement, MouseDownEvent, WindowContext}; -use ui::{Button, ButtonVariant, IconButton}; +use gpui::{ClickEvent, IntoElement, WindowContext}; +use ui::prelude::*; +use ui::{Button, IconButton}; use crate::mode::SearchMode; pub(super) fn render_nav_button( icon: ui::Icon, _active: bool, - on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> impl IntoElement { // let tooltip_style = cx.theme().tooltip.clone(); // let cursor_style = if active { @@ -21,15 +22,9 @@ pub(super) fn render_nav_button( pub(crate) fn render_search_mode_button( mode: SearchMode, is_active: bool, - on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Button { - let button_variant = if is_active { - ButtonVariant::Filled - } else { - ButtonVariant::Ghost - }; - - Button::new(mode.label()) + Button::new(mode.label(), mode.label()) + .selected(is_active) .on_click(on_click) - .variant(button_variant) } diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 2145d1f9e0a2300adece9db33946cbc19ac88381..f4e2c5ea13bcec498d69c14cc6dbab40d775e6b7 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc { target: (identifier) @name) operator: "when") ]) - (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item ) (call target: (identifier) @name (arguments (alias) @name) - (#match? @name "^(defmodule|defprotocol)$")) @item + (#any-match? @name "^(defmodule|defprotocol)$")) @item "#, ) .unwrap(), diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 6f757240ebe21df85f12e0958946f34ab06797e0..7b375b10e36a838f4a4629d087247c8e564721bc 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -2,7 +2,7 @@ use gpui::{ actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View, WindowContext, }; -use theme2::ActiveTheme; +use ui::prelude::*; actions!(ActionA, ActionB, ActionC); @@ -33,7 +33,6 @@ impl Render for FocusStory { let theme = cx.theme(); let color_1 = theme.status().created; let color_2 = theme.status().modified; - let color_3 = theme.status().deleted; let color_4 = theme.status().conflict; let color_5 = theme.status().ignored; let color_6 = theme.status().renamed; @@ -42,10 +41,10 @@ impl Render for FocusStory { .id("parent") .focusable() .key_context("parent") - .on_action(cx.listener(|_, action: &ActionA, cx| { + .on_action(cx.listener(|_, _action: &ActionA, _cx| { println!("Action A dispatched on parent"); })) - .on_action(cx.listener(|_, action: &ActionB, cx| { + .on_action(cx.listener(|_, _action: &ActionB, _cx| { println!("Action B dispatched on parent"); })) .on_focus(cx.listener(|_, _, _| println!("Parent focused"))) @@ -61,7 +60,7 @@ impl Render for FocusStory { div() .track_focus(&self.child_1_focus) .key_context("child-1") - .on_action(cx.listener(|_, action: &ActionB, cx| { + .on_action(cx.listener(|_, _action: &ActionB, _cx| { println!("Action B dispatched on child 1 during"); })) .w_full() @@ -83,7 +82,7 @@ impl Render for FocusStory { div() .track_focus(&self.child_2_focus) .key_context("child-2") - .on_action(cx.listener(|_, action: &ActionC, cx| { + .on_action(cx.listener(|_, _action: &ActionC, _cx| { println!("Action C dispatched on child 2"); })) .w_full() diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index f79a27aa89f875ae10cfdd12fd15b13192459a54..271285cc2f56263593f2ed0a7691bd5ca649b5e3 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -9,7 +9,7 @@ pub struct KitchenSinkStory; impl KitchenSinkStory { pub fn view(cx: &mut WindowContext) -> View { - cx.build_view(|cx| Self) + cx.build_view(|_cx| Self) } } diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index ae6a26161bd3a587c54aa7231336d40428dbbe69..75aa7aed055c16e2303dc1b76870ba60c2aa7ab0 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -4,7 +4,8 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use theme2::ActiveTheme; +use ui::prelude::*; +use ui::{Label, ListItem}; pub struct PickerStory { picker: View>, @@ -36,7 +37,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div; + type ListItem = ListItem; fn match_count(&self) -> usize { self.candidates.len() @@ -50,26 +51,20 @@ impl PickerDelegate for Delegate { &self, ix: usize, selected: bool, - cx: &mut gpui::ViewContext>, - ) -> Self::ListItem { - let colors = cx.theme().colors(); + _cx: &mut gpui::ViewContext>, + ) -> Option { let Some(candidate_ix) = self.matches.get(ix) else { - return div(); + return None; }; // TASK: Make StringMatchCandidate::string a SharedString let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); - div() - .text_color(colors.text) - .when(selected, |s| { - s.border_l_10().border_color(colors.terminal_ansi_yellow) - }) - .hover(|style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .child(candidate) + Some( + ListItem::new(ix) + .inset(true) + .selected(selected) + .child(Label::new(candidate)), + ) } fn selected_index(&self) -> usize { @@ -81,7 +76,7 @@ impl PickerDelegate for Delegate { cx.notify(); } - fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext>) { + fn confirm(&mut self, secondary: bool, _cx: &mut gpui::ViewContext>) { let candidate_ix = self.matches[self.selected_ix]; let candidate = self.candidates[candidate_ix].string.clone(); diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index 9b9a54e1e6736e05183654c51f4fff7ccc69b968..300aae1144b1c2756c10b7d5a0a697c5448d8784 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,12 +1,12 @@ use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; -use theme2::ActiveTheme; +use ui::prelude::*; use ui::Tooltip; pub struct ScrollStory; impl ScrollStory { pub fn view(cx: &mut WindowContext) -> View { - cx.build_view(|cx| ScrollStory) + cx.build_view(|_cx| ScrollStory) } } diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index 71502087c2c00e41bc544600788814a3e0ee1c1c..ccd13cb4d80b1e739cef0671a3bfd54b78264fea 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -8,7 +8,7 @@ pub struct TextStory; impl TextStory { pub fn view(cx: &mut WindowContext) -> View { - cx.build_view(|cx| Self) + cx.build_view(|_cx| Self) } } @@ -66,7 +66,7 @@ impl Render for TextStory { }), ]), ) - .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| { + .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { println!("Clicked range {range_ix}"); }) ) diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 9d04d3d81ff3a167b015efb494e78cb0754d7202..c6a4b68cc369f4e0bf409f714e585c2bde6f47f4 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -9,7 +9,7 @@ pub struct ZIndexStory; impl Render for ZIndexStory { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { Story::container().child(Story::title("z-index")).child( div() .flex() @@ -84,7 +84,7 @@ struct ZIndexExample { impl RenderOnce for ZIndexExample { type Rendered = Div; - fn render(self, cx: &mut WindowContext) -> Self::Rendered { + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { div() .relative() .size_full() diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 71d7f80a8e44c90a32dd9b2b550c2b8d858f706d..0354097c0b19b525662efbd630a4dd33c15515da 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -8,7 +8,6 @@ use clap::ValueEnum; use gpui::{AnyView, VisualContext}; use strum::{EnumIter, EnumString, IntoEnumIterator}; use ui::prelude::*; -use ui::{AvatarStory, ButtonStory, IconStory, InputStory, LabelStory}; #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] @@ -17,11 +16,14 @@ pub enum ComponentStory { Button, Checkbox, ContextMenu, + Disclosure, Focus, Icon, - Input, + IconButton, Keybinding, Label, + List, + ListItem, Scroll, Text, ZIndex, @@ -31,15 +33,18 @@ pub enum ComponentStory { impl ComponentStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { - Self::Avatar => cx.build_view(|_| AvatarStory).into(), - Self::Button => cx.build_view(|_| ButtonStory).into(), + Self::Avatar => cx.build_view(|_| ui::AvatarStory).into(), + Self::Button => cx.build_view(|_| ui::ButtonStory).into(), Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(), Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(), + Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(), Self::Focus => FocusStory::view(cx).into(), - Self::Icon => cx.build_view(|_| IconStory).into(), - Self::Input => cx.build_view(|_| InputStory).into(), + Self::Icon => cx.build_view(|_| ui::IconStory).into(), + Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(), Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), - Self::Label => cx.build_view(|_| LabelStory).into(), + Self::Label => cx.build_view(|_| ui::LabelStory).into(), + Self::List => cx.build_view(|_| ui::ListStory).into(), + Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(), Self::Scroll => ScrollStory::view(cx).into(), Self::Text => TextStory::view(cx).into(), Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 2a62c135b186c85910f497b33b82bbcd46739768..e1bb4ef3f46255381f919f789c9ec832911b30af 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -1,5 +1,3 @@ -#![allow(dead_code, unused_variables)] - mod assets; mod stories; mod story_selector; @@ -70,7 +68,7 @@ fn main() { language::init(cx); editor::init(cx); - let window = cx.open_window( + let _window = cx.open_window( WindowOptions { bounds: WindowBounds::Fixed(Bounds { origin: Default::default(), @@ -104,7 +102,7 @@ impl StoryWrapper { impl Render for StoryWrapper { type Element = Div; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { div() .flex() .flex_col() diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 9f3ed313880989631c25b7cb7f9fe1460c9224c3..b007d58c34bcb2163f42bd2b88e1979a18152f56 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -298,9 +298,12 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - self.context_menu = Some(ContextMenu::build(cx, |menu, _| { - menu.action("Clear", Box::new(Clear)) - .action("Close", Box::new(CloseActiveItem { save_intent: None })) + self.context_menu = Some(ContextMenu::build(cx, |menu, cx| { + menu.action("Clear", Box::new(Clear), cx).action( + "Close", + Box::new(CloseActiveItem { save_intent: None }), + cx, + ) })); dbg!(&position); // todo!() diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 91efecbfb310103deeafb683becf31b9b7732c42..4a47bc05366c5c8a9063e1b4f8fd4be560b11195 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -23,15 +23,15 @@ impl ThemeColors { surface_background: neutral().light().step_2(), background: neutral().light().step_1(), element_background: neutral().light().step_3(), - element_hover: neutral().light().step_4(), - element_active: neutral().light().step_5(), - element_selected: neutral().light().step_5(), + element_hover: neutral().light_alpha().step_4(), + element_active: neutral().light_alpha().step_5(), + element_selected: neutral().light_alpha().step_5(), element_disabled: neutral().light_alpha().step_3(), drop_target_background: blue().light_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().light().step_4(), - ghost_element_active: neutral().light().step_5(), - ghost_element_selected: neutral().light().step_5(), + ghost_element_hover: neutral().light_alpha().step_4(), + ghost_element_active: neutral().light_alpha().step_5(), + ghost_element_selected: neutral().light_alpha().step_5(), ghost_element_disabled: neutral().light_alpha().step_3(), text: yellow().light().step_9(), text_muted: neutral().light().step_11(), @@ -95,15 +95,15 @@ impl ThemeColors { surface_background: neutral().dark().step_2(), background: neutral().dark().step_1(), element_background: neutral().dark().step_3(), - element_hover: neutral().dark().step_4(), - element_active: neutral().dark().step_5(), - element_selected: neutral().dark().step_5(), + element_hover: neutral().dark_alpha().step_4(), + element_active: neutral().dark_alpha().step_5(), + element_selected: neutral().dark_alpha().step_5(), element_disabled: neutral().dark_alpha().step_3(), drop_target_background: blue().dark_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().dark().step_4(), - ghost_element_active: neutral().dark().step_5(), - ghost_element_selected: neutral().dark().step_5(), + ghost_element_hover: neutral().dark_alpha().step_4(), + ghost_element_active: neutral().dark_alpha().step_5(), + ghost_element_selected: neutral().dark_alpha().step_5(), ghost_element_disabled: neutral().dark_alpha().step_3(), text: neutral().dark().step_12(), text_muted: neutral().dark().step_11(), diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index 2802bd17b51aafebf392278bc3e8d646e304d7ca..2f663618a686c841ec13c4cc083ae44619bfd19c 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -20,7 +20,7 @@ pub fn one_family() -> ThemeFamily { pub(crate) fn one_dark() -> Theme { let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); - let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.); let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); @@ -48,7 +48,7 @@ pub(crate) fn one_dark() -> Theme { elevated_surface_background: elevated_surface, surface_background: bg, background: bg, - element_background: elevated_surface, + element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0), element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 919dd1b1099ecb35853142a2b5a66f14904a7653..b50eb831dda51b8357ad2b8c8ff9a7b6a86cfe81 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -86,6 +86,10 @@ impl ThemeRegistry { })); } + pub fn clear(&mut self) { + self.themes.clear(); + } + pub fn list_names(&self, _staff: bool) -> impl Iterator + '_ { self.themes.keys().cloned() } diff --git a/crates/theme2/src/styles/stories/players.rs b/crates/theme2/src/styles/stories/players.rs index d189d3bfb07bb0efc4691ca14abac4d94948d620..237f2f10817872ea07c0f7fafa5745ee8722cf13 100644 --- a/crates/theme2/src/styles/stories/players.rs +++ b/crates/theme2/src/styles/stories/players.rs @@ -55,9 +55,8 @@ impl Render for PlayerStory { .border_2() .border_color(player.cursor) .child( - img() + img("https://avatars.githubusercontent.com/u/1714999?v=4") .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") .size_6() .bg(gpui::red()), ) @@ -67,51 +66,62 @@ impl Render for PlayerStory { .child(div().flex().gap_1().children( cx.theme().players().0.clone().iter_mut().map(|player| { div() - .my_1() - .rounded_xl() - .flex() - .items_center() - .h_8() - .py_0p5() - .px_1p5() - .bg(player.background) - .child( - div().relative().neg_mx_1().rounded_full().z_index(3) + .my_1() + .rounded_xl() + .flex() + .items_center() + .h_8() + .py_0p5() + .px_1p5() + .bg(player.background) + .child( + div() + .relative() + .neg_mx_1() + .rounded_full() + .z_index(3) .border_2() .border_color(player.background) .size(px(28.)) .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ).child( - div().relative().neg_mx_1().rounded_full().z_index(2) - .border_2() - .border_color(player.background) - .size(px(28.)) - .child( - img() + img("https://avatars.githubusercontent.com/u/1714999?v=4") + .rounded_full() + .size(px(24.)) + .bg(gpui::red()), + ), + ) + .child( + div() + .relative() + .neg_mx_1() .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ).child( - div().relative().neg_mx_1().rounded_full().z_index(1) - .border_2() - .border_color(player.background) - .size(px(28.)) + .z_index(2) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img("https://avatars.githubusercontent.com/u/1714999?v=4") + .rounded_full() + .size(px(24.)) + .bg(gpui::red()), + ), + ) .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ) + div() + .relative() + .neg_mx_1() + .rounded_full() + .z_index(1) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img("https://avatars.githubusercontent.com/u/1714999?v=4") + .rounded_full() + .size(px(24.)) + .bg(gpui::red()), + ), + ) }), )) .child(Story::label("Player Selections")) diff --git a/crates/theme_selector2/Cargo.toml b/crates/theme_selector2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..853a53af68f25aa0ff12f4cdd7c232b65e3828c8 --- /dev/null +++ b/crates/theme_selector2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "theme_selector2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/theme_selector.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +fs = { package = "fs2", path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +picker = { package = "picker2", path = "../picker2" } +theme = { package = "theme2", path = "../theme2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..7b0a0c3d3ae72c646bf5590a29a272528f312279 --- /dev/null +++ b/crates/theme_selector2/src/theme_selector.rs @@ -0,0 +1,276 @@ +use feature_flags::FeatureFlagAppExt; +use fs::Fs; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, + SharedString, View, ViewContext, VisualContext, WeakView, +}; +use picker::{Picker, PickerDelegate}; +use settings::{update_settings_file, SettingsStore}; +use std::sync::Arc; +use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings}; +use ui::ListItem; +use util::ResultExt; +use workspace::{ui::HighlightedLabel, Workspace}; + +actions!(Toggle, Reload); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(toggle); + }, + ) + .detach(); +} + +pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + let fs = workspace.app_state().fs.clone(); + workspace.toggle_modal(cx, |cx| { + ThemeSelector::new( + ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx), + cx, + ) + }); +} + +#[cfg(debug_assertions)] +pub fn reload(cx: &mut AppContext) { + let current_theme_name = cx.theme().name.clone(); + let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| { + registry.clear(); + registry.get(¤t_theme_name) + }); + match current_theme { + Ok(theme) => { + ThemeSelectorDelegate::set_theme(theme, cx); + log::info!("reloaded theme {}", current_theme_name); + } + Err(error) => { + log::error!("failed to load theme {}: {:?}", current_theme_name, error) + } + } +} + +pub struct ThemeSelector { + picker: View>, +} + +impl EventEmitter for ThemeSelector {} + +impl FocusableView for ThemeSelector { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for ThemeSelector { + type Element = View>; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.picker.clone() + } +} + +impl ThemeSelector { + pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext) -> Self { + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +pub struct ThemeSelectorDelegate { + fs: Arc, + theme_names: Vec, + matches: Vec, + original_theme: Arc, + selection_completed: bool, + selected_index: usize, + view: WeakView, +} + +impl ThemeSelectorDelegate { + fn new( + weak_view: WeakView, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let original_theme = cx.theme().clone(); + + let staff_mode = cx.is_staff(); + let registry = cx.global::>(); + let theme_names = registry.list(staff_mode).collect::>(); + //todo!(theme sorting) + // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); + let matches = theme_names + .iter() + .map(|meta| StringMatch { + candidate_id: 0, + score: 0.0, + positions: Default::default(), + string: meta.to_string(), + }) + .collect(); + let mut this = Self { + fs, + theme_names, + matches, + original_theme: original_theme.clone(), + selected_index: 0, + selection_completed: false, + view: weak_view, + }; + this.select_if_matching(&original_theme.name); + this + } + + fn show_selected_theme(&mut self, cx: &mut ViewContext>) { + if let Some(mat) = self.matches.get(self.selected_index) { + let registry = cx.global::>(); + match registry.get(&mat.string) { + Ok(theme) => { + Self::set_theme(theme, cx); + } + Err(error) => { + log::error!("error loading theme {}: {}", mat.string, error) + } + } + } + } + + fn select_if_matching(&mut self, theme_name: &str) { + self.selected_index = self + .matches + .iter() + .position(|mat| mat.string == theme_name) + .unwrap_or(self.selected_index); + } + + fn set_theme(theme: Arc, cx: &mut AppContext) { + cx.update_global(|store: &mut SettingsStore, cx| { + let mut theme_settings = store.get::(None).clone(); + theme_settings.active_theme = theme; + store.override_global(theme_settings); + cx.refresh(); + }); + } +} + +impl PickerDelegate for ThemeSelectorDelegate { + type ListItem = ui::ListItem; + + fn placeholder_text(&self) -> Arc { + "Select Theme...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + self.selection_completed = true; + + let theme_name = cx.theme().name.clone(); + update_settings_file::(self.fs.clone(), cx, move |settings| { + settings.theme = Some(theme_name.to_string()); + }); + + self.view + .update(cx, |_, cx| { + cx.emit(DismissEvent); + }) + .ok(); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + if !self.selection_completed { + Self::set_theme(self.original_theme.clone(), cx); + self.selection_completed = true; + } + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + cx: &mut ViewContext>, + ) { + self.selected_index = ix; + self.show_selected_theme(cx); + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let background = cx.background_executor().clone(); + let candidates = self + .theme_names + .iter() + .enumerate() + .map(|(id, meta)| StringMatchCandidate { + id, + char_bag: meta.as_ref().into(), + string: meta.to_string(), + }) + .collect::>(); + + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate + .selected_index + .min(this.delegate.matches.len().saturating_sub(1)); + this.delegate.show_selected_theme(cx); + }) + .log_err(); + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + let theme_match = &self.matches[ix]; + + Some( + ListItem::new(ix) + .inset(true) + .selected(selected) + .child(HighlightedLabel::new( + theme_match.string.clone(), + theme_match.positions.clone(), + )), + ) + } +} diff --git a/crates/ui2/Cargo.toml b/crates/ui2/Cargo.toml index 9f98b92296ca40cfd2bd8fdf3eff524937ce7104..6c2b750006253ee7ac13aae6ceac4d4919717078 100644 --- a/crates/ui2/Cargo.toml +++ b/crates/ui2/Cargo.toml @@ -15,11 +15,11 @@ gpui = { package = "gpui2", path = "../gpui2" } itertools = { version = "0.11.0", optional = true } menu = { package = "menu2", path = "../menu2"} serde.workspace = true -settings2 = { path = "../settings2" } +settings = { package = "settings2", path = "../settings2" } smallvec.workspace = true story = { path = "../story", optional = true } strum = { version = "0.25.0", features = ["derive"] } -theme2 = { path = "../theme2" } +theme = { package = "theme2", path = "../theme2" } rand = "0.8" [features] diff --git a/crates/ui2/src/clickable.rs b/crates/ui2/src/clickable.rs new file mode 100644 index 0000000000000000000000000000000000000000..44f40b4cd4ccb98244a369b70639e3a768f4ddfa --- /dev/null +++ b/crates/ui2/src/clickable.rs @@ -0,0 +1,7 @@ +use gpui::{ClickEvent, WindowContext}; + +/// A trait for elements that can be clicked. +pub trait Clickable { + /// Sets the click handler that will fire whenever the element is clicked. + fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self; +} diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index c467576f4af0d11454830be704cd81fd1dd3ef19..be95fc1fab6cc66edaef96f1a74622a01457e0f1 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -5,15 +5,11 @@ mod context_menu; mod disclosure; mod divider; mod icon; -mod icon_button; -mod input; mod keybinding; mod label; mod list; mod popover; -mod slot; mod stack; -mod toggle; mod tooltip; #[cfg(feature = "stories")] @@ -26,15 +22,11 @@ pub use context_menu::*; pub use disclosure::*; pub use divider::*; pub use icon::*; -pub use icon_button::*; -pub use input::*; pub use keybinding::*; pub use label::*; pub use list::*; pub use popover::*; -pub use slot::*; pub use stack::*; -pub use toggle::*; pub use tooltip::*; #[cfg(feature = "stories")] diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 976243365cc7e608e4160629a66efb5733f1e732..3c8eefa6db2fad6551976949399204b0cbcc0e55 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,5 +1,7 @@ +use std::sync::Arc; + use crate::prelude::*; -use gpui::{img, Img, IntoElement}; +use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Shape { @@ -10,15 +12,16 @@ pub enum Shape { #[derive(IntoElement)] pub struct Avatar { - src: SharedString, + src: ImageSource, + is_available: Option, shape: Shape, } impl RenderOnce for Avatar { - type Rendered = Img; + type Rendered = Div; - fn render(self, _: &mut WindowContext) -> Self::Rendered { - let mut img = img(); + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + let mut img = img(self.src); if self.shape == Shape::Circle { img = img.rounded_full(); @@ -26,23 +29,60 @@ impl RenderOnce for Avatar { img = img.rounded_md(); } - img.uri(self.src.clone()) - .size_4() - // todo!(Pull the avatar fallback background from the theme.) - .bg(gpui::red()) + let size = rems(1.0); + + div() + .size(size) + .child( + img.size(size) + // todo!(Pull the avatar fallback background from the theme.) + .bg(gpui::red()), + ) + .children(self.is_available.map(|is_free| { + // HACK: non-integer sizes result in oval indicators. + let indicator_size = (size.0 * cx.rem_size() * 0.4).round(); + + div() + .absolute() + .z_index(1) + .bg(if is_free { gpui::green() } else { gpui::red() }) + .size(indicator_size) + .rounded(indicator_size) + .bottom_0() + .right_0() + })) } } impl Avatar { - pub fn new(src: impl Into) -> Self { + pub fn uri(src: impl Into) -> Self { + Self { + src: src.into().into(), + shape: Shape::Circle, + is_available: None, + } + } + pub fn data(src: Arc) -> Self { Self { src: src.into(), shape: Shape::Circle, + is_available: None, } } + pub fn source(src: ImageSource) -> Self { + Self { + src, + shape: Shape::Circle, + is_available: None, + } + } pub fn shape(mut self, shape: Shape) -> Self { self.shape = shape; self } + pub fn availability_indicator(mut self, is_available: impl Into>) -> Self { + self.is_available = is_available.into(); + self + } } diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs deleted file mode 100644 index 02902a4b64800e22128d89c5048c1aefc03c5867..0000000000000000000000000000000000000000 --- a/crates/ui2/src/components/button.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::rc::Rc; - -use gpui::{ - DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent, - StatefulInteractiveElement, WindowContext, -}; - -use crate::prelude::*; -use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; - -/// Provides the flexibility to use either a standard -/// button or an icon button in a given context. -pub enum ButtonOrIconButton { - Button(Button), - IconButton(IconButton), -} - -impl From