diff --git a/Cargo.lock b/Cargo.lock index 10a945c52a..5ec386837b 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" @@ -9471,6 +9490,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" @@ -11054,6 +11094,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" @@ -11508,7 +11573,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.115.0" +version = "0.116.0" dependencies = [ "activity_indicator", "ai", @@ -11651,6 +11716,7 @@ dependencies = [ name = "zed2" version = "0.109.0" dependencies = [ + "activity_indicator2", "ai2", "anyhow", "async-compression", @@ -11661,6 +11727,7 @@ dependencies = [ "auto_update2", "backtrace", "call2", + "channel2", "chrono", "cli", "client2", @@ -11719,6 +11786,7 @@ dependencies = [ "terminal_view2", "text2", "theme2", + "theme_selector2", "thiserror", "tiny_http", "toml 0.5.11", @@ -11756,6 +11824,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "welcome2", "workspace2", "zed_actions2", ] diff --git a/Cargo.toml b/Cargo.toml index 1f6a291a26..674bfbd606 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", @@ -107,6 +108,7 @@ members = [ "crates/theme2", "crates/theme_importer", "crates/theme_selector", + "crates/theme_selector2", "crates/ui2", "crates/util", "crates/semantic_index", @@ -115,6 +117,7 @@ members = [ "crates/vcs_menu", "crates/workspace2", "crates/welcome", + "crates/welcome2", "crates/xtask", "crates/zed", "crates/zed2", diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg new file mode 100644 index 0000000000..7d78497e6d --- /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 186c9c7457..57ee750490 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 7bae7f4801..7a5b1174eb 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 0000000000..81dfee8042 --- /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 0000000000..d38389aea4 --- /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 0000000000..94189dc07d --- /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 0000000000..9d54a6f34b --- /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 0000000000..683519c306 --- /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 0000000000..0232114777 --- /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 0000000000..400869d2fd --- /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 0000000000..1ee5a6689a --- /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/update_notification.rs b/crates/auto_update2/src/update_notification.rs index d15d82e112..4a2efcf807 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -51,6 +51,6 @@ impl UpdateNotification { } pub fn dismiss(&mut self, cx: &mut ViewContext) { - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 9a89ec7167..df7dd847cf 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -14,8 +14,8 @@ use client::{ use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, - View, ViewContext, VisualContext, WeakModel, WeakView, + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel, + Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle, }; pub use participant::ParticipantLocation; use postage::watch; @@ -334,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)); } } @@ -660,9 +703,12 @@ impl CallHandler for Call { self.active_call.as_ref().map(|call| { call.0.update(cx, |this, cx| { this.room().map(|room| { - room.update(cx, |this, cx| { - this.toggle_mute(cx).log_err(); + 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); }) }) }); diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index b55d3348dc..694966abe9 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -1,4 +1,7 @@ -use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; +use crate::{ + call_settings::CallSettings, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, +}; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ @@ -18,6 +21,7 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; +use settings::Settings as _; use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -328,10 +332,8 @@ impl Room { } } - pub fn mute_on_join(_cx: &AppContext) -> bool { - // todo!() po: This should be uncommented, though then unmuting does not work - false - //CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + pub fn mute_on_join(cx: &AppContext) -> bool { + CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( @@ -1265,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/client2/src/client2.rs b/crates/client2/src/client2.rs index b31451aa87..4746c9c6e4 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -693,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)] @@ -725,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() { @@ -1324,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/collab2/src/tests/channel_tests.rs b/crates/collab2/src/tests/channel_tests.rs index 8ce5d99b80..43d18ee7d1 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 f2a39f3511..e579c384e3 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) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index e9c17a6589..b62056a3be 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,33 +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 client::{Client, Contact, UserStore}; +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, Model, ParentElement, Render, Styled, 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 ui::{h_stack, Avatar, Label}; -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, _| { @@ -217,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 { @@ -268,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, + 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, - // 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, + 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>, } @@ -335,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, 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, -// } - -// 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 - // }); + 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, _, 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(); + 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.add_view(|cx| { - // Editor::single_line( - // Some(Arc::new(|theme| { - // theme.collab_panel.user_query_editor.clone() - // })), - // cx, - // ) - // }); + let channel_name_editor = cx.build_view(|cx| Editor::single_line(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(); + 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| { @@ -585,81 +565,80 @@ 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, + 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(), - // 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(), + 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); + 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(); - // }), - // ); + // 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 - // } - // }); - // } - // } - // }, - // )); + 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 }) @@ -689,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(); }); } @@ -700,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 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(), + ); + } - // 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(); + 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 mut scroll_to_top = false; + 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 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 !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 }) - // } + // 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 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 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, - // })); - // } - // } + // // 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(); + let mut request_entries = Vec::new(); - // if cx.has_flag::() { - // self.entries.push(ListEntry::Header(Section::Channels)); + 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 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; - // } - // } + 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]) - // }); + 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, - // }); - // } - // } - // } - // } + 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()) - // })); + // 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); - // } - // } - // } - // } + // 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)); + 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())), - // ); - // } + 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())), - // ); - // } + 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); - // } - // } + 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 contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); - // let (online_contacts, offline_contacts) = matches - // .iter() - // .partition::, _>(|mat| contacts[mat.candidate_id].online); + 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), - // }); - // } - // } - // } - // } - // } + 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 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)) - // } - // }); - // } + 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) + } + }); + } - // let old_scroll_top = self.list_state.logical_scroll_top(); + 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.list_state.reset(self.entries.len()); + self.scroll_handle + .set_logical_scroll_top(new_index, new_offset); + } + } - // 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., - // }) - // }); - - // self.list_state - // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); - // } - // } - - // cx.notify(); - // } + cx.notify(); + } // fn render_call_participant( // user: &User, @@ -1449,399 +1418,16 @@ 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 render_header( - // &self, - // section: Section, - // theme: &theme::Theme, - // 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, - // ) - // }), - // 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, - // ), - // ) - // } - // _ => None, - // }; - - // 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 - // }; - - // 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) - // .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 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 - // }) - // .with_child( - // Label::new(text, header_style.text.clone()) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(button.map(|button| button.aligned().right())) - // .constrained() - // .with_height(theme.collab_panel.row_height) - // .contained() - // .with_style(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 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_contact_placeholder( // &self, @@ -1877,396 +1463,6 @@ impl CollabPanel { // .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, @@ -2459,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 {} + 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 + } + }) + } - // 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), - // ); + 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 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; + 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) + }), + ); + } - // 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), - // ); + 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) + }), + ); - // 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(), - // ); - // } + 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) + }), + ); - // row.constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style( - // *theme - // .contact_row - // .in_state(is_selected) - // .style_for(&mut Default::default()), - // ) - // .into_any() - // } + 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) + }), + ); + } - // 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 - // } - // }) - // } + 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)), + ); + } - // fn deploy_channel_context_menu( - // &mut self, - // position: Option, - // channel: &Channel, - // ix: usize, - // cx: &mut ViewContext, - // ) { - // self.context_menu_on_selected = position.is_none(); + context_menu + }); - // 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()) - // }); + 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.context_menu.update(cx, |context_menu, cx| { - // context_menu.set_position_mode(if self.context_menu_on_selected { - // OverlayPositionMode::Local - // } else { - // OverlayPositionMode::Window - // }); + cx.notify(); + } - // let mut items = Vec::new(); + 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); + } + }); + } - // let select_action_name = if self.selection == Some(ix) { - // "Unselect" - // } else { - // "Select" - // }; + self.update_entries(false, cx); + } - // items.push(ContextMenuItem::action( - // select_action_name, - // ToggleSelectedIx { ix }, - // )); + 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); + } - // 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, - // }, - // )); - // } + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } - // items.push(ContextMenuItem::action( - // "Open Notes", - // OpenChannelNotes { - // channel_id: channel.id, - // }, - // )); + 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); + } - // items.push(ContextMenuItem::action( - // "Open Chat", - // JoinChannelChat { - // channel_id: channel.id, - // }, - // )); + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } - // items.push(ContextMenuItem::action( - // "Copy Channel Link", - // CopyChannelLink { - // channel_id: channel.id, - // }, - // )); + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if self.confirm_channel_edit(cx) { + return; + } - // 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(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()?; - // 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 }, - // )); - // } + 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), + _ => {} + } + } + } + } - // 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 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); + }); + } + } - // context_menu.show( - // position.unwrap_or_default(), - // if self.context_menu_on_selected { - // gpui::elements::AnchorCorner::TopRight - // } else { - // gpui::elements::AnchorCorner::BottomLeft - // }, - // items, - // 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); - // cx.notify(); - // } + *pending_name = Some(channel_name.clone()); - // 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); - // } - // }); - // } + 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.update_entries(false, cx); - // } + 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 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 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); + } - // 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 collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; - // 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 self.is_channel_collapsed(channel_id) { + return; + } - // 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(); - // } + self.toggle_channel_collapsed(channel_id, cx); + } - // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - // if self.confirm_channel_edit(cx) { - // return; - // } + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(id) = self.selected_channel().map(|channel| channel.id) else { + 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, 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()?; + if !self.is_channel_collapsed(id) { + return; + } - // 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 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 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; - // }; - - // if self.is_channel_collapsed(channel_id) { - // return; - // } - - // 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; - // }; - - // 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, @@ -2938,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, @@ -3113,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, @@ -3198,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( @@ -3303,36 +3226,39 @@ impl Render for CollabPanel { type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let contacts = self.contacts(cx).unwrap_or_default(); - let workspace = self._workspace.clone(); - div() + 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) - .children(contacts.into_iter().map(|contact| { - let id = contact.user.id; - h_stack() - .p_2() - .gap_2() - .children( - contact - .user - .avatar - .as_ref() - .map(|avatar| Avatar::data(avatar.clone())), - ) - .child(Label::new(contact.user.github_login.clone())) - .on_mouse_down(gpui::MouseButton::Left, { - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state() - .invite(id, None, cx) - .detach_and_log_err(cx) - }) - .log_err(); - } - }) + .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()) })) } } @@ -3456,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(); } @@ -3487,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) @@ -3605,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 d0c12a7f90..717ab2d897 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), + 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), ) - .with_child( - ChildView::new(&self.picker, cx) - .contained() - .with_style(theme.body), - ) - .constrained() - .with_max_height(theme.max_height) - .with_max_width(theme.max_width) - .contained() - .with_style(theme.modal) - .into_any() + .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 d208eb91f1..d76242afa3 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -37,9 +37,9 @@ use gpui::{ }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; +use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip}; use util::ResultExt; -use workspace::Workspace; +use workspace::{notifications::NotifyResultExt, Workspace}; use crate::face_pile::FacePile; @@ -153,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)), @@ -165,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 @@ -175,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| { @@ -235,7 +238,10 @@ impl Render for CollabTitlebarItem { h_stack() .child( h_stack() - .child(Button::new(if is_shared { "Unshare" } else { "Share" })) + .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| { @@ -288,13 +294,15 @@ impl Render for CollabTitlebarItem { this.child(ui::Avatar::data(avatar)) }) } else { - this.child(Button::new("Sign in").on_click(move |_, cx| { + this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { let client = client.clone(); - cx.spawn(move |cx| async move { - client.authenticate_and_connect(true, &cx).await?; - Ok::<(), anyhow::Error>(()) + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); }) - .detach_and_log_err(cx); + .detach(); })) } }) diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index e235f33ce6..162a3f261a 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -3,8 +3,8 @@ use gpui::{ }; #[derive(Default)] -pub(crate) struct FacePile { - faces: Vec, +pub struct FacePile { + pub faces: Vec, } impl RenderOnce for FacePile { diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index 0519b6fc4a..ce6c8f0f57 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -2,10 +2,11 @@ use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; use futures::StreamExt; use gpui::{ - div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, - StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, 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; @@ -199,14 +200,24 @@ impl IncomingCallNotification { fn render_buttons(&self, cx: &mut ViewContext) -> impl Element { h_stack() - .child(Button::new("Accept").render(cx).bg(green()).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(true, cx) - })) - .child(Button::new("Decline").render(cx).bg(red()).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(false, cx) - })) + .child( + Button::new("accept", "Accept") + .render(cx) + // .bg(green()) + .on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + }), + ) + .child( + Button::new("decline", "Decline") + .render(cx) + // .bg(red()) + .on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) + }), + ) // enum Accept {} // enum Decline {} diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 07b819d3a1..f9b58b1d56 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,17 +1,17 @@ -use collections::{CommandPaletteFilter, HashMap}; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - actions, div, prelude::*, 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 collections::{CommandPaletteFilter, HashMap}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, +}; +use picker::{Picker, PickerDelegate}; + +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -81,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()) } } @@ -141,7 +141,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Execute a command...".into() @@ -269,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); } @@ -294,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/diagnostics2/src/toolbar_controls.rs b/crates/diagnostics2/src/toolbar_controls.rs index e513076ec8..1a604b76c8 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 c8ce37d7e2..99d8a4bb93 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -73,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, }; @@ -99,7 +99,8 @@ 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}, @@ -154,7 +155,6 @@ pub fn render_parsed_markdown( ); let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights); - // 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) { @@ -971,95 +971,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 Some(project) = editor.project.clone() else { + return None; + }; + + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); + + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); + + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + + 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; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = project + .read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }) + .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()); + } + } + })) } - // { - // let settings = EditorSettings::get_global(cx); - // if !settings.show_completion_documentation { - // 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 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(); - - // 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; - // } - - // let server_id = completion.server_id; - // let completion = completion.lsp_completion.clone(); - // drop(completions_guard); - - // Self::resolve_completion_documentation_remote( - // project_id, - // server_id, - // completions.clone(), - // completion_index, - // completion, - // client.clone(), - // language_registry.clone(), - // ) - // .await; - - // _ = this.update(&mut cx, |_, cx| cx.notify()); - // } - // } else { - // for completion_index in completion_indices { - // let completions_guard = completions.read(); - // let completion = &completions_guard[completion_index]; - // if completion.documentation.is_some() { - // continue; - // } - - // let server_id = completion.server_id; - // let completion = completion.lsp_completion.clone(); - // drop(completions_guard); - - // let server = project.read_with(&mut cx, |project, _| { - // project.language_server_for_id(server_id) - // }); - // let Some(server) = server else { - // return; - // }; - - // Self::resolve_completion_documentation_local( - // server, - // completions.clone(), - // completion_index, - // completion, - // language_registry.clone(), - // ) - // .await; - - // _ = this.update(&mut cx, |_, cx| cx.notify()); - // } - // } - // })) - // } fn attempt_resolve_selected_completion_documentation( &mut self, @@ -1080,10 +1079,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(); @@ -1142,41 +1140,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 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; - // }; + 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); - // } + 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 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 mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); } async fn resolve_completion_documentation_local( @@ -1186,38 +1183,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 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; - // }; + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; - // if let Some(lsp_documentation) = completion_item.documentation { - // let documentation = language::prepare_completion_documentation( - // &lsp_documentation, - // &language_registry, - // None, // TODO: Try to reasonably work out which language the completion is for - // ) - // .await; + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(documentation); - // } else { - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(Documentation::Undocumented); - // } + 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 { @@ -4391,7 +4387,7 @@ impl Editor { editor.fold_at(&FoldAt { buffer_row }, cx); } })) - .color(ui::Color::Muted) + .icon_color(ui::Color::Muted) }) }) .flatten() diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 6865e81cfa..e640be8efe 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() -// }; + 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, -// ]))) -// } -// }); + 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, + ]))) + } + }); -// request.next().await; + request.next().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! {"fn main() { let a = 2.Some(2)ˇ; }"}); + 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)ˇ; }"}); -// } + 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)ˇ; }"}); +} -// #[gpui::test] -// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// 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; + 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.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() -// }, -// ]))) -// }); + 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() + }, + ]))) + }); -// cx.set_state(r#"

