From 663bbb06d9e80d4f61ea5f2ae3598e6c930d04dd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 21 Nov 2023 12:40:00 -0800 Subject: [PATCH 01/33] WIP --- Cargo.lock | 46 +++ Cargo.toml | 2 + crates/client2/src/client2.rs | 8 +- crates/gpui2/src/action.rs | 1 + crates/theme2/src/registry.rs | 4 + crates/theme_selector2/Cargo.toml | 28 ++ crates/theme_selector2/src/theme_selector.rs | 254 ++++++++++++++++ crates/welcome2/Cargo.toml | 36 +++ crates/welcome2/src/base_keymap_picker.rs | 152 ++++++++++ crates/welcome2/src/base_keymap_setting.rs | 65 +++++ crates/welcome2/src/welcome.rs | 287 +++++++++++++++++++ crates/zed2/Cargo.toml | 4 +- crates/zed2/src/main.rs | 83 +++--- script/crate-dep-graph | 2 +- 14 files changed, 921 insertions(+), 51 deletions(-) create mode 100644 crates/theme_selector2/Cargo.toml create mode 100644 crates/theme_selector2/src/theme_selector.rs create mode 100644 crates/welcome2/Cargo.toml create mode 100644 crates/welcome2/src/base_keymap_picker.rs create mode 100644 crates/welcome2/src/base_keymap_setting.rs create mode 100644 crates/welcome2/src/welcome.rs diff --git a/Cargo.lock b/Cargo.lock index 6aa94b08d0..2184ba5ebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9405,6 +9405,26 @@ 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", + "util", + "workspace2", +] + [[package]] name = "thiserror" version = "1.0.48" @@ -10978,6 +10998,30 @@ 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", + "util", + "workspace2", +] + [[package]] name = "which" version = "4.4.2" @@ -11640,6 +11684,7 @@ dependencies = [ "terminal_view2", "text2", "theme2", + "theme_selector2", "thiserror", "tiny_http", "toml 0.5.11", @@ -11676,6 +11721,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "welcome2", "workspace2", "zed_actions2", ] diff --git a/Cargo.toml b/Cargo.toml index d7b9918f62..6c1152cf9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,7 @@ members = [ "crates/theme2", "crates/theme_importer", "crates/theme_selector", + "crates/theme_selector2", "crates/ui2", "crates/util", "crates/semantic_index", @@ -115,6 +116,7 @@ members = [ "crates/vcs_menu", "crates/workspace2", "crates/welcome", + "crates/welcome2", "crates/xtask", "crates/zed", "crates/zed2", diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index b4279b023e..028dec6803 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -694,8 +694,8 @@ impl Client { } } - pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { - read_credentials_from_keychain(cx).await.is_some() + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { + read_credentials_from_keychain(cx).is_some() } #[async_recursion(?Send)] @@ -726,7 +726,7 @@ impl Client { let mut read_from_keychain = false; let mut credentials = self.state.read().credentials.clone(); if credentials.is_none() && try_keychain { - credentials = read_credentials_from_keychain(cx).await; + credentials = read_credentials_from_keychain(cx); read_from_keychain = credentials.is_some(); } if credentials.is_none() { @@ -1325,7 +1325,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/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/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..89b7487a7b --- /dev/null +++ b/crates/theme_selector2/Cargo.toml @@ -0,0 +1,28 @@ +[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" } +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..6e660caf51 --- /dev/null +++ b/crates/theme_selector2/src/theme_selector.rs @@ -0,0 +1,254 @@ +use feature_flags::FeatureFlagAppExt; +use fs::Fs; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, div, AppContext, Div, EventEmitter, FocusableView, Manager, Render, SharedString, + View, ViewContext, VisualContext, +}; +use picker::{Picker, PickerDelegate}; +use settings::{update_settings_file, SettingsStore}; +use std::sync::Arc; +use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings}; +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); + }, + ); +} + +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(fs, cx), cx) + }); +} + +#[cfg(debug_assertions)] +pub fn reload(cx: &mut AppContext) { + let current_theme_name = cx.theme().name.clone(); + let registry = cx.global::>(); + registry.clear(); + match registry.get(¤t_theme_name) { + 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, +} + +impl ThemeSelectorDelegate { + fn new(fs: Arc, cx: &mut ViewContext) -> Self { + let original_theme = cx.theme().clone(); + + let staff_mode = cx.is_staff(); + let registry = cx.global::>(); + let mut theme_names = registry.list(staff_mode).collect::>(); + 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, + }; + this.select_if_matching(&original_theme.meta.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, cx| { + let mut theme_settings = store.get::(None).clone(); + theme_settings.theme = theme; + store.override_global(theme_settings); + cx.refresh_windows(); + }); + } +} + +impl PickerDelegate for ThemeSelectorDelegate { + type ListItem = Div; + + 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().meta.name.clone(); + update_settings_file::(self.fs.clone(), cx, |settings| { + settings.theme = Some(theme_name); + }); + + cx.emit(Manager::Dismiss); + } + + 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().clone(); + let candidates = self + .theme_names + .iter() + .enumerate() + .map(|(id, meta)| StringMatchCandidate { + id, + char_bag: meta.name.as_str().into(), + string: meta.name.clone(), + }) + .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| { + let delegate = this.delegate_mut(); + delegate.matches = matches; + delegate.selected_index = delegate + .selected_index + .min(delegate.matches.len().saturating_sub(1)); + delegate.show_selected_theme(cx); + }) + .log_err(); + }) + } + + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> Self::ListItem { + let theme = cx.theme(); + let colors = theme.colors(); + + let theme_match = &self.matches[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(HighlightedLabel::new( + theme_match.string.clone(), + theme_match.positions.clone(), + )) + } +} diff --git a/crates/welcome2/Cargo.toml b/crates/welcome2/Cargo.toml new file mode 100644 index 0000000000..0a2d2fd781 --- /dev/null +++ b/crates/welcome2/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "welcome2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/welcome.rs" + +[features] +test-support = [] + +[dependencies] +client = { package = "client2", path = "../client2" } +editor = { package = "editor2", path = "../editor2" } +fs = { package = "fs2", path = "../fs2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +db = { package = "db2", path = "../db2" } +install_cli = { package = "install_cli2", path = "../install_cli2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } +util = { path = "../util" } +picker = { package = "picker2", path = "../picker2" } +workspace = { package = "workspace2", path = "../workspace2" } +# vim = { package = "vim2", path = "../vim2" } + +anyhow.workspace = true +log.workspace = true +schemars.workspace = true +serde.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/welcome2/src/base_keymap_picker.rs b/crates/welcome2/src/base_keymap_picker.rs new file mode 100644 index 0000000000..021e3b86a0 --- /dev/null +++ b/crates/welcome2/src/base_keymap_picker.rs @@ -0,0 +1,152 @@ +use super::base_keymap_setting::BaseKeymap; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, + elements::{Element as _, Label}, + AppContext, Task, ViewContext, +}; +use picker::{Picker, PickerDelegate, PickerEvent}; +use project::Fs; +use settings::update_settings_file; +use std::sync::Arc; +use util::ResultExt; +use workspace::Workspace; + +actions!(welcome, [ToggleBaseKeymapSelector]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(toggle); + BaseKeymapSelector::init(cx); +} + +pub fn toggle( + workspace: &mut Workspace, + _: &ToggleBaseKeymapSelector, + cx: &mut ViewContext, +) { + workspace.toggle_modal(cx, |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx)) + }); +} + +pub type BaseKeymapSelector = Picker; + +pub struct BaseKeymapSelectorDelegate { + matches: Vec, + selected_index: usize, + fs: Arc, +} + +impl BaseKeymapSelectorDelegate { + fn new(fs: Arc, cx: &mut ViewContext) -> Self { + let base = settings::get::(cx); + let selected_index = BaseKeymap::OPTIONS + .iter() + .position(|(_, value)| value == base) + .unwrap_or(0); + Self { + matches: Vec::new(), + selected_index, + fs, + } + } +} + +impl PickerDelegate for BaseKeymapSelectorDelegate { + fn placeholder_text(&self) -> Arc { + "Select a base keymap...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { + self.selected_index = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext, + ) -> Task<()> { + let background = cx.background().clone(); + let candidates = BaseKeymap::names() + .enumerate() + .map(|(id, name)| StringMatchCandidate { + id, + char_bag: name.into(), + string: name.into(), + }) + .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, _| { + let delegate = this.delegate_mut(); + delegate.matches = matches; + delegate.selected_index = delegate + .selected_index + .min(delegate.matches.len().saturating_sub(1)); + }) + .log_err(); + }) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext) { + if let Some(selection) = self.matches.get(self.selected_index) { + let base_keymap = BaseKeymap::from_names(&selection.string); + update_settings_file::(self.fs.clone(), cx, move |setting| { + *setting = Some(base_keymap) + }); + } + cx.emit(PickerEvent::Dismiss); + } + + fn dismissed(&mut self, _cx: &mut ViewContext) {} + + fn render_match( + &self, + ix: usize, + mouse_state: &mut gpui::MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> gpui::AnyElement> { + let theme = &theme::current(cx); + let keymap_match = &self.matches[ix]; + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + + Label::new(keymap_match.string.clone(), style.label.clone()) + .with_highlights(keymap_match.positions.clone()) + .contained() + .with_style(style.container) + .into_any() + } +} diff --git a/crates/welcome2/src/base_keymap_setting.rs b/crates/welcome2/src/base_keymap_setting.rs new file mode 100644 index 0000000000..c5b6171f9b --- /dev/null +++ b/crates/welcome2/src/base_keymap_setting.rs @@ -0,0 +1,65 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum BaseKeymap { + #[default] + VSCode, + JetBrains, + SublimeText, + Atom, + TextMate, +} + +impl BaseKeymap { + pub const OPTIONS: [(&'static str, Self); 5] = [ + ("VSCode (Default)", Self::VSCode), + ("Atom", Self::Atom), + ("JetBrains", Self::JetBrains), + ("Sublime Text", Self::SublimeText), + ("TextMate", Self::TextMate), + ]; + + pub fn asset_path(&self) -> Option<&'static str> { + match self { + BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), + BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), + BaseKeymap::Atom => Some("keymaps/atom.json"), + BaseKeymap::TextMate => Some("keymaps/textmate.json"), + BaseKeymap::VSCode => None, + } + } + + pub fn names() -> impl Iterator { + Self::OPTIONS.iter().map(|(name, _)| *name) + } + + pub fn from_names(option: &str) -> BaseKeymap { + Self::OPTIONS + .iter() + .copied() + .find_map(|(name, value)| (name == option).then(|| value)) + .unwrap_or_default() + } +} + +impl Setting for BaseKeymap { + const KEY: Option<&'static str> = Some("base_keymap"); + + type FileContent = Option; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result + where + Self: Sized, + { + Ok(user_values + .first() + .and_then(|v| **v) + .unwrap_or(default_value.unwrap())) + } +} diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs new file mode 100644 index 0000000000..a5d95429bd --- /dev/null +++ b/crates/welcome2/src/welcome.rs @@ -0,0 +1,287 @@ +mod base_keymap_picker; +mod base_keymap_setting; + +use crate::base_keymap_picker::ToggleBaseKeymapSelector; +use client::TelemetrySettings; +use db::kvp::KEY_VALUE_STORE; +use gpui::{ + elements::{Flex, Label, ParentElement}, + AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle, +}; +use settings::{update_settings_file, SettingsStore}; +use std::{borrow::Cow, sync::Arc}; +use vim::VimModeSetting; +use workspace::{ + dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace, + WorkspaceId, +}; + +pub use base_keymap_setting::BaseKeymap; + +pub const FIRST_OPEN: &str = "first_open"; + +pub fn init(cx: &mut AppContext) { + settings::register::(cx); + + cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { + let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); + workspace.add_item(Box::new(welcome_page), cx) + }); + + base_keymap_picker::init(cx); +} + +pub fn show_welcome_experience(app_state: &Arc, cx: &mut AppContext) { + open_new(&app_state, cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Left, cx); + let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); + workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); + cx.focus(&welcome_page); + cx.notify(); + }) + .detach(); + + db::write_and_log(cx, || { + KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) + }); +} + +pub struct WelcomePage { + workspace: WeakViewHandle, + _settings_subscription: Subscription, +} + +impl Entity for WelcomePage { + type Event = (); +} + +impl View for WelcomePage { + fn ui_name() -> &'static str { + "WelcomePage" + } + + fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { + let self_handle = cx.handle(); + let theme = theme::current(cx); + let width = theme.welcome.page_width; + + let telemetry_settings = *settings::get::(cx); + let vim_mode_setting = settings::get::(cx).0; + + enum Metrics {} + enum Diagnostics {} + + PaneBackdrop::new( + self_handle.id(), + Flex::column() + .with_child( + Flex::column() + .with_child( + theme::ui::svg(&theme.welcome.logo) + .aligned() + .contained() + .aligned(), + ) + .with_child( + Label::new( + "Code at the speed of thought", + theme.welcome.logo_subheading.text.clone(), + ) + .aligned() + .contained() + .with_style(theme.welcome.logo_subheading.container), + ) + .contained() + .with_style(theme.welcome.heading_group) + .constrained() + .with_width(width), + ) + .with_child( + Flex::column() + .with_child(theme::ui::cta_button::( + "Choose a theme", + width, + &theme.welcome.button, + cx, + |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + theme_selector::toggle(workspace, &Default::default(), cx) + }) + } + }, + )) + .with_child(theme::ui::cta_button::( + "Choose a keymap", + width, + &theme.welcome.button, + cx, + |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + base_keymap_picker::toggle( + workspace, + &Default::default(), + cx, + ) + }) + } + }, + )) + .with_child(theme::ui::cta_button::( + "Install the CLI", + width, + &theme.welcome.button, + cx, + |_, _, cx| { + cx.app_context() + .spawn(|cx| async move { install_cli::install_cli(&cx).await }) + .detach_and_log_err(cx); + }, + )) + .contained() + .with_style(theme.welcome.button_group) + .constrained() + .with_width(width), + ) + .with_child( + Flex::column() + .with_child( + theme::ui::checkbox::( + "Enable vim mode", + &theme.welcome.checkbox, + vim_mode_setting, + 0, + cx, + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| *setting = Some(checked), + ) + } + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container), + ) + .with_child( + theme::ui::checkbox_with_label::( + Flex::column() + .with_child( + Label::new( + "Send anonymous usage data", + theme.welcome.checkbox.label.text.clone(), + ) + .contained() + .with_style(theme.welcome.checkbox.label.container), + ) + .with_child( + Label::new( + "Help > View Telemetry", + theme.welcome.usage_note.text.clone(), + ) + .contained() + .with_style(theme.welcome.usage_note.container), + ), + &theme.welcome.checkbox, + telemetry_settings.metrics, + 0, + cx, + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| setting.metrics = Some(checked), + ) + } + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container), + ) + .with_child( + theme::ui::checkbox::( + "Send crash reports", + &theme.welcome.checkbox, + telemetry_settings.diagnostics, + 1, + cx, + |this, checked, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let fs = workspace.read(cx).app_state().fs.clone(); + update_settings_file::( + fs, + cx, + move |setting| setting.diagnostics = Some(checked), + ) + } + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container), + ) + .contained() + .with_style(theme.welcome.checkbox_group) + .constrained() + .with_width(width), + ) + .constrained() + .with_max_width(width) + .contained() + .with_uniform_padding(10.) + .aligned() + .into_any(), + ) + .into_any_named("welcome page") + } +} + +impl WelcomePage { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + WelcomePage { + workspace: workspace.weak_handle(), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + } + } +} + +impl Item for WelcomePage { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + Some("Welcome to Zed!".into()) + } + + fn tab_content( + &self, + _detail: Option, + style: &theme::Tab, + _cx: &gpui::AppContext, + ) -> AnyElement { + Flex::row() + .with_child( + Label::new("Welcome to Zed!", style.label.clone()) + .aligned() + .contained(), + ) + .into_any() + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option { + Some(WelcomePage { + workspace: self.workspace.clone(), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + }) + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 24648f87f1..5aba7faaa0 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -66,12 +66,12 @@ shellexpand = "2.1.0" text = { package = "text2", path = "../text2" } terminal_view = { package = "terminal_view2", path = "../terminal_view2" } theme = { package = "theme2", path = "../theme2" } -# theme_selector = { path = "../theme_selector" } +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } util = { path = "../util" } # semantic_index = { path = "../semantic_index" } # vim = { path = "../vim" } workspace = { package = "workspace2", path = "../workspace2" } -# welcome = { path = "../welcome" } +welcome = { package = "welcome2", path = "../welcome2" } zed_actions = {package = "zed_actions2", path = "../zed_actions2"} anyhow.workspace = true async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 9c42badb85..648c4108d7 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result}; use backtrace::Backtrace; use chrono::Utc; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::UserStore; +use client::{Client, UserStore}; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use fs::RealFs; @@ -36,7 +36,7 @@ use std::{ path::{Path, PathBuf}, sync::{ atomic::{AtomicU32, Ordering}, - Arc, + Arc, Weak, }, thread, }; @@ -99,16 +99,15 @@ fn main() { let listener = Arc::new(listener); let open_listener = listener.clone(); app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); - app.on_reopen(move |_cx| { - // todo!("workspace") - // if cx.has_global::>() { - // if let Some(app_state) = cx.global::>().upgrade() { - // workspace::open_new(&app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // } - // } + app.on_reopen(move |cx| { + if cx.has_global::>() { + if let Some(app_state) = cx.global::>().upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } }); app.run(move |cx| { @@ -180,7 +179,6 @@ fn main() { user_store, fs, build_window_options, - // background_actions: todo!("ask Mikayla"), workspace_store, node_runtime, }); @@ -236,7 +234,7 @@ fn main() { } } - let mut _triggered_authentication = false; + let mut triggered_authentication = false; fn open_paths_and_log_errs( paths: &[PathBuf], @@ -266,17 +264,17 @@ fn main() { .detach(); } Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => { - todo!() - // triggered_authentication = true; - // let app_state = app_state.clone(); - // let client = client.clone(); - // cx.spawn(|mut cx| async move { - // // ignore errors here, we'll show a generic "not signed in" - // let _ = authenticate(client, &cx).await; - // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) - // .await - // }) - // .detach_and_log_err(cx) + triggered_authentication = true; + let app_state = app_state.clone(); + let client = client.clone(); + cx.spawn(|mut cx| async move { + // ignore errors here, we'll show a generic "not signed in" + let _ = authenticate(client, &cx).await; + // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) + // .await + anyhow::Ok(()) + }) + .detach_and_log_err(cx) } Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => { todo!() @@ -315,23 +313,23 @@ fn main() { }) .detach(); - // if !triggered_authentication { - // cx.spawn(|cx| async move { authenticate(client, &cx).await }) - // .detach_and_log_err(cx); - // } + if !triggered_authentication { + cx.spawn(|cx| async move { authenticate(client, &cx).await }) + .detach_and_log_err(cx); + } }); } -// async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { -// if stdout_is_a_pty() { -// if client::IMPERSONATE_LOGIN.is_some() { -// client.authenticate_and_connect(false, &cx).await?; -// } -// } else if client.has_keychain_credentials(&cx) { -// client.authenticate_and_connect(true, &cx).await?; -// } -// Ok::<_, anyhow::Error>(()) -// } +async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { + if stdout_is_a_pty() { + if client::IMPERSONATE_LOGIN.is_some() { + client.authenticate_and_connect(false, &cx).await?; + } + } else if client.has_keychain_credentials(&cx) { + client.authenticate_and_connect(true, &cx).await?; + } + Ok::<_, anyhow::Error>(()) +} async fn installation_id() -> Result { let legacy_key_name = "device_id"; @@ -355,11 +353,8 @@ async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncApp cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))? .await .log_err(); - } else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) { - // todo!(welcome) - //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - //todo!() - // cx.update(|cx| show_welcome_experience(app_state, cx)); + } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + cx.update(|cx| show_welcome_experience(app_state, cx)); } else { cx.update(|cx| { workspace::open_new(app_state, cx, |workspace, cx| { diff --git a/script/crate-dep-graph b/script/crate-dep-graph index 25285cc097..74ea36683c 100755 --- a/script/crate-dep-graph +++ b/script/crate-dep-graph @@ -11,7 +11,7 @@ graph_file=target/crate-graph.html cargo depgraph \ --workspace-only \ --offline \ - --root=zed,cli,collab \ + --root=zed2,cli,collab2 \ --dedup-transitive-deps \ | dot -Tsvg > $graph_file From b13638fa7632502b9e0bcc98ae90bd595165f73a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 15:33:44 -0700 Subject: [PATCH 02/33] Remove debugging --- crates/collab_ui2/src/collab_panel.rs | 29 +++++++++++---------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 7ef2d47c81..3bbdfd5f92 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -3044,7 +3044,7 @@ impl CollabPanel { }) .unwrap_or(false); - let has_messages_notification = channel.unseen_message_id.is_some() || true; + 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; @@ -3092,11 +3092,10 @@ impl CollabPanel { .child( div() .id("channel_chat") - .bg(gpui::blue()) .when(!has_messages_notification, |el| el.invisible()) .group_hover("", |style| style.visible()) .child( - IconButton::new("test_chat", Icon::MessageBubbles) + IconButton::new("channel_chat", Icon::MessageBubbles) .color(if has_messages_notification { Color::Default } else { @@ -3111,20 +3110,16 @@ impl CollabPanel { .when(!has_notes_notification, |el| el.invisible()) .group_hover("", |style| style.visible()) .child( - div().child("Notes").id("test_notes").tooltip(|cx| { - Tooltip::text("Open channel notes", cx) - }), - ), // .child( - // IconButton::new("channel_notes", Icon::File) - // .color(if has_notes_notification { - // Color::Default - // } else { - // Color::Muted - // }) - // .tooltip(|cx| { - // Tooltip::text("Open channel notes", cx) - // }), - // ), + IconButton::new("channel_notes", Icon::File) + .color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .tooltip(|cx| { + Tooltip::text("Open channel notes", cx) + }), + ), ), ), ) From af3fa4ec0b4cf9b321787db860175192633b0eef Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 16:10:18 -0700 Subject: [PATCH 03/33] Basic channel joining! --- crates/call2/src/call2.rs | 51 ++++++++++++- crates/collab_ui2/src/collab_panel.rs | 106 +++++++++++++------------- 2 files changed, 102 insertions(+), 55 deletions(-) diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 7885ef6e3f..3b821d4bec 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: WindowHandle, 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 { + return cx.spawn(|this, mut cx| async move { + let answer = requesting_window.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)); } } diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 3bbdfd5f92..3c2e78423a 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -90,10 +90,10 @@ use rpc::proto; // 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 { @@ -167,10 +167,10 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, - FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, - Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, - WeakView, + actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div, + EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, + ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, + VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; @@ -322,17 +322,17 @@ pub struct CollabPanel { subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, - // drag_target_channel: ChannelDragTarget, + 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 { @@ -614,7 +614,7 @@ impl CollabPanel { workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), // context_menu_on_selected: true, - // drag_target_channel: ChannelDragTarget::None, + drag_target_channel: ChannelDragTarget::None, // list_state, }; @@ -2346,11 +2346,12 @@ impl CollabPanel { // } // } - // 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 open_channel_notes(&mut self, action: &OpenChannelNotes, 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 { @@ -2504,21 +2505,17 @@ impl CollabPanel { // .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); + active_call + .update(cx, |active_call, cx| { + active_call.join_channel(channel_id, handle, cx) + }) + .detach_and_log_err(cx) + } // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { // let channel_id = action.channel_id; @@ -2982,9 +2979,7 @@ impl CollabPanel { is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { - ListItem::new("contact-placeholder") - .child(Label::new("Add a Contact")) - .on_click(cx.listener(|this, _, cx| todo!())) + ListItem::new("contact-placeholder").child(Label::new("Add a Contact")) // enum AddContacts {} // MouseEventHandler::new::(0, cx, |state, _| { // let style = theme.list_empty_state.style_for(is_selected, state); @@ -3023,6 +3018,15 @@ impl CollabPanel { ) -> 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) @@ -3034,16 +3038,6 @@ impl CollabPanel { .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()) .unwrap_or(false); - 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 has_messages_notification = channel.unseen_message_id.is_some(); let has_notes_notification = channel.unseen_note_version.is_some(); @@ -3052,6 +3046,7 @@ impl CollabPanel { 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 @@ -3059,6 +3054,7 @@ impl CollabPanel { .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 @@ -3081,7 +3077,7 @@ impl CollabPanel { .w_full() .justify_between() .child( - div() + h_stack() .id(channel_id as usize) .child(Label::new(channel.name.clone())) .children(face_pile.map(|face_pile| face_pile.render(cx))) @@ -3128,7 +3124,15 @@ impl CollabPanel { } else { Toggle::NotToggleable }) - .on_click(cx.listener(|this, _, cx| todo!())) + .on_click(cx.listener(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_secondary_mouse_down(cx.listener(|this, _, cx| { todo!() // open context menu })), From d927c2f49795d72344153391fc5eb045beaf236b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 28 Nov 2023 15:18:19 -0800 Subject: [PATCH 04/33] Implement all but the UI --- .../command_palette2/src/command_palette.rs | 48 +- crates/file_finder2/src/file_finder.rs | 11 +- crates/picker2/src/picker2.rs | 28 +- crates/storybook2/src/stories/picker.rs | 10 +- crates/theme_selector2/src/theme_selector.rs | 105 +++-- crates/welcome2/src/base_keymap_picker.rs | 118 +++-- crates/welcome2/src/base_keymap_setting.rs | 6 +- crates/welcome2/src/welcome.rs | 439 +++++++++--------- crates/workspace2/src/modal_layer.rs | 1 - crates/workspace2/src/workspace2.rs | 32 +- crates/zed2/src/main.rs | 98 ++-- 11 files changed, 468 insertions(+), 428 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 07b819d3a1..a393c519be 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, + actions, div, prelude::*, Action, AnyElement, AppContext, DismissEvent, Div, EventEmitter, + FocusHandle, FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, + VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate}; +use picker::{simple_picker_match, Picker, PickerDelegate}; use std::{ cmp::{self, Reverse}, sync::Arc, }; -use theme::ActiveTheme; -use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; + +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -141,8 +141,6 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div; - fn placeholder_text(&self) -> Arc { "Execute a command...".into() } @@ -294,32 +292,24 @@ impl PickerDelegate for CommandPaletteDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { - let colors = cx.theme().colors(); + ) -> AnyElement { let Some(r#match) = self.matches.get(ix) else { - return div(); + return div().into_any(); }; let Some(command) = self.commands.get(r#match.candidate_id) else { - return div(); + return div().into_any(); }; - 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( - h_stack() - .justify_between() - .child(HighlightedLabel::new( - command.name.clone(), - r#match.positions.clone(), - )) - .children(KeyBinding::for_action(&*command.action, cx)), - ) + simple_picker_match(selected, cx, |cx| { + h_stack() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action(&*command.action, cx)) + .into_any() + }) } } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index ea578fbb0e..c93d29ffec 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,9 @@ 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, div, AnyElement, AppContext, DismissEvent, Div, Element, EventEmitter, FocusHandle, + FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, + View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -530,8 +530,6 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { - type ListItem = Div; - fn placeholder_text(&self) -> Arc { "Search project files...".into() } @@ -711,7 +709,7 @@ impl PickerDelegate for FileFinderDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { + ) -> AnyElement { let path_match = self .matches .get(ix) @@ -735,6 +733,7 @@ impl PickerDelegate for FileFinderDelegate { .child(HighlightedLabel::new(file_name, file_name_positions)) .child(HighlightedLabel::new(full_path, full_path_positions)), ) + .into_any() } } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index dc6b77c7c7..76d902da45 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}; @@ -15,8 +16,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 +31,7 @@ pub trait PickerDelegate: Sized + 'static { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem; + ) -> AnyElement; } impl FocusableView for Picker { @@ -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/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index ae6a26161b..13822c8545 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,6 +1,7 @@ use fuzzy::StringMatchCandidate; use gpui::{ - div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext, + div, prelude::*, AnyElement, Div, KeyBinding, Render, SharedString, Styled, Task, View, + WindowContext, }; use picker::{Picker, PickerDelegate}; use std::sync::Arc; @@ -36,8 +37,6 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div; - fn match_count(&self) -> usize { self.candidates.len() } @@ -51,10 +50,10 @@ impl PickerDelegate for Delegate { ix: usize, selected: bool, cx: &mut gpui::ViewContext>, - ) -> Self::ListItem { + ) -> AnyElement { let colors = cx.theme().colors(); let Some(candidate_ix) = self.matches.get(ix) else { - return div(); + return div().into_any(); }; // TASK: Make StringMatchCandidate::string a SharedString let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); @@ -70,6 +69,7 @@ impl PickerDelegate for Delegate { .text_color(colors.text_accent) }) .child(candidate) + .into_any() } fn selected_index(&self) -> usize { diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index 6e660caf51..1f3e6e92a1 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -2,39 +2,49 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, AppContext, Div, EventEmitter, FocusableView, Manager, Render, SharedString, - View, ViewContext, VisualContext, + actions, div, AnyElement, AppContext, DismissEvent, Element, EventEmitter, FocusableView, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, 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 util::ResultExt; -use workspace::{ui::HighlightedLabel, Workspace}; +use workspace::{ + ui::{HighlightedLabel, StyledExt}, + Workspace, +}; actions!(Toggle, Reload); pub fn init(cx: &mut AppContext) { cx.observe_new_views( - |workspace: &mut Workspace, cx: &mut ViewContext| { + |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(fs, 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 registry = cx.global::>(); - registry.clear(); - match registry.get(¤t_theme_name) { + 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); @@ -49,7 +59,7 @@ pub struct ThemeSelector { picker: View>, } -impl EventEmitter for ThemeSelector {} +impl EventEmitter for ThemeSelector {} impl FocusableView for ThemeSelector { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { @@ -60,7 +70,7 @@ impl FocusableView for ThemeSelector { impl Render for ThemeSelector { type Element = View>; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { self.picker.clone() } } @@ -79,16 +89,22 @@ pub struct ThemeSelectorDelegate { original_theme: Arc, selection_completed: bool, selected_index: usize, + view: WeakView, } impl ThemeSelectorDelegate { - fn new(fs: Arc, cx: &mut ViewContext) -> Self { + 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 mut theme_names = registry.list(staff_mode).collect::>(); - theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); + 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 { @@ -105,12 +121,13 @@ impl ThemeSelectorDelegate { original_theme: original_theme.clone(), selected_index: 0, selection_completed: false, + view: weak_view, }; - this.select_if_matching(&original_theme.meta.name); + this.select_if_matching(&original_theme.name); this } - fn show_selected_theme(&mut self, cx: &mut ViewContext) { + 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) { @@ -133,18 +150,16 @@ impl ThemeSelectorDelegate { } fn set_theme(theme: Arc, cx: &mut AppContext) { - cx.update_global::(|store, cx| { + cx.update_global(|store: &mut SettingsStore, cx| { let mut theme_settings = store.get::(None).clone(); - theme_settings.theme = theme; + theme_settings.active_theme = theme; store.override_global(theme_settings); - cx.refresh_windows(); + cx.refresh(); }); } } impl PickerDelegate for ThemeSelectorDelegate { - type ListItem = Div; - fn placeholder_text(&self) -> Arc { "Select Theme...".into() } @@ -153,18 +168,22 @@ impl PickerDelegate for ThemeSelectorDelegate { self.matches.len() } - fn confirm(&mut self, _: bool, cx: &mut ViewContext) { + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { self.selection_completed = true; - let theme_name = cx.theme().meta.name.clone(); - update_settings_file::(self.fs.clone(), cx, |settings| { - settings.theme = Some(theme_name); + let theme_name = cx.theme().name.clone(); + update_settings_file::(self.fs.clone(), cx, move |settings| { + settings.theme = Some(theme_name.to_string()); }); - cx.emit(Manager::Dismiss); + self.view + .update(cx, |_, cx| { + cx.emit(DismissEvent::Dismiss); + }) + .ok(); } - fn dismissed(&mut self, cx: &mut ViewContext) { + fn dismissed(&mut self, cx: &mut ViewContext>) { if !self.selection_completed { Self::set_theme(self.original_theme.clone(), cx); self.selection_completed = true; @@ -175,7 +194,11 @@ impl PickerDelegate for ThemeSelectorDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + fn set_selected_index( + &mut self, + ix: usize, + cx: &mut ViewContext>, + ) { self.selected_index = ix; self.show_selected_theme(cx); } @@ -183,17 +206,17 @@ impl PickerDelegate for ThemeSelectorDelegate { fn update_matches( &mut self, query: String, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) -> gpui::Task<()> { - let background = cx.background().clone(); + let background = cx.background_executor().clone(); let candidates = self .theme_names .iter() .enumerate() .map(|(id, meta)| StringMatchCandidate { id, - char_bag: meta.name.as_str().into(), - string: meta.name.clone(), + char_bag: meta.as_ref().into(), + string: meta.to_string(), }) .collect::>(); @@ -222,18 +245,23 @@ impl PickerDelegate for ThemeSelectorDelegate { }; this.update(&mut cx, |this, cx| { - let delegate = this.delegate_mut(); - delegate.matches = matches; - delegate.selected_index = delegate + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate .selected_index - .min(delegate.matches.len().saturating_sub(1)); - delegate.show_selected_theme(cx); + .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: &AppContext) -> Self::ListItem { + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> AnyElement { let theme = cx.theme(); let colors = theme.colors(); @@ -250,5 +278,6 @@ impl PickerDelegate for ThemeSelectorDelegate { theme_match.string.clone(), theme_match.positions.clone(), )) + .into_any() } } diff --git a/crates/welcome2/src/base_keymap_picker.rs b/crates/welcome2/src/base_keymap_picker.rs index 021e3b86a0..b90478f960 100644 --- a/crates/welcome2/src/base_keymap_picker.rs +++ b/crates/welcome2/src/base_keymap_picker.rs @@ -1,22 +1,23 @@ use super::base_keymap_setting::BaseKeymap; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, - elements::{Element as _, Label}, - AppContext, Task, ViewContext, + actions, AppContext, DismissEvent, EventEmitter, FocusableView, IntoElement, Render, Task, + View, ViewContext, VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{simple_picker_match, Picker, PickerDelegate}; use project::Fs; -use settings::update_settings_file; +use settings::{update_settings_file, Settings}; use std::sync::Arc; use util::ResultExt; -use workspace::Workspace; +use workspace::{ui::HighlightedLabel, Workspace}; -actions!(welcome, [ToggleBaseKeymapSelector]); +actions!(ToggleBaseKeymapSelector); pub fn init(cx: &mut AppContext) { - cx.add_action(toggle); - BaseKeymapSelector::init(cx); + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(toggle); + }) + .detach(); } pub fn toggle( @@ -24,28 +25,70 @@ pub fn toggle( _: &ToggleBaseKeymapSelector, cx: &mut ViewContext, ) { - workspace.toggle_modal(cx, |workspace, cx| { - let fs = workspace.app_state().fs.clone(); - cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx)) + let fs = workspace.app_state().fs.clone(); + workspace.toggle_modal(cx, |cx| { + BaseKeymapSelector::new( + BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx), + cx, + ) }); } -pub type BaseKeymapSelector = Picker; +pub struct BaseKeymapSelector { + focus_handle: gpui::FocusHandle, + picker: View>, +} + +impl FocusableView for BaseKeymapSelector { + fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for BaseKeymapSelector {} + +impl BaseKeymapSelector { + pub fn new( + delegate: BaseKeymapSelectorDelegate, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + let focus_handle = cx.focus_handle(); + Self { + focus_handle, + picker, + } + } +} + +impl Render for BaseKeymapSelector { + type Element = View>; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.picker.clone() + } +} pub struct BaseKeymapSelectorDelegate { + view: WeakView, matches: Vec, selected_index: usize, fs: Arc, } impl BaseKeymapSelectorDelegate { - fn new(fs: Arc, cx: &mut ViewContext) -> Self { - let base = settings::get::(cx); + fn new( + weak_view: WeakView, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let base = BaseKeymap::get(None, cx); let selected_index = BaseKeymap::OPTIONS .iter() .position(|(_, value)| value == base) .unwrap_or(0); Self { + view: weak_view, matches: Vec::new(), selected_index, fs, @@ -66,16 +109,20 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { self.selected_index } - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext) { + fn set_selected_index( + &mut self, + ix: usize, + _: &mut ViewContext>, + ) { self.selected_index = ix; } fn update_matches( &mut self, query: String, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) -> Task<()> { - let background = cx.background().clone(); + let background = cx.background_executor().clone(); let candidates = BaseKeymap::names() .enumerate() .map(|(id, name)| StringMatchCandidate { @@ -110,43 +157,44 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { }; this.update(&mut cx, |this, _| { - let delegate = this.delegate_mut(); - delegate.matches = matches; - delegate.selected_index = delegate + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate .selected_index - .min(delegate.matches.len().saturating_sub(1)); + .min(this.delegate.matches.len().saturating_sub(1)); }) .log_err(); }) } - fn confirm(&mut self, _: bool, cx: &mut ViewContext) { + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { if let Some(selection) = self.matches.get(self.selected_index) { let base_keymap = BaseKeymap::from_names(&selection.string); update_settings_file::(self.fs.clone(), cx, move |setting| { *setting = Some(base_keymap) }); } - cx.emit(PickerEvent::Dismiss); + + self.view + .update(cx, |_, cx| { + cx.emit(DismissEvent::Dismiss); + }) + .ok(); } - fn dismissed(&mut self, _cx: &mut ViewContext) {} + fn dismissed(&mut self, _cx: &mut ViewContext>) {} fn render_match( &self, ix: usize, - mouse_state: &mut gpui::MouseState, selected: bool, - cx: &gpui::AppContext, - ) -> gpui::AnyElement> { - let theme = &theme::current(cx); + cx: &mut gpui::ViewContext>, + ) -> gpui::AnyElement { let keymap_match = &self.matches[ix]; - let style = theme.picker.item.in_state(selected).style_for(mouse_state); - Label::new(keymap_match.string.clone(), style.label.clone()) - .with_highlights(keymap_match.positions.clone()) - .contained() - .with_style(style.container) - .into_any() + simple_picker_match(selected, cx, |_cx| { + HighlightedLabel::new(keymap_match.string.clone(), keymap_match.positions.clone()) + .into_any_element() + }) } } diff --git a/crates/welcome2/src/base_keymap_setting.rs b/crates/welcome2/src/base_keymap_setting.rs index c5b6171f9b..cad6e894f9 100644 --- a/crates/welcome2/src/base_keymap_setting.rs +++ b/crates/welcome2/src/base_keymap_setting.rs @@ -1,6 +1,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::Setting; +use settings::Settings; #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { @@ -44,7 +44,7 @@ impl BaseKeymap { } } -impl Setting for BaseKeymap { +impl Settings for BaseKeymap { const KEY: Option<&'static str> = Some("base_keymap"); type FileContent = Option; @@ -52,7 +52,7 @@ impl Setting for BaseKeymap { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &gpui::AppContext, + _: &mut gpui::AppContext, ) -> anyhow::Result where Self: Sized, diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs index a5d95429bd..68b515f577 100644 --- a/crates/welcome2/src/welcome.rs +++ b/crates/welcome2/src/welcome.rs @@ -1,19 +1,18 @@ mod base_keymap_picker; mod base_keymap_setting; -use crate::base_keymap_picker::ToggleBaseKeymapSelector; -use client::TelemetrySettings; use db::kvp::KEY_VALUE_STORE; use gpui::{ - elements::{Flex, Label, ParentElement}, - AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle, + div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, + FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View, + ViewContext, VisualContext, WeakView, WindowContext, }; -use settings::{update_settings_file, SettingsStore}; -use std::{borrow::Cow, sync::Arc}; -use vim::VimModeSetting; +use settings::{Settings, SettingsStore}; +use std::sync::Arc; use workspace::{ - dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace, - WorkspaceId, + dock::DockPosition, + item::{Item, ItemEvent}, + open_new, AppState, Welcome, Workspace, WorkspaceId, }; pub use base_keymap_setting::BaseKeymap; @@ -21,12 +20,15 @@ pub use base_keymap_setting::BaseKeymap; pub const FIRST_OPEN: &str = "first_open"; pub fn init(cx: &mut AppContext) { - settings::register::(cx); + BaseKeymap::register(cx); - cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { - let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); - workspace.add_item(Box::new(welcome_page), cx) - }); + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(|workspace, _: &Welcome, cx| { + let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx)); + workspace.add_item(Box::new(welcome_page), cx) + }); + }) + .detach(); base_keymap_picker::init(cx); } @@ -34,9 +36,9 @@ pub fn init(cx: &mut AppContext) { pub fn show_welcome_experience(app_state: &Arc, cx: &mut AppContext) { open_new(&app_state, cx, |workspace, cx| { workspace.toggle_dock(DockPosition::Left, cx); - let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); + let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx)); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); - cx.focus(&welcome_page); + cx.focus_view(&welcome_page); cx.notify(); }) .detach(); @@ -47,227 +49,217 @@ pub fn show_welcome_experience(app_state: &Arc, cx: &mut AppContext) { } pub struct WelcomePage { - workspace: WeakViewHandle, + workspace: WeakView, + focus_handle: FocusHandle, _settings_subscription: Subscription, } -impl Entity for WelcomePage { - type Event = (); -} +impl Render for WelcomePage { + type Element = Focusable
; -impl View for WelcomePage { - fn ui_name() -> &'static str { - "WelcomePage" - } + fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { + // let self_handle = cx.handle(); + // let theme = cx.theme(); + // let width = theme.welcome.page_width; - fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { - let self_handle = cx.handle(); - let theme = theme::current(cx); - let width = theme.welcome.page_width; + // let telemetry_settings = TelemetrySettings::get(None, cx); + // let vim_mode_setting = VimModeSettings::get(cx); - let telemetry_settings = *settings::get::(cx); - let vim_mode_setting = settings::get::(cx).0; - - enum Metrics {} - enum Diagnostics {} - - PaneBackdrop::new( - self_handle.id(), - Flex::column() - .with_child( - Flex::column() - .with_child( - theme::ui::svg(&theme.welcome.logo) - .aligned() - .contained() - .aligned(), - ) - .with_child( - Label::new( - "Code at the speed of thought", - theme.welcome.logo_subheading.text.clone(), - ) - .aligned() - .contained() - .with_style(theme.welcome.logo_subheading.container), - ) - .contained() - .with_style(theme.welcome.heading_group) - .constrained() - .with_width(width), - ) - .with_child( - Flex::column() - .with_child(theme::ui::cta_button::( - "Choose a theme", - width, - &theme.welcome.button, - cx, - |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - theme_selector::toggle(workspace, &Default::default(), cx) - }) - } - }, - )) - .with_child(theme::ui::cta_button::( - "Choose a keymap", - width, - &theme.welcome.button, - cx, - |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - base_keymap_picker::toggle( - workspace, - &Default::default(), - cx, - ) - }) - } - }, - )) - .with_child(theme::ui::cta_button::( - "Install the CLI", - width, - &theme.welcome.button, - cx, - |_, _, cx| { - cx.app_context() - .spawn(|cx| async move { install_cli::install_cli(&cx).await }) - .detach_and_log_err(cx); - }, - )) - .contained() - .with_style(theme.welcome.button_group) - .constrained() - .with_width(width), - ) - .with_child( - Flex::column() - .with_child( - theme::ui::checkbox::( - "Enable vim mode", - &theme.welcome.checkbox, - vim_mode_setting, - 0, - cx, - |this, checked, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let fs = workspace.read(cx).app_state().fs.clone(); - update_settings_file::( - fs, - cx, - move |setting| *setting = Some(checked), - ) - } - }, - ) - .contained() - .with_style(theme.welcome.checkbox_container), - ) - .with_child( - theme::ui::checkbox_with_label::( - Flex::column() - .with_child( - Label::new( - "Send anonymous usage data", - theme.welcome.checkbox.label.text.clone(), - ) - .contained() - .with_style(theme.welcome.checkbox.label.container), - ) - .with_child( - Label::new( - "Help > View Telemetry", - theme.welcome.usage_note.text.clone(), - ) - .contained() - .with_style(theme.welcome.usage_note.container), - ), - &theme.welcome.checkbox, - telemetry_settings.metrics, - 0, - cx, - |this, checked, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let fs = workspace.read(cx).app_state().fs.clone(); - update_settings_file::( - fs, - cx, - move |setting| setting.metrics = Some(checked), - ) - } - }, - ) - .contained() - .with_style(theme.welcome.checkbox_container), - ) - .with_child( - theme::ui::checkbox::( - "Send crash reports", - &theme.welcome.checkbox, - telemetry_settings.diagnostics, - 1, - cx, - |this, checked, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let fs = workspace.read(cx).app_state().fs.clone(); - update_settings_file::( - fs, - cx, - move |setting| setting.diagnostics = Some(checked), - ) - } - }, - ) - .contained() - .with_style(theme.welcome.checkbox_container), - ) - .contained() - .with_style(theme.welcome.checkbox_group) - .constrained() - .with_width(width), - ) - .constrained() - .with_max_width(width) - .contained() - .with_uniform_padding(10.) - .aligned() - .into_any(), - ) - .into_any_named("welcome page") + div() + .track_focus(&self.focus_handle) + .child(div().size_full().bg(red()).child("Welcome!")) + //todo!() + // PaneBackdrop::new( + // self_handle.id(), + // Flex::column() + // .with_child( + // Flex::column() + // .with_child( + // theme::ui::svg(&theme.welcome.logo) + // .aligned() + // .contained() + // .aligned(), + // ) + // .with_child( + // Label::new( + // "Code at the speed of thought", + // theme.welcome.logo_subheading.text.clone(), + // ) + // .aligned() + // .contained() + // .with_style(theme.welcome.logo_subheading.container), + // ) + // .contained() + // .with_style(theme.welcome.heading_group) + // .constrained() + // .with_width(width), + // ) + // .with_child( + // Flex::column() + // .with_child(theme::ui::cta_button::( + // "Choose a theme", + // width, + // &theme.welcome.button, + // cx, + // |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // theme_selector::toggle(workspace, &Default::default(), cx) + // }) + // } + // }, + // )) + // .with_child(theme::ui::cta_button::( + // "Choose a keymap", + // width, + // &theme.welcome.button, + // cx, + // |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // base_keymap_picker::toggle( + // workspace, + // &Default::default(), + // cx, + // ) + // }) + // } + // }, + // )) + // .with_child(theme::ui::cta_button::( + // "Install the CLI", + // width, + // &theme.welcome.button, + // cx, + // |_, _, cx| { + // cx.app_context() + // .spawn(|cx| async move { install_cli::install_cli(&cx).await }) + // .detach_and_log_err(cx); + // }, + // )) + // .contained() + // .with_style(theme.welcome.button_group) + // .constrained() + // .with_width(width), + // ) + // .with_child( + // Flex::column() + // .with_child( + // theme::ui::checkbox::( + // "Enable vim mode", + // &theme.welcome.checkbox, + // vim_mode_setting, + // 0, + // cx, + // |this, checked, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let fs = workspace.read(cx).app_state().fs.clone(); + // update_settings_file::( + // fs, + // cx, + // move |setting| *setting = Some(checked), + // ) + // } + // }, + // ) + // .contained() + // .with_style(theme.welcome.checkbox_container), + // ) + // .with_child( + // theme::ui::checkbox_with_label::( + // Flex::column() + // .with_child( + // Label::new( + // "Send anonymous usage data", + // theme.welcome.checkbox.label.text.clone(), + // ) + // .contained() + // .with_style(theme.welcome.checkbox.label.container), + // ) + // .with_child( + // Label::new( + // "Help > View Telemetry", + // theme.welcome.usage_note.text.clone(), + // ) + // .contained() + // .with_style(theme.welcome.usage_note.container), + // ), + // &theme.welcome.checkbox, + // telemetry_settings.metrics, + // 0, + // cx, + // |this, checked, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let fs = workspace.read(cx).app_state().fs.clone(); + // update_settings_file::( + // fs, + // cx, + // move |setting| setting.metrics = Some(checked), + // ) + // } + // }, + // ) + // .contained() + // .with_style(theme.welcome.checkbox_container), + // ) + // .with_child( + // theme::ui::checkbox::( + // "Send crash reports", + // &theme.welcome.checkbox, + // telemetry_settings.diagnostics, + // 1, + // cx, + // |this, checked, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let fs = workspace.read(cx).app_state().fs.clone(); + // update_settings_file::( + // fs, + // cx, + // move |setting| setting.diagnostics = Some(checked), + // ) + // } + // }, + // ) + // .contained() + // .with_style(theme.welcome.checkbox_container), + // ) + // .contained() + // .with_style(theme.welcome.checkbox_group) + // .constrained() + // .with_width(width), + // ) + // .constrained() + // .with_max_width(width) + // .contained() + // .with_uniform_padding(10.) + // .aligned() + // .into_any(), + // ) + // .into_any_named("welcome page") } } impl WelcomePage { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { WelcomePage { + focus_handle: cx.focus_handle(), workspace: workspace.weak_handle(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), } } } -impl Item for WelcomePage { - fn tab_tooltip_text(&self, _: &AppContext) -> Option> { - Some("Welcome to Zed!".into()) - } +impl EventEmitter for WelcomePage {} - fn tab_content( - &self, - _detail: Option, - style: &theme::Tab, - _cx: &gpui::AppContext, - ) -> AnyElement { - Flex::row() - .with_child( - Label::new("Welcome to Zed!", style.label.clone()) - .aligned() - .contained(), - ) - .into_any() +impl FocusableView for WelcomePage { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for WelcomePage { + fn tab_content(&self, _: Option, _: &WindowContext) -> AnyElement { + "Welcome to Zed!".into_any() } fn show_toolbar(&self) -> bool { @@ -278,10 +270,11 @@ impl Item for WelcomePage { &self, _workspace_id: WorkspaceId, cx: &mut ViewContext, - ) -> Option { - Some(WelcomePage { + ) -> Option> { + Some(cx.build_view(|cx| WelcomePage { + focus_handle: cx.focus_handle(), workspace: self.workspace.clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - }) + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + })) } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 6d28a6299b..6b14151e09 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -46,7 +46,6 @@ impl ModalLayer { previous_focus_handle: cx.focused(), focus_handle: cx.focus_handle(), }); - dbg!("focusing"); cx.focus_view(&new_modal); cx.notify(); } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 50f8611c4c..5d326d3c53 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1808,22 +1808,22 @@ impl Workspace { pane } - // pub fn add_item_to_center( - // &mut self, - // item: Box, - // cx: &mut ViewContext, - // ) -> bool { - // if let Some(center_pane) = self.last_active_center_pane.clone() { - // if let Some(center_pane) = center_pane.upgrade(cx) { - // center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); - // true - // } else { - // false - // } - // } else { - // false - // } - // } + pub fn add_item_to_center( + &mut self, + item: Box, + cx: &mut ViewContext, + ) -> bool { + if let Some(center_pane) = self.last_active_center_pane.clone() { + if let Some(center_pane) = center_pane.upgrade() { + center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); + true + } else { + false + } + } else { + false + } + } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { self.active_pane diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 843dd00e9f..4d9a61a52f 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE; use editor::Editor; use fs::RealFs; use futures::StreamExt; -use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; +use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use isahc::{prelude::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; @@ -48,6 +48,7 @@ use util::{ paths, ResultExt, }; use uuid::Uuid; +use welcome::{show_welcome_experience, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed2::{ build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, @@ -163,17 +164,16 @@ fn main() { // assistant::init(cx); // component_test::init(cx); - // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) // .detach(); // watch_file_types(fs.clone(), cx); languages.set_theme(cx.theme().clone()); - // cx.observe_global::({ - // let languages = languages.clone(); - // move |cx| languages.set_theme(theme::current(cx).clone()) - // }) - // .detach(); + cx.observe_global::({ + let languages = languages.clone(); + move |cx| languages.set_theme(cx.theme().clone()) + }) + .detach(); client.telemetry().start(installation_id, session_id, cx); let telemetry_settings = *client::TelemetrySettings::get_global(cx); @@ -217,14 +217,13 @@ fn main() { // journal2::init(app_state.clone(), cx); // language_selector::init(cx); - // theme_selector::init(cx); + theme_selector::init(cx); // activity_indicator::init(cx); // language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); // feedback::init(cx); - // welcome::init(cx); - // zed::init(&app_state, cx); + welcome::init(cx); // cx.set_menus(menus::menus()); initialize_workspace(app_state.clone(), cx); @@ -283,6 +282,7 @@ fn main() { cx.spawn(|mut cx| async move { // ignore errors here, we'll show a generic "not signed in" let _ = authenticate(client, &cx).await; + //todo!() // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) // .await anyhow::Ok(()) @@ -367,7 +367,8 @@ async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncApp .await .log_err(); } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_welcome_experience(app_state, cx)); + cx.update(|cx| show_welcome_experience(app_state, cx)) + .log_err(); } else { cx.update(|cx| { workspace::open_new(app_state, cx, |workspace, cx| { @@ -705,44 +706,23 @@ fn load_embedded_fonts(cx: &AppContext) { .unwrap(); } -// #[cfg(debug_assertions)] -// async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { -// let mut events = fs -// .watch("styles/src".as_ref(), Duration::from_millis(100)) -// .await; -// while (events.next().await).is_some() { -// let output = Command::new("npm") -// .current_dir("styles") -// .args(["run", "build"]) -// .output() -// .await -// .log_err()?; -// if output.status.success() { -// cx.update(|cx| theme_selector::reload(cx)) -// } else { -// eprintln!( -// "build script failed {}", -// String::from_utf8_lossy(&output.stderr) -// ); -// } -// } -// Some(()) -// } +#[cfg(debug_assertions)] +async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { + use std::time::Duration; -// #[cfg(debug_assertions)] -// async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { -// let mut events = fs -// .watch( -// "crates/zed/src/languages".as_ref(), -// Duration::from_millis(100), -// ) -// .await; -// while (events.next().await).is_some() { -// languages.reload(); -// } -// Some(()) -// } + let mut events = fs + .watch( + "crates/zed2/src/languages".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + languages.reload(); + } + Some(()) +} +//todo!() // #[cfg(debug_assertions)] // fn watch_file_types(fs: Arc, cx: &mut AppContext) { // cx.spawn(|mut cx| async move { @@ -763,26 +743,10 @@ fn load_embedded_fonts(cx: &AppContext) { // .detach() // } -// #[cfg(not(debug_assertions))] -// async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { -// None -// } - -// #[cfg(not(debug_assertions))] -// async fn watch_languages(_: Arc, _: Arc) -> Option<()> { -// None -// +#[cfg(not(debug_assertions))] +async fn watch_languages(_: Arc, _: Arc) -> Option<()> { + None +} // #[cfg(not(debug_assertions))] // fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} - -pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { - // &[ - // ("Go to file", &file_finder::Toggle), - // ("Open command palette", &command_palette::Toggle), - // ("Open recent projects", &recent_projects::OpenRecent), - // ("Change your settings", &zed_actions::OpenSettings), - // ] - // todo!() - &[] -} From 4c2348eb532029b6a9fa4b702d0fbcaac53f3ba4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 16:20:54 -0700 Subject: [PATCH 05/33] Fix tests, notify errors --- crates/call2/src/call2.rs | 6 +-- crates/collab2/src/tests/channel_tests.rs | 37 ++++++++++++------- crates/collab2/src/tests/integration_tests.rs | 11 ++++-- crates/collab_ui2/src/collab_panel.rs | 15 +++++--- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 3b821d4bec..df7dd847cf 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -334,7 +334,7 @@ impl ActiveCall { pub fn join_channel( &mut self, channel_id: u64, - requesting_window: WindowHandle, + requesting_window: Option>, cx: &mut ModelContext, ) -> Task>>> { if let Some(room) = self.room().cloned() { @@ -360,9 +360,9 @@ impl ActiveCall { && room.is_sharing_project() && room.remote_participants().len() > 0 }); - if should_prompt { + if should_prompt && requesting_window.is_some() { return cx.spawn(|this, mut cx| async move { - let answer = requesting_window.update(&mut cx, |_, cx| { + 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?", 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 3c2e78423a..a92389e6bc 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2510,11 +2510,16 @@ impl CollabPanel { return; }; let active_call = ActiveCall::global(cx); - active_call - .update(cx, |active_call, cx| { - active_call.join_channel(channel_id, handle, cx) - }) - .detach_and_log_err(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) { From ed8e62cd18c41dc20b548b60bebc37c75296ab53 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 28 Nov 2023 15:25:28 -0800 Subject: [PATCH 06/33] Restore welcome page and several pickers --- .../project_panel2/src/file_associations.rs | 77 ++++++++----------- crates/project_panel2/src/project_panel.rs | 6 +- crates/welcome2/src/welcome.rs | 1 + crates/zed2/src/main.rs | 48 ++++++------ 4 files changed, 63 insertions(+), 69 deletions(-) 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..c1f30d029f 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -1269,16 +1269,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) } } }; diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs index 68b515f577..441c2bf696 100644 --- a/crates/welcome2/src/welcome.rs +++ b/crates/welcome2/src/welcome.rs @@ -58,6 +58,7 @@ impl Render for WelcomePage { type Element = Focusable
; fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { + // todo!(welcome_ui) // let self_handle = cx.handle(); // let theme = cx.theme(); // let width = theme.welcome.page_width; diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 4d9a61a52f..0c65d115d3 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -166,7 +166,7 @@ fn main() { // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) // .detach(); - // watch_file_types(fs.clone(), cx); + watch_file_types(fs.clone(), cx); languages.set_theme(cx.theme().clone()); cx.observe_global::({ @@ -722,31 +722,33 @@ async fn watch_languages(fs: Arc, languages: Arc) Some(()) } -//todo!() -// #[cfg(debug_assertions)] -// fn watch_file_types(fs: Arc, cx: &mut AppContext) { -// cx.spawn(|mut cx| async move { -// let mut events = fs -// .watch( -// "assets/icons/file_icons/file_types.json".as_ref(), -// Duration::from_millis(100), -// ) -// .await; -// while (events.next().await).is_some() { -// cx.update(|cx| { -// cx.update_global(|file_types, _| { -// *file_types = project_panel::file_associations::FileAssociations::new(Assets); -// }); -// }) -// } -// }) -// .detach() -// } +#[cfg(debug_assertions)] +fn watch_file_types(fs: Arc, cx: &mut AppContext) { + use std::time::Duration; + + cx.spawn(|mut cx| async move { + let mut events = fs + .watch( + "assets/icons/file_icons/file_types.json".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + cx.update(|cx| { + cx.update_global(|file_types, _| { + *file_types = project_panel::file_associations::FileAssociations::new(Assets); + }); + }) + .ok(); + } + }) + .detach() +} #[cfg(not(debug_assertions))] async fn watch_languages(_: Arc, _: Arc) -> Option<()> { None } -// #[cfg(not(debug_assertions))] -// fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} +#[cfg(not(debug_assertions))] +fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} From 60ce75c34a999161f63377772f6dd94d3cbf85f5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 16:52:12 -0700 Subject: [PATCH 07/33] Togglable channels, the greatest since sliced bread --- crates/collab_ui2/src/collab_panel.rs | 31 +++++++++++++----------- crates/ui2/src/components/disclosure.rs | 25 +++++++++++++------ crates/ui2/src/components/icon_button.rs | 15 ++++++++++-- crates/ui2/src/components/list.rs | 16 +++++++++--- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index a92389e6bc..2a1ce758ab 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2233,20 +2233,20 @@ 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); todo!() + 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() @@ -3129,6 +3129,9 @@ impl CollabPanel { } else { Toggle::NotToggleable }) + .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 { diff --git a/crates/ui2/src/components/disclosure.rs b/crates/ui2/src/components/disclosure.rs index 3ec8c1953e..e0d7b1c519 100644 --- a/crates/ui2/src/components/disclosure.rs +++ b/crates/ui2/src/components/disclosure.rs @@ -1,19 +1,30 @@ -use gpui::{div, Element, ParentElement}; +use std::rc::Rc; -use crate::{Color, Icon, IconElement, IconSize, Toggle}; +use gpui::{div, Element, IntoElement, MouseDownEvent, ParentElement, WindowContext}; -pub fn disclosure_control(toggle: Toggle) -> impl Element { +use crate::{Color, Icon, IconButton, IconSize, Toggle}; + +pub fn disclosure_control( + toggle: Toggle, + on_toggle: Option>, +) -> impl Element { match (toggle.is_toggleable(), toggle.is_toggled()) { (false, _) => div(), (_, true) => div().child( - IconElement::new(Icon::ChevronDown) + IconButton::new("toggle", Icon::ChevronDown) .color(Color::Muted) - .size(IconSize::Small), + .size(IconSize::Small) + .when_some(on_toggle, move |el, on_toggle| { + el.on_click(move |e, cx| on_toggle(e, cx)) + }), ), (_, false) => div().child( - IconElement::new(Icon::ChevronRight) + IconButton::new("toggle", Icon::ChevronRight) .color(Color::Muted) - .size(IconSize::Small), + .size(IconSize::Small) + .when_some(on_toggle, move |el, on_toggle| { + el.on_click(move |e, cx| on_toggle(e, cx)) + }), ), } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index b2f3f8403d..104eb00ec8 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, Icon, IconElement}; +use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful}; #[derive(IntoElement)] @@ -6,6 +6,7 @@ pub struct IconButton { id: ElementId, icon: Icon, color: Color, + size: IconSize, variant: ButtonVariant, state: InteractionState, selected: bool, @@ -50,7 +51,11 @@ impl RenderOnce for IconButton { // place we use an icon button. // .hover(|style| style.bg(bg_hover_color)) .active(|style| style.bg(bg_active_color)) - .child(IconElement::new(self.icon).color(icon_color)); + .child( + IconElement::new(self.icon) + .size(self.size) + .color(icon_color), + ); if let Some(click_handler) = self.on_mouse_down { button = button.on_mouse_down(MouseButton::Left, move |event, cx| { @@ -76,6 +81,7 @@ impl IconButton { id: id.into(), icon, color: Color::default(), + size: Default::default(), variant: ButtonVariant::default(), state: InteractionState::default(), selected: false, @@ -94,6 +100,11 @@ impl IconButton { self } + pub fn size(mut self, size: IconSize) -> Self { + self.size = size; + self + } + pub fn variant(mut self, variant: ButtonVariant) -> Self { self.variant = variant; self diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 749de951d9..61ed483cd8 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -63,7 +63,7 @@ impl RenderOnce for ListHeader { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let disclosure_control = disclosure_control(self.toggle); + let disclosure_control = disclosure_control(self.toggle, None); let meta = match self.meta { Some(ListHeaderMeta::Tools(icons)) => div().child( @@ -177,6 +177,7 @@ pub struct ListItem { toggle: Toggle, inset: bool, on_click: Option>, + on_toggle: Option>, on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -193,6 +194,7 @@ impl ListItem { inset: false, on_click: None, on_secondary_mouse_down: None, + on_toggle: None, children: SmallVec::new(), } } @@ -230,6 +232,14 @@ impl ListItem { self } + pub fn on_toggle( + mut self, + on_toggle: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_toggle = Some(Rc::new(on_toggle)); + self + } + pub fn selected(mut self, selected: bool) -> Self { self.selected = selected; self @@ -283,7 +293,7 @@ impl RenderOnce for ListItem { this.bg(cx.theme().colors().ghost_element_selected) }) .when_some(self.on_click.clone(), |this, on_click| { - this.on_click(move |event, cx| { + this.cursor_pointer().on_click(move |event, cx| { // HACK: GPUI currently fires `on_click` with any mouse button, // but we only care about the left button. if event.down.button == MouseButton::Left { @@ -304,7 +314,7 @@ impl RenderOnce for ListItem { .gap_1() .items_center() .relative() - .child(disclosure_control(self.toggle)) + .child(disclosure_control(self.toggle, self.on_toggle)) .children(left_content) .children(self.children) // HACK: We need to attach the `on_click` handler to the child element in order to have the click From 8d1518d70c1a0e94ec5647cb3fab3745901f9ded Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 20:47:11 -0700 Subject: [PATCH 08/33] Fix stateful elements in Components Previously a component assumed its element was stateless, this was incorrect! --- crates/gpui2/src/element.rs | 44 +++++++++++++++++------- crates/ui2/src/components/icon_button.rs | 3 +- 2 files changed, 33 insertions(+), 14 deletions(-) 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/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 104eb00ec8..2ccda3ea0e 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -70,8 +70,7 @@ impl RenderOnce for IconButton { } } - // HACK: Add an additional identified element wrapper to fix tooltips not showing up. - div().id(self.id.clone()).child(button) + button } } From db5ded0252609ba169e7159f22c0be3b94f5fbd2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 20:53:46 -0700 Subject: [PATCH 09/33] Remove useless method We need to move state from layout to paint in any case --- crates/gpui2/src/window.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6f342f7065..b02005aa78 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1939,23 +1939,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() From e36c7dd30151bbb48e2a7d0a329c4a20b0a0bce3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 28 Nov 2023 23:11:29 -0500 Subject: [PATCH 10/33] Remove ID hack in `ListItem` (#3431) This PR removes the ID hack in `ListItem`, since the underlying issue was fixed in #3430. Release Notes: - N/A --- crates/ui2/src/components/list.rs | 16 +--------------- crates/ui2/src/components/stories/list_item.rs | 8 ++++---- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 61ed483cd8..f3e75f272d 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -316,21 +316,7 @@ impl RenderOnce for ListItem { .relative() .child(disclosure_control(self.toggle, self.on_toggle)) .children(left_content) - .children(self.children) - // HACK: We need to attach the `on_click` handler to the child element in order to have the click - // event actually fire. - // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the - // outer `div`. - .id("on_click_hack") - .when_some(self.on_click, |this, on_click| { - this.on_click(move |event, cx| { - // HACK: GPUI currently fires `on_click` with any mouse button, - // but we only care about the left button. - if event.down.button == MouseButton::Left { - (on_click)(event, cx) - } - }) - }), + .children(self.children), ) } } diff --git a/crates/ui2/src/components/stories/list_item.rs b/crates/ui2/src/components/stories/list_item.rs index 38359dbd5e..f6f00007f1 100644 --- a/crates/ui2/src/components/stories/list_item.rs +++ b/crates/ui2/src/components/stories/list_item.rs @@ -24,11 +24,11 @@ impl Render for ListItemStory { ) .child(Story::label("With `on_secondary_mouse_down`")) .child( - ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down( - |_event, _cx| { + ListItem::new("with_on_secondary_mouse_down") + .child("Right click me") + .on_secondary_mouse_down(|_event, _cx| { println!("Right mouse down!"); - }, - ), + }), ) } } From 0d4839b973c7b5f246156d40c7db57248e5cb8ba Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 21:14:17 -0700 Subject: [PATCH 11/33] use the right click event for buttons --- crates/search2/src/search_bar.rs | 6 +++--- crates/ui2/src/components/button.rs | 15 +++++---------- crates/ui2/src/components/disclosure.rs | 4 ++-- crates/ui2/src/components/icon_button.rs | 18 +++++++----------- crates/ui2/src/components/list.rs | 4 ++-- 5 files changed, 19 insertions(+), 28 deletions(-) diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 1a7456f41c..f5a9a8c8f7 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,4 +1,4 @@ -use gpui::{IntoElement, MouseDownEvent, WindowContext}; +use gpui::{ClickEvent, IntoElement, WindowContext}; use ui::{Button, ButtonVariant, IconButton}; use crate::mode::SearchMode; @@ -6,7 +6,7 @@ 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,7 +21,7 @@ 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 diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 02902a4b64..fbe5b951fa 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,9 +1,7 @@ -use std::rc::Rc; - use gpui::{ - DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent, - StatefulInteractiveElement, WindowContext, + ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext, }; +use std::rc::Rc; use crate::prelude::*; use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; @@ -67,7 +65,7 @@ impl ButtonVariant { #[derive(IntoElement)] pub struct Button { disabled: bool, - click_handler: Option>, + click_handler: Option>, icon: Option, icon_position: Option, label: SharedString, @@ -118,7 +116,7 @@ impl RenderOnce for Button { } if let Some(click_handler) = self.click_handler.clone() { - button = button.on_mouse_down(MouseButton::Left, move |event, cx| { + button = button.on_click(move |event, cx| { click_handler(event, cx); }); } @@ -168,10 +166,7 @@ impl Button { self } - pub fn on_click( - mut self, - handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, - ) -> Self { + pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { self.click_handler = Some(Rc::new(handler)); self } diff --git a/crates/ui2/src/components/disclosure.rs b/crates/ui2/src/components/disclosure.rs index e0d7b1c519..6206a2edd8 100644 --- a/crates/ui2/src/components/disclosure.rs +++ b/crates/ui2/src/components/disclosure.rs @@ -1,12 +1,12 @@ use std::rc::Rc; -use gpui::{div, Element, IntoElement, MouseDownEvent, ParentElement, WindowContext}; +use gpui::{div, ClickEvent, Element, IntoElement, ParentElement, WindowContext}; use crate::{Color, Icon, IconButton, IconSize, Toggle}; pub fn disclosure_control( toggle: Toggle, - on_toggle: Option>, + on_toggle: Option>, ) -> impl Element { match (toggle.is_toggleable(), toggle.is_toggled()) { (false, _) => div(), diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 2ccda3ea0e..cdaec6a770 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; -use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful}; +use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful}; #[derive(IntoElement)] pub struct IconButton { @@ -11,7 +11,7 @@ pub struct IconButton { state: InteractionState, selected: bool, tooltip: Option AnyView + 'static>>, - on_mouse_down: Option>, + on_click: Option>, } impl RenderOnce for IconButton { @@ -57,9 +57,8 @@ impl RenderOnce for IconButton { .color(icon_color), ); - if let Some(click_handler) = self.on_mouse_down { - button = button.on_mouse_down(MouseButton::Left, move |event, cx| { - cx.stop_propagation(); + if let Some(click_handler) = self.on_click { + button = button.on_click(move |event, cx| { click_handler(event, cx); }) } @@ -85,7 +84,7 @@ impl IconButton { state: InteractionState::default(), selected: false, tooltip: None, - on_mouse_down: None, + on_click: None, } } @@ -124,11 +123,8 @@ impl IconButton { self } - pub fn on_click( - mut self, - handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext), - ) -> Self { - self.on_mouse_down = Some(Box::new(handler)); + pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self { + self.on_click = Some(Box::new(handler)); self } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index f3e75f272d..aa61c8333e 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -177,7 +177,7 @@ pub struct ListItem { toggle: Toggle, inset: bool, on_click: Option>, - on_toggle: Option>, + on_toggle: Option>, on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -234,7 +234,7 @@ impl ListItem { pub fn on_toggle( mut self, - on_toggle: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { self.on_toggle = Some(Rc::new(on_toggle)); self From 5fbc60d8da40b3da125315e4d024dda7c08d657d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 28 Nov 2023 22:46:35 -0700 Subject: [PATCH 12/33] Inviting/Responding/Creating Channels... etc. --- crates/collab_ui2/src/collab_panel.rs | 1036 +++++++---------- .../src/collab_panel/contact_finder.rs | 1 - crates/feature_flags2/src/feature_flags2.rs | 4 +- crates/ui2/src/components/icon_button.rs | 1 + crates/ui2/src/components/list.rs | 22 +- 5 files changed, 420 insertions(+), 644 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 2a1ce758ab..64580f0efc 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -17,6 +17,7 @@ mod contact_finder; // Client, Contact, User, UserStore, // }; use contact_finder::ContactFinder; +use menu::Confirm; use rpc::proto; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; @@ -160,26 +161,26 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; use std::{iter::once, mem, sync::Arc}; use call::ActiveCall; -use channel::{Channel, ChannelId, ChannelStore}; +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}; +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, - ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, - VisualContext, WeakView, + ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, + ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; -use settings::Settings; +use settings::{Settings, SettingsStore}; use ui::{ - h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, Label, List, ListHeader, ListItem, - Toggle, Tooltip, + h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List, + ListHeader, ListItem, Toggle, Tooltip, }; -use util::{maybe, ResultExt}; +use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::NotifyResultExt, @@ -293,10 +294,10 @@ pub enum ChannelEditingState { } impl ChannelEditingState { - fn pending_name(&self) -> Option<&str> { + fn pending_name(&self) -> Option { match self { - ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), - ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), + ChannelEditingState::Create { pending_name, .. } => pending_name.clone(), + ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(), } } } @@ -306,10 +307,10 @@ pub struct CollabPanel { fs: Arc, focus_handle: FocusHandle, // channel_clipboard: Option, - // pending_serialization: Task>, + pending_serialization: Task>, // context_menu: ViewHandle, filter_editor: View, - // channel_name_editor: ViewHandle, + channel_name_editor: View, channel_editing_state: Option, entries: Vec, selection: Option, @@ -438,28 +439,21 @@ impl CollabPanel { // }) // .detach(); - // let channel_name_editor = cx.add_view(|cx| { - // Editor::single_line( - // Some(Arc::new(|theme| { - // theme.collab_panel.user_query_editor.clone() - // })), - // cx, - // ) - // }); + 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| { @@ -597,9 +591,9 @@ impl CollabPanel { focus_handle: cx.focus_handle(), // channel_clipboard: None, fs: workspace.app_state().fs.clone(), - // pending_serialization: Task::ready(None), + pending_serialization: Task::ready(None), // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - // channel_name_editor, + channel_name_editor, filter_editor, entries: Vec::default(), channel_editing_state: None, @@ -620,53 +614,52 @@ impl CollabPanel { 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); + 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.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 }) @@ -696,10 +689,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(); }); } @@ -707,25 +699,25 @@ 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); @@ -1456,16 +1448,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 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, @@ -1501,67 +1493,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_notes( // &self, // channel_id: ChannelId, @@ -1754,109 +1685,6 @@ impl CollabPanel { // .into_any() // } - // fn render_contact_request( - // user: Arc, - // user_store: ModelHandle, - // theme: &theme::CollabPanel, - // is_incoming: bool, - // is_selected: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum Decline {} - // enum Accept {} - // enum Cancel {} - - // let mut row = Flex::row() - // .with_children(user.avatar.clone().map(|avatar| { - // Image::from_data(avatar) - // .with_style(theme.contact_avatar) - // .aligned() - // .left() - // })) - // .with_child( - // Label::new( - // user.github_login.clone(), - // theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ); - - // let user_id = user.id; - // let github_login = user.github_login.clone(); - // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - // let button_spacing = theme.contact_button_spacing; - - // if is_incoming { - // row.add_child( - // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - // let button_style = if is_contact_request_pending { - // &theme.disabled_button - // } else { - // theme.contact_button.style_for(mouse_state) - // }; - // render_icon_button(button_style, "icons/x.svg").aligned() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.respond_to_contact_request(user_id, false, cx); - // }) - // .contained() - // .with_margin_right(button_spacing), - // ); - - // row.add_child( - // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - // let button_style = if is_contact_request_pending { - // &theme.disabled_button - // } else { - // theme.contact_button.style_for(mouse_state) - // }; - // render_icon_button(button_style, "icons/check.svg") - // .aligned() - // .flex_float() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.respond_to_contact_request(user_id, true, cx); - // }), - // ); - // } else { - // row.add_child( - // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - // let button_style = if is_contact_request_pending { - // &theme.disabled_button - // } else { - // theme.contact_button.style_for(mouse_state) - // }; - // render_icon_button(button_style, "icons/x.svg") - // .aligned() - // .flex_float() - // }) - // .with_padding(Padding::uniform(2.)) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.remove_contact(user_id, &github_login, cx); - // }) - // .flex_float(), - // ); - // } - - // row.constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style( - // *theme - // .contact_row - // .in_state(is_selected) - // .style_for(&mut Default::default()), - // ) - // .into_any() - // } - // fn has_subchannels(&self, ix: usize) -> bool { // self.entries.get(ix).map_or(false, |entry| { // if let ListEntry::Channel { has_children, .. } = entry { @@ -2054,148 +1882,148 @@ impl CollabPanel { // cx.notify(); // } - // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - // if self.confirm_channel_edit(cx) { - // return; - // } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if self.confirm_channel_edit(cx) { + return; + } - // if let Some(selection) = self.selection { - // if let Some(entry) = self.entries.get(selection) { - // match entry { - // ListEntry::Header(section) => match section { - // Section::ActiveCall => Self::leave_call(cx), - // Section::Channels => self.new_root_channel(cx), - // Section::Contacts => self.toggle_contact_finder(cx), - // Section::ContactRequests - // | Section::Online - // | Section::Offline - // | Section::ChannelInvites => { - // self.toggle_section_expanded(*section, cx); - // } - // }, - // ListEntry::Contact { contact, calling } => { - // if contact.online && !contact.busy && !calling { - // self.call(contact.user.id, 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 let Some(selection) = self.selection { + // if let Some(entry) = self.entries.get(selection) { + // match entry { + // ListEntry::Header(section) => match section { + // Section::ActiveCall => Self::leave_call(cx), + // Section::Channels => self.new_root_channel(cx), + // Section::Contacts => self.toggle_contact_finder(cx), + // Section::ContactRequests + // | Section::Online + // | Section::Offline + // | Section::ChannelInvites => { + // self.toggle_section_expanded(*section, cx); + // } + // }, + // ListEntry::Contact { contact, calling } => { + // if contact.online && !contact.busy && !calling { + // self.call(contact.user.id, Some(self.project.clone()), cx); + // } + // } + // ListEntry::ParticipantProject { + // project_id, + // host_user_id, + // .. + // } => { + // if let Some(workspace) = self.workspace.upgrade(cx) { + // let app_state = workspace.read(cx).app_state().clone(); + // workspace::join_remote_project( + // *project_id, + // *host_user_id, + // app_state, + // cx, + // ) + // .detach_and_log_err(cx); + // } + // } + // ListEntry::ParticipantScreen { peer_id, .. } => { + // let Some(peer_id) = peer_id else { + // return; + // }; + // if let Some(workspace) = self.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // workspace.open_shared_screen(*peer_id, cx) + // }); + // } + // } + // ListEntry::Channel { channel, .. } => { + // let is_active = maybe!({ + // let call_channel = ActiveCall::global(cx) + // .read(cx) + // .room()? + // .read(cx) + // .channel_id()?; - // Some(call_channel == channel.id) - // }) - // .unwrap_or(false); - // if is_active { - // self.open_channel_notes( - // &OpenChannelNotes { - // channel_id: channel.id, - // }, - // cx, - // ) - // } else { - // self.join_channel(channel.id, cx) - // } - // } - // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), - // _ => {} - // } - // } - // } - // } + // 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 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); + 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()); + *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.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 - // } - // } + self.channel_store + .update(cx, |channel_store, cx| { + channel_store.rename(*location, &channel_name, cx) + }) + .detach(); + cx.notify(); + } + } + cx.focus_self(); + true + } else { + false + } + } - // fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { - // if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { - // self.collapsed_sections.remove(ix); - // } else { - // self.collapsed_sections.push(section); - // } - // self.update_entries(false, cx); - // } + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { + if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { + self.collapsed_sections.remove(ix); + } else { + self.collapsed_sections.push(section); + } + self.update_entries(false, cx); + } // fn collapse_selected_channel( // &mut self, @@ -2242,7 +2070,7 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; - // self.serialize(cx); todo!() + self.serialize(cx); self.update_entries(true, cx); cx.notify(); cx.focus_self(); @@ -2270,23 +2098,23 @@ impl CollabPanel { } } - // 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 @@ -2440,44 +2268,38 @@ impl CollabPanel { // // Should move to the filter editor if clicking on it // // Should move selection to the channel editor if activating it - // fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { - // let user_store = self.user_store.clone(); - // let prompt_message = format!( - // "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, @@ -2592,7 +2414,9 @@ impl CollabPanel { } => self .render_channel(&*channel, depth, has_children, is_selected, cx) .into_any_element(), - ListEntry::ChannelEditor { depth } => todo!(), + ListEntry::ChannelEditor { depth } => { + self.render_channel_editor(depth, cx).into_any_element() + } } })) } @@ -2691,10 +2515,7 @@ impl CollabPanel { Some( IconButton::new("add-channel", Icon::Plus) - .on_click(cx.listener(|this, _, cx| { - todo!() - // this.new_root_channel(cx) - })) + .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) .tooltip(|cx| Tooltip::text("Create a channel", cx)), ) } @@ -2709,18 +2530,16 @@ impl CollabPanel { | Section::Offline => true, }; - let mut header = ListHeader::new(text); - if let Some(button) = button { - header = header.right_button(button) - } - // todo!() is selected - if can_collapse { - // todo!() on click to toggle - header = header.toggle(ui::Toggle::Toggled(is_collapsed)); - } - - header + ListHeader::new(text) + .when_some(button, |el, button| el.right_button(button)) + .selected(is_selected) + .when(can_collapse, |el| { + el.toggle(ui::Toggle::Toggled(is_collapsed)).on_toggle( + cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)), + ) + }) } + fn render_contact( &mut self, contact: &Contact, @@ -2744,10 +2563,32 @@ impl CollabPanel { }) .log_err(); })) - .child(Label::new(github_login.clone())); + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(github_login.clone())) + .child( + div() + .id("remove_contact") + .invisible() + .group_hover("", |style| style.visible()) + .child( + IconButton::new("remove_contact", Icon::Close) + .color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener(move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + })), + ), + ), + ); + if let Some(avatar) = contact.user.avatar.clone() { - //item = item.left_avatar(avatar); + item = item.left_avatar(avatar); } + + div().group("").child(item) // let event_handler = // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { // Flex::row() @@ -2776,40 +2617,7 @@ impl CollabPanel { // ) // .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()) @@ -2867,8 +2675,6 @@ impl CollabPanel { // ) // .into_any() // }; - - item } fn render_contact_request( @@ -2879,104 +2685,48 @@ impl CollabPanel { 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 mut item = ListItem::new(github_login.clone()) - .child(Label::new(github_login.clone())) - .on_click(cx.listener(|this, _, cx| { - todo!(); - })); - if let Some(avatar) = user.avatar.clone() { - item = item.left_avatar(avatar); - } - // .with_children(user.avatar.clone().map(|avatar| { - // Image::from_data(avatar) - // .with_style(theme.contact_avatar) - // .aligned() - // .left() - // })) - // .with_child( - // Label::new( - // user.github_login.clone(), - // theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ); + let 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); + })) + .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); + })) + .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); + })) + .color(color) + .tooltip(|cx| Tooltip::text("Cancel invite", cx))] + }; - // let user_id = user.id; - // let github_login = user.github_login.clone(); - // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - // let button_spacing = theme.contact_button_spacing; - - // if is_incoming { - // row.add_child( - // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - // let button_style = if is_contact_request_pending { - // &theme.disabled_button - // } else { - // theme.contact_button.style_for(mouse_state) - // }; - // render_icon_button(button_style, "icons/x.svg").aligned() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.respond_to_contact_request(user_id, false, cx); - // }) - // .contained() - // .with_margin_right(button_spacing), - // ); - - // row.add_child( - // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - // let button_style = if is_contact_request_pending { - // &theme.disabled_button - // } else { - // theme.contact_button.style_for(mouse_state) - // }; - // render_icon_button(button_style, "icons/check.svg") - // .aligned() - // .flex_float() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.respond_to_contact_request(user_id, true, cx); - // }), - // ); - // } else { - // row.add_child( - // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { - // let button_style = if is_contact_request_pending { - // &theme.disabled_button - // } else { - // theme.contact_button.style_for(mouse_state) - // }; - // render_icon_button(button_style, "icons/x.svg") - // .aligned() - // .flex_float() - // }) - // .with_padding(Padding::uniform(2.)) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.remove_contact(user_id, &github_login, cx); - // }) - // .flex_float(), - // ); - // } - - // row.constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style( - // *theme - // .contact_row - // .in_state(is_selected) - // .style_for(&mut Default::default()), - // ) - // .into_any() - item + 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( @@ -2984,33 +2734,11 @@ impl CollabPanel { is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { - ListItem::new("contact-placeholder").child(Label::new("Add a Contact")) - // enum AddContacts {} - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = theme.list_empty_state.style_for(is_selected, state); - // Flex::row() - // .with_child( - // Svg::new("icons/plus.svg") - // .with_color(theme.list_empty_icon.color) - // .constrained() - // .with_width(theme.list_empty_icon.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("Add a contact", style.text.clone()) - // .contained() - // .with_style(theme.list_empty_label_container), - // ) - // .align_children_center() - // .contained() - // .with_style(style.container) - // .into_any() - // }) - // .on_click(MouseButton::Left, |_, this, cx| { - // this.toggle_contact_finder(cx); - // }) - // .into_any() + 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( @@ -3452,6 +3180,32 @@ impl CollabPanel { // .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( @@ -3506,6 +3260,8 @@ impl Render for CollabPanel { div() .key_context("CollabPanel") .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::insert_space)) .map(|el| { if self.user_store.read(cx).current_user().is_none() { el.child(self.render_signed_out(cx)) diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index 48453ada72..31f2764b11 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -181,7 +181,6 @@ impl PickerDelegate for ContactFinderDelegate { ContactRequestStatus::RequestSent => Some("icons/x.svg"), ContactRequestStatus::RequestAccepted => None, }; - dbg!(icon_path); Some( div() .flex_1() 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/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index cdaec6a770..133c3d9bc6 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -59,6 +59,7 @@ impl RenderOnce for IconButton { if let Some(click_handler) = self.on_click { button = button.on_click(move |event, cx| { + cx.stop_propagation(); click_handler(event, cx); }) } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index aa61c8333e..52a3770cbd 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -25,7 +25,9 @@ pub struct ListHeader { left_icon: Option, meta: Option, toggle: Toggle, + on_toggle: Option>, inset: bool, + selected: bool, } impl ListHeader { @@ -36,6 +38,8 @@ impl ListHeader { meta: None, inset: false, toggle: Toggle::NotToggleable, + on_toggle: None, + selected: false, } } @@ -44,6 +48,14 @@ impl ListHeader { self } + pub fn on_toggle( + mut self, + on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + ) -> Self { + self.on_toggle = Some(Rc::new(on_toggle)); + self + } + pub fn left_icon(mut self, left_icon: Option) -> Self { self.left_icon = left_icon; self @@ -57,13 +69,18 @@ impl ListHeader { self.meta = meta; self } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } } impl RenderOnce for ListHeader { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let disclosure_control = disclosure_control(self.toggle, None); + let disclosure_control = disclosure_control(self.toggle, self.on_toggle); let meta = match self.meta { Some(ListHeaderMeta::Tools(icons)) => div().child( @@ -85,6 +102,9 @@ impl RenderOnce for ListHeader { div() .h_5() .when(self.inset, |this| this.px_2()) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) .flex() .flex_1() .items_center() From d010f5f98db5f0c930d4441d058bcf017899fcbd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 27 Nov 2023 12:54:35 +0200 Subject: [PATCH 13/33] Exctract the common code --- crates/project/src/project.rs | 168 ++++++++++++++-------------------- 1 file changed, 69 insertions(+), 99 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cf3fa547f6..d7b10cad7c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -170,11 +170,13 @@ pub struct Project { node: Option>, default_prettier: Option, prettiers_per_worktree: HashMap>>, - prettier_instances: HashMap, Arc>>>>, + prettier_instances: HashMap, } +type PrettierInstance = Shared, Arc>>>; + struct DefaultPrettier { - instance: Option, Arc>>>>, + instance: Option, installation_process: Option>>>>, #[cfg(not(any(test, feature = "test-support")))] installed_plugins: HashSet<&'static str>, @@ -542,6 +544,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 { @@ -4099,14 +4109,6 @@ 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; @@ -4155,46 +4157,10 @@ 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) = + 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()) { @@ -4213,47 +4179,11 @@ 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:#}"); - }, - } - } - } - } + if let Some(new_operation) = + format_with_prettier(&project, buffer, &mut cx).await + { + format_operation = Some(new_operation); + } } }; @@ -8541,12 +8471,7 @@ impl Project { &mut self, buffer: &ModelHandle, cx: &mut ModelContext, - ) -> Task< - Option<( - Option, - Shared, Arc>>>, - )>, - > { + ) -> Task, PrettierInstance)>> { let buffer = buffer.read(cx); let buffer_file = buffer.file(); let Some(buffer_language) = buffer.language() else { @@ -8814,7 +8739,7 @@ fn start_default_prettier( node: Arc, worktree_id: Option, cx: &mut ModelContext<'_, Project>, -) -> Task, Arc>>>> { +) -> Task { cx.spawn(|project, mut cx| async move { loop { let default_prettier_installing = project.update(&mut cx, |project, _| { @@ -8864,7 +8789,7 @@ fn start_prettier( prettier_dir: PathBuf, worktree_id: Option, cx: &mut ModelContext<'_, Project>, -) -> Shared, Arc>>> { +) -> PrettierInstance { cx.spawn(|project, mut cx| async move { let new_server_id = project.update(&mut cx, |project, _| { project.languages.next_language_server_id() @@ -9281,3 +9206,48 @@ fn include_text(server: &lsp::LanguageServer) -> bool { }) .unwrap_or(false) } + +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, _| match &prettier_path { + Some(prettier_path) => { + log::error!("Failed to create prettier instance from {prettier_path:?} for buffer: {e:#}"); + project.prettier_instances.remove(prettier_path); + } + None => { + log::error!("Failed to create default prettier instance from {prettier_path:?} for buffer: {e:#}"); + if let Some(default_prettier) = project.default_prettier.as_mut() { + default_prettier.instance = None; + } + } + }); + } + } + } + + None +} From c288c6eaf9e063fa50c38455eec79c9bab61a0f0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 27 Nov 2023 15:13:10 +0200 Subject: [PATCH 14/33] Use enum variants for prettier installation and startup phases --- crates/prettier/src/prettier.rs | 4 + crates/project/src/project.rs | 444 ++++++++++++++++---------------- 2 files changed, 227 insertions(+), 221 deletions(-) diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index cb9d32d0b0..61a6656ea4 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 LAUNCH_THRESHOLD: usize = 5; 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/project/src/project.rs b/crates/project/src/project.rs index d7b10cad7c..52b011cb2a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -168,20 +168,54 @@ 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, } -type PrettierInstance = Shared, Arc>>>; +type PrettierInstance = Shared>; + +#[derive(Clone)] +enum PrettierProcess { + Running(Arc), + Stopped { start_attempts: usize }, +} struct DefaultPrettier { - instance: Option, - installation_process: Option>>>>, - #[cfg(not(any(test, feature = "test-support")))] + prettier: PrettierInstallation, installed_plugins: HashSet<&'static str>, } +enum PrettierInstallation { + NotInstalled { + attempts: usize, + installation_process: Option>>>>, + }, + Installed(PrettierInstance), +} + +impl Default for DefaultPrettier { + fn default() -> Self { + Self { + prettier: PrettierInstallation::NotInstalled { + attempts: 0, + installation_process: None, + }, + installed_plugins: HashSet::default(), + } + } +} + +impl DefaultPrettier { + fn instance(&self) -> Option<&PrettierInstance> { + if let PrettierInstallation::Installed(instance) = &self.prettier { + Some(instance) + } else { + None + } + } +} + struct DelayedDebounced { task: Option>, cancel_channel: Option>, @@ -700,7 +734,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(), } @@ -801,7 +835,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(), }; @@ -6521,55 +6555,45 @@ impl Project { 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::>(); + 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 { - for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { + let _: Vec<()> = 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) + if let PrettierProcess::Running(prettier) = prettier_task.await { + if let Err(e) = prettier + .clear_cache() + .await { + 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 - { - if let Err(e) = task_result { - log::error!("Failed to clear cache for prettier: {e:#}"); - } - } + .await; }) .detach(); } @@ -8514,19 +8538,23 @@ impl Project { .entry(worktree_id) .or_default() .insert(None); - project.default_prettier.as_ref().and_then( - |default_prettier| default_prettier.instance.clone(), - ) + project.default_prettier.instance().cloned() }); match started_default_prettier { - Some(old_task) => return Some((None, old_task)), + Some(old_task) => { + dbg!("Old prettier was found!"); + return Some((None, old_task)); + } None => { + dbg!("starting new default prettier"); let new_default_prettier = project .update(&mut cx, |_, cx| { start_default_prettier(node, Some(worktree_id), cx) }) + .log_err() .await; - return Some((None, new_default_prettier)); + dbg!("started a default prettier"); + return Some((None, new_default_prettier?)); } } } @@ -8565,52 +8593,37 @@ impl Project { Some((Some(prettier_dir), new_prettier_task)) } Err(e) => { - return Some(( - None, - Task::ready(Err(Arc::new( - e.context("determining prettier path"), - ))) - .shared(), - )); + log::error!("Failed to determine prettier path for buffer: {e:#}"); + return None; } } }); } - 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)) }); - } + None => match self.default_prettier.instance().cloned() { + Some(old_task) => return Task::ready(Some((None, old_task))), + None => { + let new_task = start_default_prettier(node, None, cx).log_err(); + 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(), - ))) + return Task::ready(None); } } - #[cfg(any(test, feature = "test-support"))] - fn install_default_formatters( - &mut self, - _worktree: Option, - _new_language: &Language, - _language_settings: &LanguageSettings, - _cx: &mut ModelContext, - ) { - } + // TODO kb uncomment + // #[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")))] + // #[cfg(not(any(test, feature = "test-support")))] fn install_default_formatters( &mut self, worktree: Option, @@ -8660,78 +8673,86 @@ impl Project { 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)); + plugins_to_install + .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); + let mut installation_attempts = 0; + let previous_installation_process = match &self.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_process, + attempts, + } => { + installation_attempts = *attempts; + installation_process.clone() + } + PrettierInstallation::Installed { .. } => { if plugins_to_install.is_empty() { return; } - default_prettier.installation_process.clone() - } else { None - }; + } + }; + + if installation_attempts > prettier::LAUNCH_THRESHOLD { + log::warn!( + "Default prettier installation has failed {installation_attempts} times, not attempting again", + ); + return; + } + 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 { + self.default_prettier.prettier = PrettierInstallation::NotInstalled { + attempts: installation_attempts + 1, + 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, _| { plugins_to_install.retain(|plugin| { - !default_prettier.installed_plugins.contains(plugin) + !this.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); }); + 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, cx| { + this.default_prettier.prettier = + PrettierInstallation::Installed( + cx.spawn(|_, _| async move { + PrettierProcess::Stopped { start_attempts: 0 } + }) + .shared(), + ); + this.default_prettier + .installed_plugins + .extend(installed_plugins); + }); + } } } - } - Ok(()) - }) - .shared(), - ); + Ok(()) + }) + .shared(), + ), + }; } } @@ -8739,48 +8760,40 @@ fn start_default_prettier( node: Arc, worktree_id: Option, cx: &mut ModelContext<'_, Project>, -) -> Task { +) -> Task> { 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; + let installation_process = project.update(&mut cx, |project, _| { + match &project.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_process, + .. + } => ControlFlow::Continue(installation_process.clone()), + PrettierInstallation::Installed(default_prettier) => { + ControlFlow::Break(default_prettier.clone()) } } - None => break, + }); + + match installation_process { + ControlFlow::Continue(installation_process) => { + if let Some(installation_process) = installation_process.clone() { + if let Err(e) = installation_process.await { + 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(new_default_prettier.clone()); + new_default_prettier + }); + return Ok(new_default_prettier); + } + ControlFlow::Break(prettier) => return Ok(prettier), } } - - 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 - } - } - }) }) } @@ -8794,13 +8807,18 @@ fn start_prettier( 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) + + match Prettier::start(new_server_id, prettier_dir.clone(), node, cx.clone()).await { + Ok(new_prettier) => { + register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); + PrettierProcess::Running(Arc::new(new_prettier)) + } + Err(e) => { + log::error!("Failed to start prettier in dir {prettier_dir:?}: {e:#}"); + // TODO kb increment + PrettierProcess::Stopped { start_attempts: 1 } + } + } }) .shared() } @@ -8855,7 +8873,6 @@ fn register_new_prettier( } } -#[cfg(not(any(test, feature = "test-support")))] async fn install_default_prettier( plugins_to_install: HashSet<&'static str>, node: Arc, @@ -9218,34 +9235,19 @@ async fn format_with_prettier( }) .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:#}" - ); - } + // TODO kb re-insert incremented value here? + if let PrettierProcess::Running(prettier) = prettier_task.await { + 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, _| match &prettier_path { - Some(prettier_path) => { - log::error!("Failed to create prettier instance from {prettier_path:?} for buffer: {e:#}"); - project.prettier_instances.remove(prettier_path); - } - None => { - log::error!("Failed to create default prettier instance from {prettier_path:?} for buffer: {e:#}"); - if let Some(default_prettier) = project.default_prettier.as_mut() { - default_prettier.instance = None; - } - } - }); - } } } From e7e56757dc1e0e4356c170887758f58a92c75461 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 00:40:01 +0200 Subject: [PATCH 15/33] Limit prettier installation and start attempts --- crates/project/src/project.rs | 291 +++++++++++++++++++++++----------- 1 file changed, 198 insertions(+), 93 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 52b011cb2a..13cee48fed 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -173,12 +173,59 @@ pub struct Project { prettier_instances: HashMap, } -type PrettierInstance = Shared>; +type PrettierTask = Shared, Arc>>>; #[derive(Clone)] -enum PrettierProcess { - Running(Arc), - Stopped { start_attempts: usize }, +struct PrettierInstance { + attempt: usize, + prettier: Option, +} + +impl PrettierInstance { + fn prettier_task( + &mut self, + node: &Arc, + prettier_dir: Option<&Path>, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, + ) -> Option>> { + if self.attempt > prettier::LAUNCH_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 + }) + } + }, + }) + } } struct DefaultPrettier { @@ -214,6 +261,24 @@ impl DefaultPrettier { None } } + + fn prettier_task( + &mut self, + node: &Arc, + worktree_id: Option, + cx: &mut ModelContext<'_, Project>, + ) -> Option>> { + match &mut self.prettier { + PrettierInstallation::NotInstalled { .. } => { + // `start_default_prettier` will start the installation process if it's not already running and wait for it to finish + let new_task = start_default_prettier(Arc::clone(node), worktree_id, cx); + Some(cx.spawn(|_, _| async move { new_task.await })) + } + PrettierInstallation::Installed(existing_instance) => { + existing_instance.prettier_task(node, None, worktree_id, cx) + } + } + } } struct DelayedDebounced { @@ -6575,20 +6640,23 @@ impl Project { cx.background() .spawn(async move { - let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { + let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| { async move { - if let PrettierProcess::Running(prettier) = prettier_task.await { - if let Err(e) = prettier - .clear_cache() - .await { - 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:#}" - ), - } + 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:#}" + ), + } + }, } } } @@ -8495,7 +8563,7 @@ impl Project { &mut self, buffer: &ModelHandle, cx: &mut ModelContext, - ) -> Task, PrettierInstance)>> { + ) -> Task, PrettierTask)>> { let buffer = buffer.read(cx); let buffer_file = buffer.file(); let Some(buffer_language) = buffer.language() else { @@ -8531,32 +8599,19 @@ impl Project { 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.instance().cloned() - }); - match started_default_prettier { - Some(old_task) => { - dbg!("Old prettier was found!"); - return Some((None, old_task)); - } - None => { - dbg!("starting new default prettier"); - let new_default_prettier = project - .update(&mut cx, |_, cx| { - start_default_prettier(node, Some(worktree_id), cx) - }) - .log_err() - .await; - dbg!("started a default prettier"); - return Some((None, new_default_prettier?)); - } - } + 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, _| { @@ -8566,15 +8621,27 @@ impl Project { .or_default() .insert(Some(prettier_dir.clone())) }); - if let Some(existing_prettier) = - project.update(&mut cx, |project, _| { - project.prettier_instances.get(&prettier_dir).cloned() + 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), existing_prettier)); + return Some(( + Some(prettier_dir), + prettier_task?.await.log_err()?, + )); } log::info!("Found prettier in {prettier_dir:?}, starting."); @@ -8585,9 +8652,13 @@ impl Project { Some(worktree_id), cx, ); - project - .prettier_instances - .insert(prettier_dir.clone(), new_prettier_task.clone()); + 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)) @@ -8599,13 +8670,11 @@ impl Project { } }); } - None => match self.default_prettier.instance().cloned() { - Some(old_task) => return Task::ready(Some((None, old_task))), - None => { - let new_task = start_default_prettier(node, None, cx).log_err(); - return cx.spawn(|_, _| async move { Some((None, new_task.await?)) }); - } - }, + 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); @@ -8727,20 +8796,19 @@ impl Project { if needs_install { let installed_plugins = plugins_to_install.clone(); cx.background() + // TODO kb instead of always installing, try to start the existing installation first? .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, cx| { + this.update(&mut cx, |this, _| { this.default_prettier.prettier = - PrettierInstallation::Installed( - cx.spawn(|_, _| async move { - PrettierProcess::Stopped { start_attempts: 0 } - }) - .shared(), - ); + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); this.default_prettier .installed_plugins .extend(installed_plugins); @@ -8760,20 +8828,24 @@ fn start_default_prettier( node: Arc, worktree_id: Option, cx: &mut ModelContext<'_, Project>, -) -> Task> { +) -> Task> { cx.spawn(|project, mut cx| async move { loop { + let mut install_attempt = 0; let installation_process = project.update(&mut cx, |project, _| { match &project.default_prettier.prettier { PrettierInstallation::NotInstalled { installation_process, - .. - } => ControlFlow::Continue(installation_process.clone()), + attempts + } => { + install_attempt = *attempts; + ControlFlow::Continue(installation_process.clone())}, PrettierInstallation::Installed(default_prettier) => { ControlFlow::Break(default_prettier.clone()) } } }); + install_attempt += 1; match installation_process { ControlFlow::Continue(installation_process) => { @@ -8786,12 +8858,26 @@ fn start_default_prettier( let new_default_prettier = start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); project.default_prettier.prettier = - PrettierInstallation::Installed(new_default_prettier.clone()); + PrettierInstallation::Installed(PrettierInstance { attempt: install_attempt, prettier: Some(new_default_prettier.clone()) }); new_default_prettier }); return Ok(new_default_prettier); } - ControlFlow::Break(prettier) => return Ok(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); + }, + } + }, } } }) @@ -8802,23 +8888,19 @@ fn start_prettier( prettier_dir: PathBuf, worktree_id: Option, cx: &mut ModelContext<'_, Project>, -) -> PrettierInstance { +) -> PrettierTask { cx.spawn(|project, mut cx| async move { let new_server_id = project.update(&mut cx, |project, _| { project.languages.next_language_server_id() }); - match Prettier::start(new_server_id, prettier_dir.clone(), node, cx.clone()).await { - Ok(new_prettier) => { - register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); - PrettierProcess::Running(Arc::new(new_prettier)) - } - Err(e) => { - log::error!("Failed to start prettier in dir {prettier_dir:?}: {e:#}"); - // TODO kb increment - PrettierProcess::Stopped { start_attempts: 1 } - } - } + 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() } @@ -9235,19 +9317,42 @@ async fn format_with_prettier( }) .await { - // TODO kb re-insert incremented value here? - if let PrettierProcess::Running(prettier) = prettier_task.await { - 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:#}" - ); + 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; + } + }), } } From eab347630473257cb543f6373c75c6f025fe8fd2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 12:09:55 +0200 Subject: [PATCH 16/33] Split prettier code off to a separate module --- crates/project/src/prettier_support.rs | 708 +++++++++++++++++++++++++ crates/project/src/project.rs | 703 +----------------------- 2 files changed, 721 insertions(+), 690 deletions(-) create mode 100644 crates/project/src/prettier_support.rs diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs new file mode 100644 index 0000000000..f59ff20a24 --- /dev/null +++ b/crates/project/src/prettier_support.rs @@ -0,0 +1,708 @@ +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(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_process: Option>>>>, + }, + 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_process: None, + }, + 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 { .. } => { + // `start_default_prettier` will start the installation process if it's not already running and wait for it to finish + let new_task = start_default_prettier(Arc::clone(node), worktree_id, cx); + Some(cx.spawn(|_, _| async move { new_task.await })) + } + 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::LAUNCH_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 mut install_attempt = 0; + let installation_process = project.update(&mut cx, |project, _| { + match &project.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_process, + attempts + } => { + install_attempt = *attempts; + ControlFlow::Continue(installation_process.clone())}, + PrettierInstallation::Installed(default_prettier) => { + ControlFlow::Break(default_prettier.clone()) + } + } + }); + install_attempt += 1; + + match installation_process { + ControlFlow::Continue(installation_process) => { + if let Some(installation_process) = installation_process.clone() { + if let Err(e) = installation_process.await { + 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: install_attempt, 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 { + 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_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(()) +} + +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, + _new_language: &Language, + language_settings: &LanguageSettings, + _cx: &mut ModelContext, + ) { + // suppress unused code warnings + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return, + }; + let _ = &self.default_prettier.installed_plugins; + } + + #[cfg(not(any(test, feature = "test-support")))] + pub fn install_default_prettier( + &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; + plugins_to_install + .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); + let mut installation_attempts = 0; + let previous_installation_process = match &self.default_prettier.prettier { + PrettierInstallation::NotInstalled { + installation_process, + attempts, + } => { + installation_attempts = *attempts; + installation_process.clone() + } + PrettierInstallation::Installed { .. } => { + if plugins_to_install.is_empty() { + return; + } + None + } + }; + + if installation_attempts > prettier::LAUNCH_THRESHOLD { + log::warn!( + "Default prettier installation has failed {installation_attempts} times, not attempting again", + ); + return; + } + + let fs = Arc::clone(&self.fs); + self.default_prettier.prettier = PrettierInstallation::NotInstalled { + attempts: installation_attempts + 1, + 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, _| { + plugins_to_install.retain(|plugin| { + !this.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() + // TODO kb instead of always installing, try to start the existing installation first? + .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, _| { + this.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); + this.default_prettier + .installed_plugins + .extend(installed_plugins); + }); + } + } + } + Ok(()) + }) + .shared(), + ), + }; + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 13cee48fed..ad2a13482b 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::*; @@ -173,114 +169,6 @@ pub struct Project { prettier_instances: HashMap, } -type PrettierTask = Shared, Arc>>>; - -#[derive(Clone)] -struct PrettierInstance { - attempt: usize, - prettier: Option, -} - -impl PrettierInstance { - fn prettier_task( - &mut self, - node: &Arc, - prettier_dir: Option<&Path>, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, - ) -> Option>> { - if self.attempt > prettier::LAUNCH_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 - }) - } - }, - }) - } -} - -struct DefaultPrettier { - prettier: PrettierInstallation, - installed_plugins: HashSet<&'static str>, -} - -enum PrettierInstallation { - NotInstalled { - attempts: usize, - installation_process: Option>>>>, - }, - Installed(PrettierInstance), -} - -impl Default for DefaultPrettier { - fn default() -> Self { - Self { - prettier: PrettierInstallation::NotInstalled { - attempts: 0, - installation_process: None, - }, - installed_plugins: HashSet::default(), - } - } -} - -impl DefaultPrettier { - fn instance(&self) -> Option<&PrettierInstance> { - if let PrettierInstallation::Installed(instance) = &self.prettier { - Some(instance) - } else { - None - } - } - - fn prettier_task( - &mut self, - node: &Arc, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, - ) -> Option>> { - match &mut self.prettier { - PrettierInstallation::NotInstalled { .. } => { - // `start_default_prettier` will start the installation process if it's not already running and wait for it to finish - let new_task = start_default_prettier(Arc::clone(node), worktree_id, cx); - Some(cx.spawn(|_, _| async move { new_task.await })) - } - PrettierInstallation::Installed(existing_instance) => { - existing_instance.prettier_task(node, None, worktree_id, cx) - } - } - } -} - struct DelayedDebounced { task: Option>, cancel_channel: Option>, @@ -1038,7 +926,7 @@ impl Project { } for (worktree, language, settings) in language_formatters_to_check { - self.install_default_formatters(worktree, &language, &settings, cx); + self.install_default_prettier(worktree, &language, &settings, cx); } // Start all the newly-enabled language servers. @@ -2795,7 +2683,7 @@ impl Project { 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); + self.install_default_prettier(worktree, &new_language, &settings, cx); if let Some(file) = buffer_file { let worktree = file.worktree.clone(); if let Some(tree) = worktree.read(cx).as_local() { @@ -4257,7 +4145,8 @@ impl Project { } (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { if let Some(new_operation) = - format_with_prettier(&project, buffer, &mut cx).await + prettier_support::format_with_prettier(&project, buffer, &mut cx) + .await { format_operation = Some(new_operation); } else if let Some((language_server, buffer_abs_path)) = @@ -4279,7 +4168,8 @@ impl Project { } (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { if let Some(new_operation) = - format_with_prettier(&project, buffer, &mut cx).await + prettier_support::format_with_prettier(&project, buffer, &mut cx) + .await { format_operation = Some(new_operation); } @@ -6595,78 +6485,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.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(); - } - } - 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)?; @@ -8558,448 +8376,6 @@ impl Project { Vec::new() } } - - 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); - } - } - - // TODO kb uncomment - // #[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; - plugins_to_install - .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); - let mut installation_attempts = 0; - let previous_installation_process = match &self.default_prettier.prettier { - PrettierInstallation::NotInstalled { - installation_process, - attempts, - } => { - installation_attempts = *attempts; - installation_process.clone() - } - PrettierInstallation::Installed { .. } => { - if plugins_to_install.is_empty() { - return; - } - None - } - }; - - if installation_attempts > prettier::LAUNCH_THRESHOLD { - log::warn!( - "Default prettier installation has failed {installation_attempts} times, not attempting again", - ); - return; - } - - let fs = Arc::clone(&self.fs); - self.default_prettier.prettier = PrettierInstallation::NotInstalled { - attempts: installation_attempts + 1, - 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, _| { - plugins_to_install.retain(|plugin| { - !this.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() - // TODO kb instead of always installing, try to start the existing installation first? - .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, _| { - this.default_prettier.prettier = - PrettierInstallation::Installed(PrettierInstance { - attempt: 0, - prettier: None, - }); - this.default_prettier - .installed_plugins - .extend(installed_plugins); - }); - } - } - } - Ok(()) - }) - .shared(), - ), - }; - } -} - -fn start_default_prettier( - node: Arc, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, -) -> Task> { - cx.spawn(|project, mut cx| async move { - loop { - let mut install_attempt = 0; - let installation_process = project.update(&mut cx, |project, _| { - match &project.default_prettier.prettier { - PrettierInstallation::NotInstalled { - installation_process, - attempts - } => { - install_attempt = *attempts; - ControlFlow::Continue(installation_process.clone())}, - PrettierInstallation::Installed(default_prettier) => { - ControlFlow::Break(default_prettier.clone()) - } - } - }); - install_attempt += 1; - - match installation_process { - ControlFlow::Continue(installation_process) => { - if let Some(installation_process) = installation_process.clone() { - if let Err(e) = installation_process.await { - 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: install_attempt, 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 { - 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_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( @@ -9305,56 +8681,3 @@ fn include_text(server: &lsp::LanguageServer) -> bool { }) .unwrap_or(false) } - -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 -} From 938f2531c40a766d67d3f6a95fc95a99fce9b98c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 16:31:24 +0200 Subject: [PATCH 17/33] Always write prettier server file --- crates/project/src/prettier_support.rs | 92 ++++++++++++++------------ 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index f59ff20a24..32f32cc0dc 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -193,51 +193,53 @@ fn start_default_prettier( ) -> Task> { cx.spawn(|project, mut cx| async move { loop { - let mut install_attempt = 0; let installation_process = project.update(&mut cx, |project, _| { match &project.default_prettier.prettier { PrettierInstallation::NotInstalled { installation_process, - attempts - } => { - install_attempt = *attempts; - ControlFlow::Continue(installation_process.clone())}, + .. + } => ControlFlow::Continue(installation_process.clone()), PrettierInstallation::Installed(default_prettier) => { ControlFlow::Break(default_prettier.clone()) } } }); - install_attempt += 1; - match installation_process { - ControlFlow::Continue(installation_process) => { - if let Some(installation_process) = installation_process.clone() { - if let Err(e) = installation_process.await { - anyhow::bail!("Cannot start default prettier due to its installation failure: {e:#}"); - } + ControlFlow::Continue(None) => { + anyhow::bail!("Default prettier is not installed and cannot be started") + } + ControlFlow::Continue(Some(installation_process)) => { + if let Err(e) = installation_process.await { + 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: install_attempt, prettier: Some(new_default_prettier.clone()) }); + 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); - }, + 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); } }, } @@ -322,21 +324,6 @@ async fn install_default_prettier( 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 { @@ -364,6 +351,23 @@ async fn install_default_prettier( 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, @@ -662,7 +666,10 @@ impl Project { .map_err(Arc::new)? { ControlFlow::Break(()) => return Ok(()), - ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()), + ControlFlow::Continue(Some(_non_default_prettier)) => { + save_prettier_server_file(fs.as_ref()).await?; + return Ok(()); + } ControlFlow::Continue(None) => { let mut needs_install = match previous_installation_process { Some(previous_installation_process) => { @@ -681,6 +688,7 @@ impl Project { cx.background() // TODO kb instead of always installing, try to start the existing installation first? .spawn(async move { + save_prettier_server_file(fs.as_ref()).await?; install_default_prettier(plugins_to_install, node, fs).await }) .await From 46ac82f49862992074c9835e509e7f643edd3ace Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 18:45:12 +0200 Subject: [PATCH 18/33] Do not attempt to run default prettier if it's not installed yet --- crates/project/src/prettier_support.rs | 180 +++++++++++++------------ 1 file changed, 96 insertions(+), 84 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 32f32cc0dc..04e56d2b8a 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -86,7 +86,7 @@ pub struct DefaultPrettier { pub enum PrettierInstallation { NotInstalled { attempts: usize, - installation_process: Option>>>>, + installation_task: Option>>>>, }, Installed(PrettierInstance), } @@ -104,7 +104,7 @@ impl Default for DefaultPrettier { Self { prettier: PrettierInstallation::NotInstalled { attempts: 0, - installation_process: None, + installation_task: None, }, installed_plugins: HashSet::default(), } @@ -128,9 +128,7 @@ impl DefaultPrettier { ) -> Option>> { match &mut self.prettier { PrettierInstallation::NotInstalled { .. } => { - // `start_default_prettier` will start the installation process if it's not already running and wait for it to finish - let new_task = start_default_prettier(Arc::clone(node), worktree_id, cx); - Some(cx.spawn(|_, _| async move { new_task.await })) + Some(start_default_prettier(Arc::clone(node), worktree_id, cx)) } PrettierInstallation::Installed(existing_instance) => { existing_instance.prettier_task(node, None, worktree_id, cx) @@ -193,23 +191,31 @@ fn start_default_prettier( ) -> Task> { cx.spawn(|project, mut cx| async move { loop { - let installation_process = project.update(&mut cx, |project, _| { + let installation_task = project.update(&mut cx, |project, _| { match &project.default_prettier.prettier { PrettierInstallation::NotInstalled { - installation_process, - .. - } => ControlFlow::Continue(installation_process.clone()), + installation_task, .. + } => ControlFlow::Continue(installation_task.clone()), PrettierInstallation::Installed(default_prettier) => { ControlFlow::Break(default_prettier.clone()) } } }); - match installation_process { + match installation_task { ControlFlow::Continue(None) => { anyhow::bail!("Default prettier is not installed and cannot be started") } - ControlFlow::Continue(Some(installation_process)) => { - if let Err(e) = installation_process.await { + 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, .. + } = &mut project.default_prettier.prettier + { + *installation_task = None; + } + }); anyhow::bail!( "Cannot start default prettier due to its installation failure: {e:#}" ); @@ -254,6 +260,7 @@ fn start_prettier( 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() }); @@ -319,10 +326,9 @@ fn register_new_prettier( } } -async fn install_default_prettier( +async fn install_prettier_packages( plugins_to_install: HashSet<&'static str>, node: Arc, - fs: Arc, ) -> anyhow::Result<()> { let packages_to_versions = future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( @@ -563,23 +569,24 @@ impl Project { } } - #[cfg(any(test, feature = "test-support"))] - pub fn install_default_prettier( - &mut self, - _worktree: Option, - _new_language: &Language, - language_settings: &LanguageSettings, - _cx: &mut ModelContext, - ) { - // suppress unused code warnings - match &language_settings.formatter { - Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return, - }; - let _ = &self.default_prettier.installed_plugins; - } + // TODO kb uncomment + // #[cfg(any(test, feature = "test-support"))] + // pub fn install_default_prettier( + // &mut self, + // _worktree: Option, + // _new_language: &Language, + // language_settings: &LanguageSettings, + // _cx: &mut ModelContext, + // ) { + // // suppress unused code warnings + // match &language_settings.formatter { + // Formatter::Prettier { .. } | Formatter::Auto => {} + // Formatter::LanguageServer | Formatter::External { .. } => return, + // }; + // let _ = &self.default_prettier.installed_plugins; + // } - #[cfg(not(any(test, feature = "test-support")))] + // #[cfg(not(any(test, feature = "test-support")))] pub fn install_default_prettier( &mut self, worktree: Option, @@ -632,13 +639,13 @@ impl Project { plugins_to_install .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); let mut installation_attempts = 0; - let previous_installation_process = match &self.default_prettier.prettier { + let previous_installation_task = match &self.default_prettier.prettier { PrettierInstallation::NotInstalled { - installation_process, + installation_task, attempts, } => { installation_attempts = *attempts; - installation_process.clone() + installation_task.clone() } PrettierInstallation::Installed { .. } => { if plugins_to_install.is_empty() { @@ -656,61 +663,66 @@ impl Project { } let fs = Arc::clone(&self.fs); - self.default_prettier.prettier = PrettierInstallation::NotInstalled { - attempts: installation_attempts + 1, - 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)) => { - save_prettier_server_file(fs.as_ref()).await?; - return Ok(()); - } - ControlFlow::Continue(None) => { - let mut needs_install = match previous_installation_process { - Some(previous_installation_process) => { - previous_installation_process.await.is_err() + let new_installation_task = 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)) => { + save_prettier_server_file(fs.as_ref()).await?; + return Ok(()); + } + ControlFlow::Continue(None) => { + let mut needs_install = match previous_installation_task { + Some(previous_installation_task) => { + match previous_installation_task.await { + Ok(()) => false, + Err(e) => { + log::error!("Failed to install default prettier: {e:#}"); + true + } } - None => true, - }; - this.update(&mut cx, |this, _| { - plugins_to_install.retain(|plugin| { - !this.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() - // TODO kb instead of always installing, try to start the existing installation first? - .spawn(async move { - save_prettier_server_file(fs.as_ref()).await?; - install_default_prettier(plugins_to_install, node, fs).await - }) - .await - .context("prettier & plugins install") - .map_err(Arc::new)?; - this.update(&mut cx, |this, _| { - this.default_prettier.prettier = - PrettierInstallation::Installed(PrettierInstance { - attempt: 0, - prettier: None, - }); - this.default_prettier - .installed_plugins - .extend(installed_plugins); - }); } + None => true, + }; + this.update(&mut cx, |this, _| { + plugins_to_install.retain(|plugin| { + !this.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 { + save_prettier_server_file(fs.as_ref()).await?; + install_prettier_packages(plugins_to_install, node).await + }) + .await + .context("prettier & plugins install") + .map_err(Arc::new)?; + this.update(&mut cx, |this, _| { + this.default_prettier.prettier = + PrettierInstallation::Installed(PrettierInstance { + attempt: 0, + prettier: None, + }); + this.default_prettier + .installed_plugins + .extend(installed_plugins); + }); } } - Ok(()) - }) - .shared(), - ), + } + Ok(()) + }) + .shared(); + self.default_prettier.prettier = PrettierInstallation::NotInstalled { + attempts: installation_attempts + 1, + installation_task: Some(new_installation_task), }; } } From 465e53ef41d636601fe8b5a292e3786e97caee53 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 18:48:46 +0200 Subject: [PATCH 19/33] Always install default prettier --- crates/project/src/prettier_support.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 04e56d2b8a..de256192b4 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -671,11 +671,7 @@ impl Project { .map_err(Arc::new)? { ControlFlow::Break(()) => return Ok(()), - ControlFlow::Continue(Some(_non_default_prettier)) => { - save_prettier_server_file(fs.as_ref()).await?; - return Ok(()); - } - ControlFlow::Continue(None) => { + ControlFlow::Continue(_) => { let mut needs_install = match previous_installation_task { Some(previous_installation_task) => { match previous_installation_task.await { From 43d28cc0c1b22c4bef81532e0764c13a39568759 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 18:56:16 +0200 Subject: [PATCH 20/33] Ignore `initialized` LSP request in prettier wrapper --- crates/prettier/src/prettier_server.js | 3 +++ 1 file changed, 3 insertions(+) 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)}`); } From 64259e4a0b106efc4dc50583fe71a34669ca5a08 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 21:18:35 +0200 Subject: [PATCH 21/33] Properly increment installation attempts --- crates/project/src/prettier_support.rs | 46 +++++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index de256192b4..ab99eed346 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -210,10 +210,12 @@ fn start_default_prettier( if let Err(e) = installation_task.await { project.update(&mut cx, |project, _| { if let PrettierInstallation::NotInstalled { - installation_task, .. + installation_task, + attempts, } = &mut project.default_prettier.prettier { *installation_task = None; + *attempts += 1; } }); anyhow::bail!( @@ -654,17 +656,9 @@ impl Project { None } }; - - if installation_attempts > prettier::LAUNCH_THRESHOLD { - log::warn!( - "Default prettier installation has failed {installation_attempts} times, not attempting again", - ); - return; - } - let fs = Arc::clone(&self.fs); let new_installation_task = cx - .spawn(|this, mut cx| async move { + .spawn(|project, mut cx| async move { match locate_prettier_installation .await .context("locate prettier installation") @@ -677,6 +671,18 @@ impl Project { match previous_installation_task.await { Ok(()) => false, Err(e) => { + project.update(&mut cx, |project, _| { + if let PrettierInstallation::NotInstalled { + attempts, + .. + } = &mut project.default_prettier.prettier + { + *attempts += 1; + installation_attempts = *attempts; + } else { + installation_attempts += 1; + } + }); log::error!("Failed to install default prettier: {e:#}"); true } @@ -684,9 +690,17 @@ impl Project { } None => true, }; - this.update(&mut cx, |this, _| { + + if installation_attempts > prettier::LAUNCH_THRESHOLD { + log::warn!( + "Default prettier installation has failed {installation_attempts} times, not attempting again", + ); + return Ok(()); + } + + project.update(&mut cx, |project, _| { plugins_to_install.retain(|plugin| { - !this.default_prettier.installed_plugins.contains(plugin) + !project.default_prettier.installed_plugins.contains(plugin) }); needs_install |= !plugins_to_install.is_empty(); }); @@ -700,13 +714,13 @@ impl Project { .await .context("prettier & plugins install") .map_err(Arc::new)?; - this.update(&mut cx, |this, _| { - this.default_prettier.prettier = + project.update(&mut cx, |project, _| { + project.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance { attempt: 0, prettier: None, }); - this.default_prettier + project.default_prettier .installed_plugins .extend(installed_plugins); }); @@ -717,7 +731,7 @@ impl Project { }) .shared(); self.default_prettier.prettier = PrettierInstallation::NotInstalled { - attempts: installation_attempts + 1, + attempts: installation_attempts, installation_task: Some(new_installation_task), }; } From acd1aec86206ac8d977b88f63e82ca3ee0c46f27 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 22:16:16 +0200 Subject: [PATCH 22/33] Properly determine default prettier plugins to install --- crates/project/src/prettier_support.rs | 109 +++++++++++++------------ 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index ab99eed346..35ba1d0f8b 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -87,6 +87,7 @@ pub enum PrettierInstallation { NotInstalled { attempts: usize, installation_task: Option>>>>, + not_installed_plugins: HashSet<&'static str>, }, Installed(PrettierInstance), } @@ -105,6 +106,7 @@ impl Default for DefaultPrettier { prettier: PrettierInstallation::NotInstalled { attempts: 0, installation_task: None, + not_installed_plugins: HashSet::default(), }, installed_plugins: HashSet::default(), } @@ -212,6 +214,7 @@ fn start_default_prettier( if let PrettierInstallation::NotInstalled { installation_task, attempts, + .. } = &mut project.default_prettier.prettier { *installation_task = None; @@ -571,24 +574,23 @@ impl Project { } } - // TODO kb uncomment - // #[cfg(any(test, feature = "test-support"))] - // pub fn install_default_prettier( - // &mut self, - // _worktree: Option, - // _new_language: &Language, - // language_settings: &LanguageSettings, - // _cx: &mut ModelContext, - // ) { - // // suppress unused code warnings - // match &language_settings.formatter { - // Formatter::Prettier { .. } | Formatter::Auto => {} - // Formatter::LanguageServer | Formatter::External { .. } => return, - // }; - // let _ = &self.default_prettier.installed_plugins; - // } + #[cfg(any(test, feature = "test-support"))] + pub fn install_default_prettier( + &mut self, + _worktree: Option, + _new_language: &Language, + language_settings: &LanguageSettings, + _cx: &mut ModelContext, + ) { + // suppress unused code warnings + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return, + }; + let _ = &self.default_prettier.installed_plugins; + } - // #[cfg(not(any(test, feature = "test-support")))] + #[cfg(not(any(test, feature = "test-support")))] pub fn install_default_prettier( &mut self, worktree: Option, @@ -637,25 +639,28 @@ impl Project { } None => Task::ready(Ok(ControlFlow::Break(()))), }; - let mut plugins_to_install = prettier_plugins; - plugins_to_install + let mut new_plugins = prettier_plugins; + new_plugins .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); - let mut installation_attempts = 0; + let mut installation_attempt = 0; let previous_installation_task = match &self.default_prettier.prettier { PrettierInstallation::NotInstalled { installation_task, attempts, + not_installed_plugins } => { - installation_attempts = *attempts; + installation_attempt = *attempts; + new_plugins.extend(not_installed_plugins.iter()); installation_task.clone() } PrettierInstallation::Installed { .. } => { - if plugins_to_install.is_empty() { + 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 { @@ -665,51 +670,48 @@ impl Project { .map_err(Arc::new)? { ControlFlow::Break(()) => return Ok(()), - ControlFlow::Continue(_) => { - let mut needs_install = match previous_installation_task { - Some(previous_installation_task) => { - match previous_installation_task.await { - Ok(()) => false, - Err(e) => { - project.update(&mut cx, |project, _| { - if let PrettierInstallation::NotInstalled { - attempts, - .. - } = &mut project.default_prettier.prettier - { - *attempts += 1; - installation_attempts = *attempts; - } else { - installation_attempts += 1; - } - }); - log::error!("Failed to install default prettier: {e:#}"); - true + 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 (attempt {installation_attempt}): {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; } - } + }) } - None => true, }; - - if installation_attempts > prettier::LAUNCH_THRESHOLD { + if installation_attempt > prettier::LAUNCH_THRESHOLD { log::warn!( - "Default prettier installation has failed {installation_attempts} times, not attempting again", + "Default prettier installation has failed {installation_attempt} times, not attempting again", ); return Ok(()); } - project.update(&mut cx, |project, _| { - plugins_to_install.retain(|plugin| { + new_plugins.retain(|plugin| { !project.default_prettier.installed_plugins.contains(plugin) }); - needs_install |= !plugins_to_install.is_empty(); + 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 = plugins_to_install.clone(); + let installed_plugins = new_plugins.clone(); cx.background() .spawn(async move { save_prettier_server_file(fs.as_ref()).await?; - install_prettier_packages(plugins_to_install, node).await + install_prettier_packages(new_plugins, node).await }) .await .context("prettier & plugins install") @@ -731,8 +733,9 @@ impl Project { }) .shared(); self.default_prettier.prettier = PrettierInstallation::NotInstalled { - attempts: installation_attempts, + attempts: installation_attempt, installation_task: Some(new_installation_task), + not_installed_plugins: plugins_to_install, }; } } From 96f6b89508367d564e3a1ef3698e54fff1aae441 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 23:12:46 +0200 Subject: [PATCH 23/33] Clear failed installation task when error threshold gets exceeded --- crates/prettier/src/prettier.rs | 2 +- crates/project/src/prettier_support.rs | 27 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 61a6656ea4..0886d68747 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -34,7 +34,7 @@ pub struct TestPrettier { default: bool, } -pub const LAUNCH_THRESHOLD: usize = 5; +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/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 35ba1d0f8b..a61589b8b4 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -147,7 +147,7 @@ impl PrettierInstance { worktree_id: Option, cx: &mut ModelContext<'_, Project>, ) -> Option>> { - if self.attempt > prettier::LAUNCH_THRESHOLD { + if self.attempt > prettier::FAIL_THRESHOLD { match prettier_dir { Some(prettier_dir) => log::warn!( "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting" @@ -643,13 +643,20 @@ impl Project { new_plugins .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); let mut installation_attempt = 0; - let previous_installation_task = match &self.default_prettier.prettier { + 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() } @@ -660,6 +667,7 @@ impl Project { None } }; + let plugins_to_install = new_plugins.clone(); let fs = Arc::clone(&self.fs); let new_installation_task = cx @@ -677,20 +685,25 @@ impl Project { 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 (attempt {installation_attempt}): {e:#}"); + 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::LAUNCH_THRESHOLD { + 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 has failed {installation_attempt} times, not attempting again", + "Default prettier installation had failed {installation_attempt} times, not attempting again", ); return Ok(()); } From f1314afe357e1f3ed05cc6f37c34765c330048e6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 28 Nov 2023 23:23:05 +0200 Subject: [PATCH 24/33] Simplify default prettier installation function --- crates/project/src/prettier_support.rs | 53 +++++++++++--------------- crates/project/src/project.rs | 13 +++++-- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index a61589b8b4..314c571fd8 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -25,6 +25,26 @@ 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, @@ -578,15 +598,10 @@ impl Project { pub fn install_default_prettier( &mut self, _worktree: Option, - _new_language: &Language, - language_settings: &LanguageSettings, + _plugins: HashSet<&'static str>, _cx: &mut ModelContext, ) { // suppress unused code warnings - match &language_settings.formatter { - Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return, - }; let _ = &self.default_prettier.installed_plugins; } @@ -594,33 +609,12 @@ impl Project { pub fn install_default_prettier( &mut self, worktree: Option, - new_language: &Language, - language_settings: &LanguageSettings, + mut new_plugins: HashSet<&'static str>, 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) @@ -637,9 +631,8 @@ impl Project { .await }) } - None => Task::ready(Ok(ControlFlow::Break(()))), + None => Task::ready(Ok(ControlFlow::Continue(None))), }; - let mut new_plugins = prettier_plugins; new_plugins .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); let mut installation_attempt = 0; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ad2a13482b..a4ee9fb552 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -925,8 +925,14 @@ impl Project { .detach(); } + let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language, settings) in language_formatters_to_check { - self.install_default_prettier(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. @@ -2682,8 +2688,9 @@ 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_prettier(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() { From 6e44f53ea15cd770a814601cde23031e59ea167b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 29 Nov 2023 11:33:29 +0200 Subject: [PATCH 25/33] Style fixes --- crates/project/src/prettier_support.rs | 12 ++++++++---- crates/project/src/project.rs | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 314c571fd8..063c4abcc4 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -25,7 +25,10 @@ use crate::{ Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId, }; -pub fn prettier_plugins_for_language(language: &Language, language_settings: &LanguageSettings) -> Option> { +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, @@ -603,6 +606,8 @@ impl Project { ) { // suppress unused code warnings let _ = &self.default_prettier.installed_plugins; + let _ = install_prettier_packages; + let _ = save_prettier_server_file; } #[cfg(not(any(test, feature = "test-support")))] @@ -633,14 +638,13 @@ impl Project { } None => Task::ready(Ok(ControlFlow::Continue(None))), }; - new_plugins - .retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin)); + 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 + not_installed_plugins, } => { installation_attempt = *attempts; if installation_attempt > prettier::FAIL_THRESHOLD { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a4ee9fb552..a2ad82585e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -927,8 +927,13 @@ impl Project { let mut prettier_plugins_by_worktree = HashMap::default(); for (worktree, language, settings) in language_formatters_to_check { - 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); + 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 { @@ -2688,7 +2693,9 @@ 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)); - if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(&new_language, &settings) { + 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 { @@ -4077,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. @@ -4106,7 +4111,7 @@ impl Project { // 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) @@ -4173,7 +4178,7 @@ impl Project { )); } } - (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { + (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => { if let Some(new_operation) = prettier_support::format_with_prettier(&project, buffer, &mut cx) .await From 3796e7eecb4b922de599b815c87a2bd3808485c9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 29 Nov 2023 11:48:10 +0200 Subject: [PATCH 26/33] Port to gpui2 --- crates/prettier2/src/prettier2.rs | 4 + crates/prettier2/src/prettier_server.js | 3 + crates/project2/src/prettier_support.rs | 765 ++++++++++++++++++++++++ crates/project2/src/project2.rs | 728 ++-------------------- 4 files changed, 823 insertions(+), 677 deletions(-) create mode 100644 crates/project2/src/prettier_support.rs 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/project2/src/prettier_support.rs b/crates/project2/src/prettier_support.rs new file mode 100644 index 0000000000..15f3028d85 --- /dev/null +++ b/crates/project2/src/prettier_support.rs @@ -0,0 +1,765 @@ +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 _ = &self.default_prettier.installed_plugins; + let _ = install_prettier_packages; + let _ = save_prettier_server_file; + } + + #[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; + }; + 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)?; + 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( From 3e3b64bb1cf4a642d85618f8e058c8255f26ee90 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 29 Nov 2023 12:10:41 +0200 Subject: [PATCH 27/33] Fix the tests --- crates/project/src/prettier_support.rs | 9 +++++++-- crates/project2/src/prettier_support.rs | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 063c4abcc4..1b0faff3e9 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -601,13 +601,18 @@ impl Project { pub fn install_default_prettier( &mut self, _worktree: Option, - _plugins: HashSet<&'static str>, + plugins: HashSet<&'static str>, _cx: &mut ModelContext, ) { // suppress unused code warnings - let _ = &self.default_prettier.installed_plugins; 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")))] diff --git a/crates/project2/src/prettier_support.rs b/crates/project2/src/prettier_support.rs index 15f3028d85..e9ce8d9a06 100644 --- a/crates/project2/src/prettier_support.rs +++ b/crates/project2/src/prettier_support.rs @@ -615,13 +615,18 @@ impl Project { pub fn install_default_prettier( &mut self, _worktree: Option, - _plugins: HashSet<&'static str>, + plugins: HashSet<&'static str>, _cx: &mut ModelContext, ) { // suppress unused code warnings - let _ = &self.default_prettier.installed_plugins; 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")))] From d92153218cee5c1869437863a29c55513ce72d49 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 29 Nov 2023 13:44:19 +0200 Subject: [PATCH 28/33] Log prettier installation start & success --- crates/project/src/prettier_support.rs | 2 ++ crates/project2/src/prettier_support.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 1b0faff3e9..c438f294b6 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -625,6 +625,7 @@ impl Project { 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) @@ -731,6 +732,7 @@ impl Project { .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 { diff --git a/crates/project2/src/prettier_support.rs b/crates/project2/src/prettier_support.rs index e9ce8d9a06..c176c79a91 100644 --- a/crates/project2/src/prettier_support.rs +++ b/crates/project2/src/prettier_support.rs @@ -639,6 +639,7 @@ impl Project { 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) @@ -745,6 +746,7 @@ impl Project { .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 { From f735f5287e7364fecac1e86c00e558f83bc34aa1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 29 Nov 2023 11:08:32 -0500 Subject: [PATCH 29/33] v0.116.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c41daa031..f02c748fbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11554,7 +11554,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.115.0" +version = "0.116.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 297785fe9f..7d8289e867 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.115.0" +version = "0.116.0" publish = false [lib] From 35481e2c79b0ec43776255ff19aa2744ccce07ba Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 29 Nov 2023 10:05:31 -0700 Subject: [PATCH 30/33] Move padding on uniform list inside the scrollable area --- crates/gpui2/src/elements/uniform_list.rs | 6 ++---- crates/picker2/src/picker2.rs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) 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/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 672cb12466..44056dabd1 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -205,7 +205,6 @@ impl Render for Picker { .when(self.delegate.match_count() > 0, |el| { el.child( v_stack() - .p_1() .grow() .child( uniform_list( @@ -239,7 +238,8 @@ impl Render for Picker { } }, ) - .track_scroll(self.scroll_handle.clone()), + .track_scroll(self.scroll_handle.clone()) + .p_1() ) .max_h_72() .overflow_hidden(), From a8bf0834e6afde50107b4bc067dd86a00141f86a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 29 Nov 2023 12:23:09 -0500 Subject: [PATCH 31/33] =?UTF-8?q?Button2=20=E2=80=93=20Part1=20(#3420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## TODO - [x] Remove `InteractionState` - [ ] `Selectable` should use `Selection` instead of a boolean - [x] Clean out ui2 prelude - [ ] Build out button2 button types - [ ] Port old buttons Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/collab_ui2/src/collab_titlebar_item.rs | 26 +- crates/storybook2/src/stories/focus.rs | 2 +- crates/storybook2/src/stories/picker.rs | 2 +- crates/storybook2/src/stories/scroll.rs | 2 +- crates/storybook2/src/story_selector.rs | 2 - crates/ui2/src/clickable.rs | 5 + crates/ui2/src/components.rs | 4 +- crates/ui2/src/components/button2.rs | 413 ++++++++++++++++++ crates/ui2/src/components/icon_button.rs | 18 +- crates/ui2/src/components/input.rs | 108 ----- crates/ui2/src/components/stories.rs | 3 - crates/ui2/src/components/stories/button.rs | 164 ++----- crates/ui2/src/components/stories/input.rs | 18 - crates/ui2/src/fixed.rs | 6 + crates/ui2/src/prelude.rs | 59 +-- crates/ui2/src/selectable.rs | 26 ++ crates/ui2/src/styles/color.rs | 2 +- crates/ui2/src/ui2.rs | 6 + crates/workspace2/src/dock.rs | 24 +- crates/workspace2/src/pane.rs | 12 +- crates/workspace2/src/status_bar.rs | 2 +- crates/workspace2/src/toolbar.rs | 2 +- 22 files changed, 567 insertions(+), 339 deletions(-) create mode 100644 crates/ui2/src/clickable.rs create mode 100644 crates/ui2/src/components/button2.rs delete mode 100644 crates/ui2/src/components/input.rs delete mode 100644 crates/ui2/src/components/stories/input.rs create mode 100644 crates/ui2/src/fixed.rs create mode 100644 crates/ui2/src/selectable.rs diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2307ba2fcb..ac72176d67 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -37,7 +37,10 @@ use gpui::{ }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; +use ui::{ + h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton, + IconElement, IconSize, KeyBinding, Tooltip, +}; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem { }) .detach(); })) + // Temporary, will be removed when the last part of button2 is merged + .child( + div().border().border_color(gpui::blue()).child( + ButtonLike::new("test-button") + .children([ + Avatar::uri( + "https://avatars.githubusercontent.com/u/1714999?v=4", + ) + .into_element() + .into_any(), + IconElement::new(ui::Icon::ChevronDown) + .size(IconSize::Small) + .into_element() + .into_any(), + ]) + .on_click(move |event, _cx| { + dbg!(format!("clicked: {:?}", event.down.position)); + }) + .tooltip(|cx| Tooltip::text("Test tooltip", cx)), + ), + ) } }) } diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 77aa057b09..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); diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 75eb0d88e7..75aa7aed05 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -4,7 +4,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use theme2::ActiveTheme; +use ui::prelude::*; use ui::{Label, ListItem}; pub struct PickerStory { diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index 297e65d411..300aae1144 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,5 +1,5 @@ use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; -use theme2::ActiveTheme; +use ui::prelude::*; use ui::Tooltip; pub struct ScrollStory; diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 9945b2e7ef..1c0890e4be 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -19,7 +19,6 @@ pub enum ComponentStory { Focus, Icon, IconButton, - Input, Keybinding, Label, ListItem, @@ -39,7 +38,6 @@ impl ComponentStory { Self::Focus => FocusStory::view(cx).into(), Self::Icon => cx.build_view(|_| ui::IconStory).into(), Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(), - Self::Input => cx.build_view(|_| ui::InputStory).into(), Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), Self::Label => cx.build_view(|_| ui::LabelStory).into(), Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(), diff --git a/crates/ui2/src/clickable.rs b/crates/ui2/src/clickable.rs new file mode 100644 index 0000000000..b25f6b0e70 --- /dev/null +++ b/crates/ui2/src/clickable.rs @@ -0,0 +1,5 @@ +use gpui::{ClickEvent, WindowContext}; + +pub trait Clickable { + 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..9dc061e31f 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -1,12 +1,12 @@ mod avatar; mod button; +mod button2; mod checkbox; mod context_menu; mod disclosure; mod divider; mod icon; mod icon_button; -mod input; mod keybinding; mod label; mod list; @@ -21,13 +21,13 @@ mod stories; pub use avatar::*; pub use button::*; +pub use button2::*; pub use checkbox::*; 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::*; diff --git a/crates/ui2/src/components/button2.rs b/crates/ui2/src/components/button2.rs new file mode 100644 index 0000000000..e9192afea0 --- /dev/null +++ b/crates/ui2/src/components/button2.rs @@ -0,0 +1,413 @@ +use gpui::{ + rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful, + StatefulInteractiveElement, WindowContext, +}; +use smallvec::SmallVec; + +use crate::{h_stack, prelude::*}; + +// 🚧 Heavily WIP 🚧 + +// #[derive(Default, PartialEq, Clone, Copy)] +// pub enum ButtonType2 { +// #[default] +// DefaultButton, +// IconButton, +// ButtonLike, +// SplitButton, +// ToggleButton, +// } + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum IconPosition2 { + #[default] + Before, + After, +} + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum ButtonStyle2 { + #[default] + Filled, + // Tinted, + Subtle, + Transparent, +} + +#[derive(Debug, Clone, Copy)] +pub struct ButtonStyle { + pub background: Hsla, + pub border_color: Hsla, + pub label_color: Hsla, + pub icon_color: Hsla, +} + +impl ButtonStyle2 { + pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle { + match self { + ButtonStyle2::Filled => ButtonStyle { + background: cx.theme().colors().element_background, + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Subtle => ButtonStyle { + background: cx.theme().colors().ghost_element_background, + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Transparent => ButtonStyle { + background: gpui::transparent_black(), + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + } + } + + pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle { + match self { + ButtonStyle2::Filled => ButtonStyle { + background: cx.theme().colors().element_hover, + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Subtle => ButtonStyle { + background: cx.theme().colors().ghost_element_hover, + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Transparent => ButtonStyle { + background: gpui::transparent_black(), + border_color: gpui::transparent_black(), + // TODO: These are not great + label_color: Color::Muted.color(cx), + // TODO: These are not great + icon_color: Color::Muted.color(cx), + }, + } + } + + pub fn active(self, cx: &mut WindowContext) -> ButtonStyle { + match self { + ButtonStyle2::Filled => ButtonStyle { + background: cx.theme().colors().element_active, + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Subtle => ButtonStyle { + background: cx.theme().colors().ghost_element_active, + border_color: gpui::transparent_black(), + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Transparent => ButtonStyle { + background: gpui::transparent_black(), + border_color: gpui::transparent_black(), + // TODO: These are not great + label_color: Color::Muted.color(cx), + // TODO: These are not great + icon_color: Color::Muted.color(cx), + }, + } + } + + pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle { + match self { + ButtonStyle2::Filled => ButtonStyle { + background: cx.theme().colors().element_background, + border_color: cx.theme().colors().border_focused, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Subtle => ButtonStyle { + background: cx.theme().colors().ghost_element_background, + border_color: cx.theme().colors().border_focused, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, + ButtonStyle2::Transparent => ButtonStyle { + background: gpui::transparent_black(), + border_color: cx.theme().colors().border_focused, + label_color: Color::Accent.color(cx), + icon_color: Color::Accent.color(cx), + }, + } + } + + pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle { + match self { + ButtonStyle2::Filled => ButtonStyle { + background: cx.theme().colors().element_disabled, + border_color: cx.theme().colors().border_disabled, + label_color: Color::Disabled.color(cx), + icon_color: Color::Disabled.color(cx), + }, + ButtonStyle2::Subtle => ButtonStyle { + background: cx.theme().colors().ghost_element_disabled, + border_color: cx.theme().colors().border_disabled, + label_color: Color::Disabled.color(cx), + icon_color: Color::Disabled.color(cx), + }, + ButtonStyle2::Transparent => ButtonStyle { + background: gpui::transparent_black(), + border_color: gpui::transparent_black(), + label_color: Color::Disabled.color(cx), + icon_color: Color::Disabled.color(cx), + }, + } + } +} + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum ButtonSize2 { + #[default] + Default, + Compact, + None, +} + +impl ButtonSize2 { + fn height(self) -> Rems { + match self { + ButtonSize2::Default => rems(22. / 16.), + ButtonSize2::Compact => rems(18. / 16.), + ButtonSize2::None => rems(16. / 16.), + } + } +} + +// pub struct Button { +// id: ElementId, +// icon: Option, +// icon_color: Option, +// icon_position: Option, +// label: Option