"#); + cx.set_state(r#"

"#); -// // 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"); -// } -// }); + // 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.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"); -// } -// }); + 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"); -// } -// }); -// } + // 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 6a6afd5461..24402c7e37 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -22,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; @@ -47,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; @@ -316,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; @@ -326,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; @@ -384,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(); @@ -396,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(); @@ -418,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; @@ -457,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() { @@ -490,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 } } @@ -498,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; } @@ -2282,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 { @@ -2293,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(); @@ -2304,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 { @@ -2311,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 { @@ -2323,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 { @@ -2351,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 { @@ -2617,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); diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 37c7df650b..2f2e8ee937 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -15,7 +15,7 @@ use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; -use ui::Tooltip; +use ui::{StyledExt, Tooltip}; use util::TryFutureExt; use workspace::Workspace; @@ -476,16 +476,15 @@ impl InfoPopover { ) -> AnyElement { div() .id("info_popover") + .elevation_2(cx) + .text_ui() + .p_2() .overflow_y_scroll() - .bg(gpui::red()) .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()) - // 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()) .child(crate::render_parsed_markdown( "content", &self.parsed_content, diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index d36762f395..092882573c 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()) - // } - // }); + 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, - // ), - // } + 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 bcf41f135b..6542ace5fb 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/feature_flags2/src/feature_flags2.rs b/crates/feature_flags2/src/feature_flags2.rs index 23167796ec..065d06f96d 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_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index ea578fbb0e..a24da580eb 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, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, - InteractiveElement, IntoElement, 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; @@ -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() @@ -689,9 +687,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - finder - .update(&mut cx, |_, cx| cx.emit(DismissEvent::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(DismissEvent::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)), - ) + ), + ) } } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index c734281d22..5ad95c1f6e 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -90,7 +90,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss), + editor::EditorEvent::Blurred => cx.emit(DismissEvent), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -125,7 +125,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -142,7 +142,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(DismissEvent::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 958eaabdb8..03ef2d2281 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 e8f2a60a6a..94a7d3be0b 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 } => { diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 11420bee69..92ccc118f1 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -325,8 +325,7 @@ impl VisualContext for AsyncWindowContext { where V: crate::ManagedView, { - self.window.update(self, |_, cx| { - view.update(cx, |_, cx| cx.emit(DismissEvent::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 71bc8e3d81..9637720a67 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::DismissEvent::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 b18ffb8ca6..3c8f678b89 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 406f2ea311..ced0a4767c 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/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 1a8084ec0a..764bdfabcd 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -105,7 +105,6 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; - dbg!(limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index a0715b81a9..d57daca062 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -265,7 +265,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 { @@ -284,6 +286,7 @@ impl InteractiveText { element_id: id.into(), text, click_listener: None, + clickable_ranges: Vec::new(), } } @@ -292,7 +295,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) { @@ -300,6 +303,7 @@ impl InteractiveText { } } })); + self.clickable_ranges = ranges; self } } @@ -334,6 +338,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() { @@ -343,6 +360,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 8e61f247bd..2d5a46f3d9 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -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 bc927a17ba..d32c2e849b 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; @@ -1052,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/window.rs b/crates/gpui2/src/window.rs index 1f98860f09..76932f28e4 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -40,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)] @@ -198,9 +198,7 @@ pub trait ManagedView: FocusableView + EventEmitter {} impl> ManagedView for M {} -pub enum DismissEvent { - Dismiss, -} +pub struct DismissEvent; // Holds the state for a specific window. pub struct Window { @@ -244,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>, } @@ -258,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(), } @@ -807,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, @@ -1171,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. @@ -1471,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 =========== @@ -1688,7 +1717,7 @@ impl VisualContext for WindowContext<'_> { where V: ManagedView, { - self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss)) + self.update_view(view, |_, cx| cx.emit(DismissEvent)) } } @@ -1928,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() @@ -2392,7 +2404,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: ManagedView, { - self.defer(|_, cx| cx.emit(DismissEvent::Dismiss)) + self.defer(|_, cx| cx.emit(DismissEvent)) } pub fn listener( @@ -2588,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/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 2621c58120..ecabdeb718 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 dc6b77c7c7..44056dabd1 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 { @@ -114,7 +114,6 @@ impl Picker { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - dbg!("canceling!"); self.delegate.dismissed(cx); } @@ -206,7 +205,6 @@ impl Render for Picker { .when(self.delegate.match_count() > 0, |el| { el.child( v_stack() - .p_1() .grow() .child( uniform_list( @@ -230,7 +228,7 @@ impl Render for Picker { ) }), ) - .child(picker.delegate.render_match( + .children(picker.delegate.render_match( ix, ix == selected_index, cx, @@ -240,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(), @@ -257,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 cb9d32d0b0..0886d68747 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 191431da0b..bf62e538dd 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 a01144ced3..61bcf9c9b3 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 191431da0b..bf62e538dd 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 0000000000..c438f294b6 --- /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 cf3fa547f6..a2ad82585e 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. @@ -2685,8 +2693,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() { @@ -4073,8 +4084,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 +4108,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 +4156,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 +4178,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 +6497,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)?; @@ -8536,446 +8388,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/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs index 94c277db1e..a2de52b21a 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 0000000000..c176c79a91 --- /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 856c280ac0..d2cc4fe406 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. @@ -2722,8 +2730,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() { @@ -4126,7 +4137,8 @@ impl Project { this.buffers_being_formatted .remove(&buffer.read(cx).remote_id()); } - }).ok(); + }) + .ok(); } }); @@ -4138,8 +4150,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 +4174,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 +4222,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 +4244,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 +6570,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)?; @@ -8579,486 +8433,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/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs index 9e9a865f3e..82aebe7913 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 { + 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 this = cx.has_global::().then(|| cx.global::())?; + 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())) - }) - .unwrap_or_else(|| Arc::from("".to_string())) - } - - pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; - - let key = if expanded { - EXPANDED_DIRECTORY_TYPE - } else { - COLLAPSED_DIRECTORY_TYPE - }; - - this.types - .get(key) + 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_chevron_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_CHEVRON_TYPE - } else { - COLLAPSED_CHEVRON_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) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; + + 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 b027209870..dc584d52ff 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, - // }); + 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 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(); - // 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, - // )); - // } + 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_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Rename", Rename)); - // if !is_root { - // menu_entries.push(ContextMenuItem::action("Delete", Delete)); - // } - // } + 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); - // // self.context_menu.update(cx, |menu, cx| { - // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - // // }); + if let Some(clipboard_entry) = self.clipboard_entry { + if clipboard_entry.worktree_id() == worktree_id { + menu = menu.action("Paste", Box::new(Paste), cx); + } + } - // cx.notify(); + 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) { @@ -644,7 +659,6 @@ impl ProjectPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - dbg!("odd"); self.edit_state = None; self.update_visible_entries(None, cx); cx.focus(&self.focus_handle); @@ -1269,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) } } }; @@ -1335,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 @@ -1354,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) @@ -1370,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); @@ -1410,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, @@ -1437,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)) @@ -1480,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 d80d9f5d50..b3d6006113 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -18,7 +18,7 @@ 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, @@ -214,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)) }), @@ -586,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 118d9054e6..65a4ddfd42 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 1a7456f41c..44ba287d78 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/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 6f757240eb..7b375b10e3 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 f79a27aa89..271285cc2f 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 ae6a26161b..75aa7aed05 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 9b9a54e1e6..300aae1144 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 42009136c4..3cb39aa01a 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) } } @@ -68,7 +68,7 @@ impl Render for TextStory { cx.text_style().to_run(18), ]), ) - .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 9d04d3d81f..c6a4b68cc3 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 0c4abf9a13..0354097c0b 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -16,11 +16,13 @@ pub enum ComponentStory { Button, Checkbox, ContextMenu, + Disclosure, Focus, Icon, - Input, + IconButton, Keybinding, Label, + List, ListItem, Scroll, Text, @@ -35,11 +37,13 @@ impl ComponentStory { 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(|_| ui::IconStory).into(), - Self::Input => cx.build_view(|_| ui::InputStory).into(), + Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(), Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).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(), diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 2a62c135b1..e1bb4ef3f4 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 9f3ed31388..b007d58c34 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 91efecbfb3..4a47bc0536 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 2802bd17b5..2f663618a6 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 919dd1b109..b50eb831dd 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/theme_selector2/Cargo.toml b/crates/theme_selector2/Cargo.toml new file mode 100644 index 0000000000..853a53af68 --- /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 0000000000..7b0a0c3d3a --- /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 9f98b92296..6c2b750006 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 0000000000..44f40b4cd4 --- /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 c467576f4a..be95fc1fab 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 f74a7d02ea..3c8eefa6db 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::prelude::*; -use gpui::{img, ImageData, ImageSource, Img, IntoElement}; +use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Shape { @@ -13,13 +13,14 @@ pub enum Shape { #[derive(IntoElement)] pub struct Avatar { src: ImageSource, + is_available: Option, shape: Shape, } impl RenderOnce for Avatar { - type Rendered = Img; + type Rendered = Div; - fn render(self, _: &mut WindowContext) -> Self::Rendered { + fn render(self, cx: &mut WindowContext) -> Self::Rendered { let mut img = img(self.src); if self.shape == Shape::Circle { @@ -28,9 +29,28 @@ impl RenderOnce for Avatar { img = img.rounded_md(); } - img.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() + })) } } @@ -39,17 +59,30 @@ impl Avatar { 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 02902a4b64..0000000000 --- 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