Merge branch 'main' into project-panel-context-menu

Max Brunsfeld created

Change summary

Cargo.lock                                           |  50 
Cargo.toml                                           |   2 
crates/call2/src/call2.rs                            |  51 
crates/client2/src/client2.rs                        |   8 
crates/collab2/src/tests/channel_tests.rs            |  37 
crates/collab2/src/tests/integration_tests.rs        |  11 
crates/collab_ui2/src/collab_panel.rs                | 850 +++++--------
crates/collab_ui2/src/collab_panel/contact_finder.rs |   1 
crates/collab_ui2/src/collab_titlebar_item.rs        |  26 
crates/command_palette2/src/command_palette.rs       |  10 
crates/feature_flags2/src/feature_flags2.rs          |   4 
crates/gpui2/src/action.rs                           |   1 
crates/gpui2/src/element.rs                          |  44 
crates/gpui2/src/elements/uniform_list.rs            |   6 
crates/gpui2/src/window.rs                           |  17 
crates/picker2/src/picker2.rs                        |  29 
crates/prettier/src/prettier.rs                      |   4 
crates/prettier/src/prettier_server.js               |   3 
crates/prettier2/src/prettier2.rs                    |   4 
crates/prettier2/src/prettier_server.js              |   3 
crates/project/src/prettier_support.rs               | 758 ++++++++++++
crates/project/src/project.rs                        | 686 ----------
crates/project2/src/prettier_support.rs              | 772 ++++++++++++
crates/project2/src/project2.rs                      | 728 -----------
crates/project_panel2/src/file_associations.rs       |  71 
crates/project_panel2/src/project_panel.rs           |   6 
crates/search2/src/search_bar.rs                     |   6 
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/theme2/src/registry.rs                        |   4 
crates/theme_selector2/Cargo.toml                    |  29 
crates/theme_selector2/src/theme_selector.rs         | 276 ++++
crates/ui2/src/clickable.rs                          |   5 
crates/ui2/src/components.rs                         |   4 
crates/ui2/src/components/button.rs                  |  15 
crates/ui2/src/components/button2.rs                 | 413 ++++++
crates/ui2/src/components/disclosure.rs              |  25 
crates/ui2/src/components/icon_button.rs             |  55 
crates/ui2/src/components/input.rs                   | 108 -
crates/ui2/src/components/list.rs                    |  78 
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/components/stories/list_item.rs       |  24 
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/welcome2/Cargo.toml                           |  37 
crates/welcome2/src/base_keymap_picker.rs            | 208 +++
crates/welcome2/src/base_keymap_setting.rs           |  65 +
crates/welcome2/src/welcome.rs                       | 281 ++++
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 
crates/workspace2/src/workspace2.rs                  |  32 
crates/zed/Cargo.toml                                |   2 
crates/zed2/Cargo.toml                               |   4 
crates/zed2/src/main.rs                              | 200 +-
script/crate-dep-graph                               |   2 
64 files changed, 3,895 insertions(+), 2,492 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9471,6 +9471,27 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "theme_selector2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "feature_flags2",
+ "fs2",
+ "fuzzy2",
+ "gpui2",
+ "log",
+ "parking_lot 0.11.2",
+ "picker2",
+ "postage",
+ "settings2",
+ "smol",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.48"
@@ -11054,6 +11075,31 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "welcome2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "db2",
+ "editor2",
+ "fs2",
+ "fuzzy2",
+ "gpui2",
+ "install_cli2",
+ "log",
+ "picker2",
+ "project2",
+ "schemars",
+ "serde",
+ "settings2",
+ "theme2",
+ "theme_selector2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -11508,7 +11554,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.115.0"
+version = "0.116.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -11720,6 +11766,7 @@ dependencies = [
  "terminal_view2",
  "text2",
  "theme2",
+ "theme_selector2",
  "thiserror",
  "tiny_http",
  "toml 0.5.11",
@@ -11757,6 +11804,7 @@ dependencies = [
  "urlencoding",
  "util",
  "uuid 1.4.1",
+ "welcome2",
  "workspace2",
  "zed_actions2",
 ]

Cargo.toml 🔗

@@ -107,6 +107,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",

crates/call2/src/call2.rs 🔗

@@ -14,8 +14,8 @@ use client::{
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
-    View, ViewContext, VisualContext, WeakModel, WeakView,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
+    Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
 };
 pub use participant::ParticipantLocation;
 use postage::watch;
@@ -334,12 +334,55 @@ impl ActiveCall {
     pub fn join_channel(
         &mut self,
         channel_id: u64,
+        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Model<Room>>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
-                return Task::ready(Ok(Some(room)));
-            } else {
+                return cx.spawn(|_, _| async move {
+                    todo!();
+                    // let future = room.update(&mut cx, |room, cx| {
+                    //     room.most_active_project(cx).map(|(host, project)| {
+                    //         room.join_project(project, host, app_state.clone(), cx)
+                    //     })
+                    // })
+
+                    // if let Some(future) = future {
+                    //     future.await?;
+                    // }
+
+                    // Ok(Some(room))
+                });
+            }
+
+            let should_prompt = room.update(cx, |room, _| {
+                room.channel_id().is_some()
+                    && room.is_sharing_project()
+                    && room.remote_participants().len() > 0
+            });
+            if should_prompt && requesting_window.is_some() {
+                return cx.spawn(|this, mut cx| async move {
+                    let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            "Leaving this call will unshare your current project.\nDo you want to switch channels?",
+                            &["Yes, Join Channel", "Cancel"],
+                        )
+                    })?;
+                    if answer.await? == 1 {
+                        return Ok(None);
+                    }
+
+                    room.update(&mut cx, |room, cx| room.clear_state(cx))?;
+
+                    this.update(&mut cx, |this, cx| {
+                        this.join_channel(channel_id, requesting_window, cx)
+                    })?
+                    .await
+                });
+            }
+
+            if room.read(cx).channel_id().is_some() {
                 room.update(cx, |room, cx| room.clear_state(cx));
             }
         }

crates/client2/src/client2.rs 🔗

@@ -693,8 +693,8 @@ impl Client {
         }
     }
 
-    pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
-        read_credentials_from_keychain(cx).await.is_some()
+    pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
+        read_credentials_from_keychain(cx).is_some()
     }
 
     #[async_recursion(?Send)]
@@ -725,7 +725,7 @@ impl Client {
         let mut read_from_keychain = false;
         let mut credentials = self.state.read().credentials.clone();
         if credentials.is_none() && try_keychain {
-            credentials = read_credentials_from_keychain(cx).await;
+            credentials = read_credentials_from_keychain(cx);
             read_from_keychain = credentials.is_some();
         }
         if credentials.is_none() {
@@ -1324,7 +1324,7 @@ impl Client {
     }
 }
 
-async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     if IMPERSONATE_LOGIN.is_some() {
         return None;
     }

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();
 

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)

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;
@@ -90,10 +91,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 {
@@ -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, 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, 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<String> {
         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<dyn Fs>,
     focus_handle: FocusHandle,
     // channel_clipboard: Option<ChannelMoveClipboard>,
-    // pending_serialization: Task<Option<()>>,
+    pending_serialization: Task<Option<()>>,
     // context_menu: ViewHandle<ContextMenu>,
     filter_editor: View<Editor>,
-    // channel_name_editor: ViewHandle<Editor>,
+    channel_name_editor: View<Editor>,
     channel_editing_state: Option<ChannelEditingState>,
     entries: Vec<ListEntry>,
     selection: Option<usize>,
@@ -322,17 +323,17 @@ pub struct CollabPanel {
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
     collapsed_channels: Vec<ChannelId>,
-    // drag_target_channel: ChannelDragTarget,
+    drag_target_channel: ChannelDragTarget,
     workspace: WeakView<Workspace>,
     // 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 {
@@ -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,
-            //                 )
-            //             });
-
-            //             cx.subscribe(&channel_name_editor, |this, _, event, cx| {
-            //                 if let editor::Event::Blurred = event {
-            //                     if let Some(state) = &this.channel_editing_state {
-            //                         if state.pending_name().is_some() {
-            //                             return;
-            //                         }
-            //                     }
-            //                     this.take_editing_state(cx);
-            //                     this.update_entries(false, cx);
-            //                     cx.notify();
-            //                 }
-            //             })
-            //             .detach();
+            let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
+
+            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
+                if let editor::EditorEvent::Blurred = event {
+                    if let Some(state) = &this.channel_editing_state {
+                        if state.pending_name().is_some() {
+                            return;
+                        }
+                    }
+                    this.take_editing_state(cx);
+                    this.update_entries(false, cx);
+                    cx.notify();
+                }
+            })
+            .detach();
 
             //             let list_state =
             //                 ListState::<Self>::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,
@@ -614,59 +608,58 @@ 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,
             };
 
             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::<SettingsStore, _>(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::<SettingsStore>(
+                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::<ChannelsAlpha, _>(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::<ChannelsAlpha, _>(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<Self>) {
-    //         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<Self>) {
+        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<Self>) {
         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<Self>) -> 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<Self>) -> 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<Self> {
-    //         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>,
-    //         user_store: ModelHandle<UserStore>,
-    //         theme: &theme::CollabPanel,
-    //         is_incoming: bool,
-    //         is_selected: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         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::<Decline, _>(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::<Accept, _>(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::<Cancel, _>(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<Self>) {
-    //         if self.confirm_channel_edit(cx) {
-    //             return;
-    //         }
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        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()?;
-
-    //                             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),
-    //                     _ => {}
-    //                 }
-    //             }
-    //         }
-    //     }
+        // if let Some(selection) = self.selection {
+        //     if let Some(entry) = self.entries.get(selection) {
+        //         match entry {
+        //             ListEntry::Header(section) => match section {
+        //                 Section::ActiveCall => Self::leave_call(cx),
+        //                 Section::Channels => self.new_root_channel(cx),
+        //                 Section::Contacts => self.toggle_contact_finder(cx),
+        //                 Section::ContactRequests
+        //                 | Section::Online
+        //                 | Section::Offline
+        //                 | Section::ChannelInvites => {
+        //                     self.toggle_section_expanded(*section, cx);
+        //                 }
+        //             },
+        //             ListEntry::Contact { contact, calling } => {
+        //                 if contact.online && !contact.busy && !calling {
+        //                     self.call(contact.user.id, Some(self.project.clone()), cx);
+        //                 }
+        //             }
+        //             ListEntry::ParticipantProject {
+        //                 project_id,
+        //                 host_user_id,
+        //                 ..
+        //             } => {
+        //                 if let Some(workspace) = self.workspace.upgrade(cx) {
+        //                     let app_state = workspace.read(cx).app_state().clone();
+        //                     workspace::join_remote_project(
+        //                         *project_id,
+        //                         *host_user_id,
+        //                         app_state,
+        //                         cx,
+        //                     )
+        //                     .detach_and_log_err(cx);
+        //                 }
+        //             }
+        //             ListEntry::ParticipantScreen { peer_id, .. } => {
+        //                 let Some(peer_id) = peer_id else {
+        //                     return;
+        //                 };
+        //                 if let Some(workspace) = self.workspace.upgrade(cx) {
+        //                     workspace.update(cx, |workspace, cx| {
+        //                         workspace.open_shared_screen(*peer_id, cx)
+        //                     });
+        //                 }
+        //             }
+        //             ListEntry::Channel { channel, .. } => {
+        //                 let is_active = maybe!({
+        //                     let call_channel = ActiveCall::global(cx)
+        //                         .read(cx)
+        //                         .room()?
+        //                         .read(cx)
+        //                         .channel_id()?;
+
+        //                     Some(call_channel == channel.id)
+        //                 })
+        //                 .unwrap_or(false);
+        //                 if is_active {
+        //                     self.open_channel_notes(
+        //                         &OpenChannelNotes {
+        //                             channel_id: channel.id,
+        //                         },
+        //                         cx,
+        //                     )
+        //                 } else {
+        //                     self.join_channel(channel.id, cx)
+        //                 }
+        //             }
+        //             ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
+        //             _ => {}
+        //         }
+        //     }
+        // }
+    }
 
-    //     fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
-    //         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<Self>) {
+        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<CollabPanel>) -> 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<CollabPanel>) -> 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.rename(*location, &channel_name, cx)
-    //                         })
-    //                         .detach();
-    //                     cx.notify();
-    //                 }
-    //             }
-    //             cx.focus_self();
-    //             true
-    //         } else {
-    //             false
-    //         }
-    //     }
+                    self.channel_store
+                        .update(cx, |channel_store, cx| {
+                            channel_store.create_channel(&channel_name, *location, cx)
+                        })
+                        .detach();
+                    cx.notify();
+                }
+                ChannelEditingState::Rename {
+                    location,
+                    pending_name,
+                } => {
+                    if pending_name.is_some() {
+                        return false;
+                    }
+                    let channel_name = self.channel_name_editor.read(cx).text(cx);
+                    *pending_name = Some(channel_name.clone());
+
+                    self.channel_store
+                        .update(cx, |channel_store, cx| {
+                            channel_store.rename(*location, &channel_name, cx)
+                        })
+                        .detach();
+                    cx.notify();
+                }
+            }
+            cx.focus_self();
+            true
+        } else {
+            false
+        }
+    }
 
-    //     fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
-    //         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<Self>) {
+        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,
@@ -2233,20 +2061,20 @@ impl CollabPanel {
     //         self.toggle_channel_collapsed(action.location, cx);
     //     }
 
-    //     fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-    //         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<Self>) {
+        match self.collapsed_channels.binary_search(&channel_id) {
+            Ok(ix) => {
+                self.collapsed_channels.remove(ix);
+            }
+            Err(ix) => {
+                self.collapsed_channels.insert(ix, channel_id);
+            }
+        };
+        self.serialize(cx);
+        self.update_entries(true, cx);
+        cx.notify();
+        cx.focus_self();
+    }
 
     fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
         self.collapsed_channels.binary_search(&channel_id).is_ok()
@@ -2270,23 +2098,23 @@ impl CollabPanel {
         }
     }
 
-    //     fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
-    //         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>) {
+        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>) {
     //         self.collapsed_channels
@@ -2346,11 +2174,12 @@ impl CollabPanel {
     //         }
     //     }
 
-    //     fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
-    //         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<Self>) {
+        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<Self>) {
     //         let Some(channel) = self.selected_channel() else {
@@ -2439,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<Self>) {
-    //         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<Self>) {
+        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>,
-    //     ) {
-    //         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>,
+    ) {
+        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,
@@ -2504,21 +2327,22 @@ impl CollabPanel {
     //             .detach_and_log_err(cx);
     //     }
 
-    //     fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
-    //         let Some(workspace) = self.workspace.upgrade(cx) else {
-    //             return;
-    //         };
-    //         let Some(handle) = cx.window().downcast::<Workspace>() 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<Self>) {
+        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
+            return;
+        };
+        let active_call = ActiveCall::global(cx);
+        cx.spawn(|_, mut cx| async move {
+            active_call
+                .update(&mut cx, |active_call, cx| {
+                    active_call.join_channel(channel_id, Some(handle), cx)
+                })
+                .log_err()?
+                .await
+                .notify_async_err(&mut cx)
+        })
+        .detach()
+    }
 
     //     fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
     //         let channel_id = action.channel_id;

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)),
+                        ),
+                    )
                 }
             })
     }

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,3 +1,8 @@
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
+
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -5,10 +10,7 @@ use gpui::{
     Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
-use std::{
-    cmp::{self, Reverse},
-    sync::Arc,
-};
+
 use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},

crates/feature_flags2/src/feature_flags2.rs 🔗

@@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> {
 
 impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
 where
-    V: 'static + Send + Sync,
+    V: 'static,
 {
     fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
     where
-        F: Fn(bool, &mut V, &mut ViewContext<V>) + Send + Sync + 'static,
+        F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
     {
         self.observe_global::<FeatureFlags>(move |v, cx| {
             let feature_flags = cx.global::<FeatureFlags>();

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;
     };
 

crates/gpui2/src/element.rs 🔗

@@ -111,7 +111,7 @@ pub struct Component<C> {
 
 pub struct CompositeElementState<C: RenderOnce> {
     rendered_element: Option<<C::Rendered as IntoElement>::Element>,
-    rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
+    rendered_element_state: Option<<<C::Rendered as IntoElement>::Element as Element>::State>,
 }
 
 impl<C> Component<C> {
@@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> {
         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<Pixels>, 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,
+            );
+        }
     }
 }
 

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

crates/gpui2/src/window.rs 🔗

@@ -1939,23 +1939,6 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         })
     }
 
-    /// 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<S, R>(
-        &mut self,
-        element_id: Option<ElementId>,
-        f: impl FnOnce(Option<S>, &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<Pixels> {
         self.window()

crates/picker2/src/picker2.rs 🔗

@@ -1,7 +1,8 @@
 use editor::Editor;
 use gpui::{
-    div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
-    MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
+    div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
+    MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
+    WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Color, Divider, Label};
@@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> {
 
 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<Picker<Self>>);
@@ -205,7 +205,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
             .when(self.delegate.match_count() > 0, |el| {
                 el.child(
                     v_stack()
-                        .p_1()
                         .grow()
                         .child(
                             uniform_list(
@@ -239,7 +238,8 @@ impl<D: PickerDelegate> Render for Picker<D> {
                                     }
                                 },
                             )
-                            .track_scroll(self.scroll_handle.clone()),
+                            .track_scroll(self.scroll_handle.clone())
+                            .p_1()
                         )
                         .max_h_72()
                         .overflow_hidden(),
@@ -256,3 +256,22 @@ impl<D: PickerDelegate> Render for Picker<D> {
             })
     }
 }
+
+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()
+}

crates/prettier/src/prettier.rs 🔗

@@ -13,12 +13,14 @@ use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
 
+#[derive(Clone)]
 pub enum Prettier {
     Real(RealPrettier),
     #[cfg(any(test, feature = "test-support"))]
     Test(TestPrettier),
 }
 
+#[derive(Clone)]
 pub struct RealPrettier {
     default: bool,
     prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier {
 }
 
 #[cfg(any(test, feature = "test-support"))]
+#[derive(Clone)]
 pub struct TestPrettier {
     prettier_dir: PathBuf,
     default: bool,
 }
 
+pub const FAIL_THRESHOLD: usize = 4;
 pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
 pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 const PRETTIER_PACKAGE_NAME: &str = "prettier";

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)}`);
     }

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";

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)}`);
     }

crates/project/src/prettier_support.rs 🔗

@@ -0,0 +1,758 @@
+use std::{
+    ops::ControlFlow,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+use anyhow::Context;
+use collections::HashSet;
+use fs::Fs;
+use futures::{
+    future::{self, Shared},
+    FutureExt,
+};
+use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task};
+use language::{
+    language_settings::{Formatter, LanguageSettings},
+    Buffer, Language, LanguageServerName, LocalFile,
+};
+use lsp::LanguageServerId;
+use node_runtime::NodeRuntime;
+use prettier::Prettier;
+use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
+
+use crate::{
+    Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
+};
+
+pub fn prettier_plugins_for_language(
+    language: &Language,
+    language_settings: &LanguageSettings,
+) -> Option<HashSet<&'static str>> {
+    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<Project>,
+    buffer: &ModelHandle<Buffer>,
+    cx: &mut AsyncAppContext,
+) -> Option<FormatOperation> {
+    if let Some((prettier_path, prettier_task)) = project
+        .update(cx, |project, cx| {
+            project.prettier_instance_for_buffer(buffer, cx)
+        })
+        .await
+    {
+        match prettier_task.await {
+            Ok(prettier) => {
+                let buffer_path = buffer.update(cx, |buffer, cx| {
+                    File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
+                });
+                match prettier.format(buffer, buffer_path, cx).await {
+                    Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
+                    Err(e) => {
+                        log::error!(
+                            "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
+                        );
+                    }
+                }
+            }
+            Err(e) => project.update(cx, |project, _| {
+                let instance_to_update = match prettier_path {
+                    Some(prettier_path) => {
+                        log::error!(
+                            "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
+                        );
+                        project.prettier_instances.get_mut(&prettier_path)
+                    }
+                    None => {
+                        log::error!("Default prettier instance failed to spawn: {e:#}");
+                        match &mut project.default_prettier.prettier {
+                            PrettierInstallation::NotInstalled { .. } => None,
+                            PrettierInstallation::Installed(instance) => Some(instance),
+                        }
+                    }
+                };
+
+                if let Some(instance) = instance_to_update {
+                    instance.attempt += 1;
+                    instance.prettier = None;
+                }
+            }),
+        }
+    }
+
+    None
+}
+
+pub struct DefaultPrettier {
+    prettier: PrettierInstallation,
+    installed_plugins: HashSet<&'static str>,
+}
+
+pub enum PrettierInstallation {
+    NotInstalled {
+        attempts: usize,
+        installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
+        not_installed_plugins: HashSet<&'static str>,
+    },
+    Installed(PrettierInstance),
+}
+
+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
+
+#[derive(Clone)]
+pub struct PrettierInstance {
+    attempt: usize,
+    prettier: Option<PrettierTask>,
+}
+
+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<dyn NodeRuntime>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        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<dyn NodeRuntime>,
+        prettier_dir: Option<&Path>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        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<dyn NodeRuntime>,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> Task<anyhow::Result<PrettierTask>> {
+    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<dyn NodeRuntime>,
+    prettier_dir: PathBuf,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> PrettierTask {
+    cx.spawn(|project, mut cx| async move {
+        log::info!("Starting prettier at path {prettier_dir:?}");
+        let new_server_id = project.update(&mut cx, |project, _| {
+            project.languages.next_language_server_id()
+        });
+
+        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
+            .await
+            .context("default prettier spawn")
+            .map(Arc::new)
+            .map_err(Arc::new)?;
+        register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
+        Ok(new_prettier)
+    })
+    .shared()
+}
+
+fn register_new_prettier(
+    project: &ModelHandle<Project>,
+    prettier: &Prettier,
+    worktree_id: Option<WorktreeId>,
+    new_server_id: LanguageServerId,
+    cx: &mut AsyncAppContext,
+) {
+    let prettier_dir = prettier.prettier_dir();
+    let is_default = prettier.is_default();
+    if is_default {
+        log::info!("Started default prettier in {prettier_dir:?}");
+    } else {
+        log::info!("Started prettier in {prettier_dir:?}");
+    }
+    if let Some(prettier_server) = prettier.server() {
+        project.update(cx, |project, cx| {
+            let name = if is_default {
+                LanguageServerName(Arc::from("prettier (default)"))
+            } else {
+                let worktree_path = worktree_id
+                    .and_then(|id| project.worktree_for_id(id, cx))
+                    .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
+                let name = match worktree_path {
+                    Some(worktree_path) => {
+                        if prettier_dir == worktree_path.as_ref() {
+                            let name = prettier_dir
+                                .file_name()
+                                .and_then(|name| name.to_str())
+                                .unwrap_or_default();
+                            format!("prettier ({name})")
+                        } else {
+                            let dir_to_display = prettier_dir
+                                .strip_prefix(worktree_path.as_ref())
+                                .ok()
+                                .unwrap_or(prettier_dir);
+                            format!("prettier ({})", dir_to_display.display())
+                        }
+                    }
+                    None => format!("prettier ({})", prettier_dir.display()),
+                };
+                LanguageServerName(Arc::from(name))
+            };
+            project
+                .supplementary_language_servers
+                .insert(new_server_id, (name, Arc::clone(prettier_server)));
+            cx.emit(Event::LanguageServerAdded(new_server_id));
+        });
+    }
+}
+
+async fn install_prettier_packages(
+    plugins_to_install: HashSet<&'static str>,
+    node: Arc<dyn NodeRuntime>,
+) -> 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::<Vec<_>>();
+    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
+        .await
+        .context("fetching formatter packages")?;
+    anyhow::Ok(())
+}
+
+async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
+    let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
+    fs.save(
+        &prettier_wrapper_path,
+        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
+        text::LineEnding::Unix,
+    )
+    .await
+    .with_context(|| {
+        format!(
+            "writing {} file at {prettier_wrapper_path:?}",
+            prettier::PRETTIER_SERVER_FILE
+        )
+    })?;
+    Ok(())
+}
+
+impl Project {
+    pub fn update_prettier_settings(
+        &self,
+        worktree: &ModelHandle<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<'_, Project>,
+    ) {
+        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+            .iter()
+            .map(Path::new)
+            .collect::<HashSet<_>>();
+
+        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(&current_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::<Vec<_>>();
+
+            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<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<(Option<PathBuf>, 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<WorktreeId>,
+        plugins: HashSet<&'static str>,
+        _cx: &mut ModelContext<Self>,
+    ) {
+        // suppress unused code warnings
+        let _ = install_prettier_packages;
+        let _ = save_prettier_server_file;
+
+        self.default_prettier.installed_plugins.extend(plugins);
+        self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
+            attempt: 0,
+            prettier: None,
+        });
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    pub fn install_default_prettier(
+        &mut self,
+        worktree: Option<WorktreeId>,
+        mut new_plugins: HashSet<&'static str>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let Some(node) = self.node.as_ref().cloned() else {
+            return;
+        };
+        log::info!("Initializing default prettier with plugins {new_plugins:?}");
+        let fs = Arc::clone(&self.fs);
+        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
+            self.worktree_for_id(worktree_id, cx)
+                .map(|worktree| worktree.read(cx).abs_path())
+        }) {
+            Some(locate_from) => {
+                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                cx.background().spawn(async move {
+                    Prettier::locate_prettier_installation(
+                        fs.as_ref(),
+                        &installed_prettiers,
+                        locate_from.as_ref(),
+                    )
+                    .await
+                })
+            }
+            None => Task::ready(Ok(ControlFlow::Continue(None))),
+        };
+        new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
+        let mut installation_attempt = 0;
+        let previous_installation_task = match &mut self.default_prettier.prettier {
+            PrettierInstallation::NotInstalled {
+                installation_task,
+                attempts,
+                not_installed_plugins,
+            } => {
+                installation_attempt = *attempts;
+                if installation_attempt > prettier::FAIL_THRESHOLD {
+                    *installation_task = None;
+                    log::warn!(
+                        "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                    );
+                    return;
+                }
+                new_plugins.extend(not_installed_plugins.iter());
+                installation_task.clone()
+            }
+            PrettierInstallation::Installed { .. } => {
+                if new_plugins.is_empty() {
+                    return;
+                }
+                None
+            }
+        };
+
+        let plugins_to_install = new_plugins.clone();
+        let fs = Arc::clone(&self.fs);
+        let new_installation_task = cx
+            .spawn(|project, mut cx| async move {
+                match locate_prettier_installation
+                    .await
+                    .context("locate prettier installation")
+                    .map_err(Arc::new)?
+                {
+                    ControlFlow::Break(()) => return Ok(()),
+                    ControlFlow::Continue(prettier_path) => {
+                        if prettier_path.is_some() {
+                            new_plugins.clear();
+                        }
+                        let mut needs_install = false;
+                        if let Some(previous_installation_task) = previous_installation_task {
+                            if let Err(e) = previous_installation_task.await {
+                                log::error!("Failed to install default prettier: {e:#}");
+                                project.update(&mut cx, |project, _| {
+                                    if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                        *attempts += 1;
+                                        new_plugins.extend(not_installed_plugins.iter());
+                                        installation_attempt = *attempts;
+                                        needs_install = true;
+                                    };
+                                });
+                            }
+                        };
+                        if installation_attempt > prettier::FAIL_THRESHOLD {
+                            project.update(&mut cx, |project, _| {
+                                if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
+                                    *installation_task = None;
+                                };
+                            });
+                            log::warn!(
+                                "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                            );
+                            return Ok(());
+                        }
+                        project.update(&mut cx, |project, _| {
+                            new_plugins.retain(|plugin| {
+                                !project.default_prettier.installed_plugins.contains(plugin)
+                            });
+                            if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                not_installed_plugins.retain(|plugin| {
+                                    !project.default_prettier.installed_plugins.contains(plugin)
+                                });
+                                not_installed_plugins.extend(new_plugins.iter());
+                            }
+                            needs_install |= !new_plugins.is_empty();
+                        });
+                        if needs_install {
+                            let installed_plugins = new_plugins.clone();
+                            cx.background()
+                                .spawn(async move {
+                                    save_prettier_server_file(fs.as_ref()).await?;
+                                    install_prettier_packages(new_plugins, node).await
+                                })
+                                .await
+                                .context("prettier & plugins install")
+                                .map_err(Arc::new)?;
+                            log::info!("Initialized prettier with plugins: {installed_plugins:?}");
+                            project.update(&mut cx, |project, _| {
+                                project.default_prettier.prettier =
+                                    PrettierInstallation::Installed(PrettierInstance {
+                                        attempt: 0,
+                                        prettier: None,
+                                    });
+                                project.default_prettier
+                                    .installed_plugins
+                                    .extend(installed_plugins);
+                            });
+                        }
+                    }
+                }
+                Ok(())
+            })
+            .shared();
+        self.default_prettier.prettier = PrettierInstallation::NotInstalled {
+            attempts: installation_attempt,
+            installation_task: Some(new_installation_task),
+            not_installed_plugins: plugins_to_install,
+        };
+    }
+}

crates/project/src/project.rs 🔗

@@ -1,5 +1,6 @@
 mod ignore;
 mod lsp_command;
+mod prettier_support;
 pub mod project_settings;
 pub mod search;
 pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{
         mpsc::{self, UnboundedReceiver},
         oneshot,
     },
-    future::{self, try_join_all, Shared},
+    future::{try_join_all, Shared},
     stream::FuturesUnordered,
     AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
 };
@@ -31,9 +32,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{
-    language_settings::{
-        language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
-    },
+    language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use postage::watch;
-use prettier::Prettier;
+use prettier_support::{DefaultPrettier, PrettierInstance};
 use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
 use search::SearchQuery;
@@ -72,7 +71,7 @@ use std::{
     hash::Hash,
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Range},
+    ops::Range,
     path::{self, Component, Path, PathBuf},
     process::Stdio,
     str,
@@ -85,11 +84,8 @@ use std::{
 use terminals::Terminals;
 use text::Anchor;
 use util::{
-    debug_panic, defer,
-    http::HttpClient,
-    merge_json_value_into,
-    paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
-    post_inc, ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, merge_json_value_into,
+    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -168,16 +164,9 @@ pub struct Project {
     copilot_log_subscription: Option<lsp::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     node: Option<Arc<dyn NodeRuntime>>,
-    default_prettier: Option<DefaultPrettier>,
+    default_prettier: DefaultPrettier,
     prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
-    prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-}
-
-struct DefaultPrettier {
-    instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-    installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
-    #[cfg(not(any(test, feature = "test-support")))]
-    installed_plugins: HashSet<&'static str>,
+    prettier_instances: HashMap<PathBuf, PrettierInstance>,
 }
 
 struct DelayedDebounced {
@@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate {
     http_client: Arc<dyn HttpClient>,
 }
 
+// Currently, formatting operations are represented differently depending on
+// whether they come from a language server or an external command.
+enum FormatOperation {
+    Lsp(Vec<(Range<Anchor>, String)>),
+    External(Diff),
+    Prettier(Diff),
+}
+
 impl FormatTrigger {
     fn from_proto(value: i32) -> FormatTrigger {
         match value {
@@ -690,7 +687,7 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
                 node: Some(node),
-                default_prettier: None,
+                default_prettier: DefaultPrettier::default(),
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             }
@@ -791,7 +788,7 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
                 node: None,
-                default_prettier: None,
+                default_prettier: DefaultPrettier::default(),
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             };
@@ -928,8 +925,19 @@ impl Project {
                 .detach();
         }
 
+        let mut prettier_plugins_by_worktree = HashMap::default();
         for (worktree, language, settings) in language_formatters_to_check {
-            self.install_default_formatters(worktree, &language, &settings, cx);
+            if let Some(plugins) =
+                prettier_support::prettier_plugins_for_language(&language, &settings)
+            {
+                prettier_plugins_by_worktree
+                    .entry(worktree)
+                    .or_insert_with(|| HashSet::default())
+                    .extend(plugins);
+            }
+        }
+        for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
+            self.install_default_prettier(worktree, prettier_plugins, cx);
         }
 
         // Start all the newly-enabled language servers.
@@ -2685,8 +2693,11 @@ impl Project {
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
         let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
-
-        self.install_default_formatters(worktree, &new_language, &settings, cx);
+        if let Some(prettier_plugins) =
+            prettier_support::prettier_plugins_for_language(&new_language, &settings)
+        {
+            self.install_default_prettier(worktree, prettier_plugins, cx);
+        };
         if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
             if let Some(tree) = worktree.read(cx).as_local() {
@@ -4073,8 +4084,6 @@ impl Project {
 
                     let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
                     let ensure_final_newline = settings.ensure_final_newline_on_save;
-                    let format_on_save = settings.format_on_save.clone();
-                    let formatter = settings.formatter.clone();
                     let tab_size = settings.tab_size;
 
                     // First, format buffer's whitespace according to the settings.
@@ -4099,18 +4108,10 @@ impl Project {
                         buffer.end_transaction(cx)
                     });
 
-                    // Currently, formatting operations are represented differently depending on
-                    // whether they come from a language server or an external command.
-                    enum FormatOperation {
-                        Lsp(Vec<(Range<Anchor>, String)>),
-                        External(Diff),
-                        Prettier(Diff),
-                    }
-
                     // Apply language-specific formatting using either a language server
                     // or external command.
                     let mut format_operation = None;
-                    match (formatter, format_on_save) {
+                    match (&settings.formatter, &settings.format_on_save) {
                         (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
 
                         (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4155,46 +4156,11 @@ impl Project {
                             }
                         }
                         (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
-                            if let Some((prettier_path, prettier_task)) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instance_for_buffer(buffer, cx)
-                                }).await {
-                                    match prettier_task.await
-                                    {
-                                        Ok(prettier) => {
-                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
-                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                                            });
-                                            format_operation = Some(FormatOperation::Prettier(
-                                                prettier
-                                                    .format(buffer, buffer_path, &cx)
-                                                    .await
-                                                    .context("formatting via prettier")?,
-                                            ));
-                                        }
-                                        Err(e) => {
-                                            project.update(&mut cx, |project, _| {
-                                                match &prettier_path {
-                                                    Some(prettier_path) => {
-                                                        project.prettier_instances.remove(prettier_path);
-                                                    },
-                                                    None => {
-                                                        if let Some(default_prettier) = project.default_prettier.as_mut() {
-                                                            default_prettier.instance = None;
-                                                        }
-                                                    },
-                                                }
-                                            });
-                                            match &prettier_path {
-                                                Some(prettier_path) => {
-                                                    log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
-                                                },
-                                                None => {
-                                                    log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
-                                                },
-                                            }
-                                        }
-                                    }
+                            if let Some(new_operation) =
+                                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                                    .await
+                            {
+                                format_operation = Some(new_operation);
                             } else if let Some((language_server, buffer_abs_path)) =
                                 language_server.as_ref().zip(buffer_abs_path.as_ref())
                             {
@@ -4212,48 +4178,13 @@ impl Project {
                                 ));
                             }
                         }
-                        (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
-                            if let Some((prettier_path, prettier_task)) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instance_for_buffer(buffer, cx)
-                                }).await {
-                                    match prettier_task.await
-                                    {
-                                        Ok(prettier) => {
-                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
-                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                                            });
-                                            format_operation = Some(FormatOperation::Prettier(
-                                                prettier
-                                                    .format(buffer, buffer_path, &cx)
-                                                    .await
-                                                    .context("formatting via prettier")?,
-                                            ));
-                                        }
-                                        Err(e) => {
-                                            project.update(&mut cx, |project, _| {
-                                                match &prettier_path {
-                                                    Some(prettier_path) => {
-                                                        project.prettier_instances.remove(prettier_path);
-                                                    },
-                                                    None => {
-                                                        if let Some(default_prettier) = project.default_prettier.as_mut() {
-                                                            default_prettier.instance = None;
-                                                        }
-                                                    },
-                                                }
-                                            });
-                                            match &prettier_path {
-                                                Some(prettier_path) => {
-                                                    log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
-                                                },
-                                                None => {
-                                                    log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
-                                                },
-                                            }
-                                        }
-                                    }
-                                }
+                        (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
+                            if let Some(new_operation) =
+                                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                                    .await
+                            {
+                                format_operation = Some(new_operation);
+                            }
                         }
                     };
 
@@ -6566,85 +6497,6 @@ impl Project {
         .detach();
     }
 
-    fn update_prettier_settings(
-        &self,
-        worktree: &ModelHandle<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
-        cx: &mut ModelContext<'_, Project>,
-    ) {
-        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
-            .iter()
-            .map(Path::new)
-            .collect::<HashSet<_>>();
-
-        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(&current_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::<Vec<_>>();
-
-            cx.background()
-                .spawn(async move {
-                    for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
-                        async move {
-                            prettier_task.await?
-                                .clear_cache()
-                                .await
-                                .with_context(|| {
-                                    match prettier_path {
-                                        Some(prettier_path) => format!(
-                                            "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
-                                        ),
-                                        None => format!(
-                                            "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
-                                        ),
-                                    }
-
-                                })
-                                .map_err(Arc::new)
-                        }
-                    }))
-                    .await
-                    {
-                        if let Err(e) = task_result {
-                            log::error!("Failed to clear cache for prettier: {e:#}");
-                        }
-                    }
-                })
-                .detach();
-        }
-    }
-
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -8536,446 +8388,6 @@ impl Project {
             Vec::new()
         }
     }
-
-    fn prettier_instance_for_buffer(
-        &mut self,
-        buffer: &ModelHandle<Buffer>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<
-        Option<(
-            Option<PathBuf>,
-            Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
-        )>,
-    > {
-        let buffer = buffer.read(cx);
-        let buffer_file = buffer.file();
-        let Some(buffer_language) = buffer.language() else {
-            return Task::ready(None);
-        };
-        if buffer_language.prettier_parser_name().is_none() {
-            return Task::ready(None);
-        }
-
-        if self.is_local() {
-            let Some(node) = self.node.as_ref().map(Arc::clone) else {
-                return Task::ready(None);
-            };
-            match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
-            {
-                Some((worktree_id, buffer_path)) => {
-                    let fs = Arc::clone(&self.fs);
-                    let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                    return cx.spawn(|project, mut cx| async move {
-                        match cx
-                            .background()
-                            .spawn(async move {
-                                Prettier::locate_prettier_installation(
-                                    fs.as_ref(),
-                                    &installed_prettiers,
-                                    &buffer_path,
-                                )
-                                .await
-                            })
-                            .await
-                        {
-                            Ok(ControlFlow::Break(())) => {
-                                return None;
-                            }
-                            Ok(ControlFlow::Continue(None)) => {
-                                let started_default_prettier =
-                                    project.update(&mut cx, |project, _| {
-                                        project
-                                            .prettiers_per_worktree
-                                            .entry(worktree_id)
-                                            .or_default()
-                                            .insert(None);
-                                        project.default_prettier.as_ref().and_then(
-                                            |default_prettier| default_prettier.instance.clone(),
-                                        )
-                                    });
-                                match started_default_prettier {
-                                    Some(old_task) => return Some((None, old_task)),
-                                    None => {
-                                        let new_default_prettier = project
-                                            .update(&mut cx, |_, cx| {
-                                                start_default_prettier(node, Some(worktree_id), cx)
-                                            })
-                                            .await;
-                                        return Some((None, new_default_prettier));
-                                    }
-                                }
-                            }
-                            Ok(ControlFlow::Continue(Some(prettier_dir))) => {
-                                project.update(&mut cx, |project, _| {
-                                    project
-                                        .prettiers_per_worktree
-                                        .entry(worktree_id)
-                                        .or_default()
-                                        .insert(Some(prettier_dir.clone()))
-                                });
-                                if let Some(existing_prettier) =
-                                    project.update(&mut cx, |project, _| {
-                                        project.prettier_instances.get(&prettier_dir).cloned()
-                                    })
-                                {
-                                    log::debug!(
-                                        "Found already started prettier in {prettier_dir:?}"
-                                    );
-                                    return Some((Some(prettier_dir), existing_prettier));
-                                }
-
-                                log::info!("Found prettier in {prettier_dir:?}, starting.");
-                                let new_prettier_task = project.update(&mut cx, |project, cx| {
-                                    let new_prettier_task = start_prettier(
-                                        node,
-                                        prettier_dir.clone(),
-                                        Some(worktree_id),
-                                        cx,
-                                    );
-                                    project
-                                        .prettier_instances
-                                        .insert(prettier_dir.clone(), new_prettier_task.clone());
-                                    new_prettier_task
-                                });
-                                Some((Some(prettier_dir), new_prettier_task))
-                            }
-                            Err(e) => {
-                                return Some((
-                                    None,
-                                    Task::ready(Err(Arc::new(
-                                        e.context("determining prettier path"),
-                                    )))
-                                    .shared(),
-                                ));
-                            }
-                        }
-                    });
-                }
-                None => {
-                    let started_default_prettier = self
-                        .default_prettier
-                        .as_ref()
-                        .and_then(|default_prettier| default_prettier.instance.clone());
-                    match started_default_prettier {
-                        Some(old_task) => return Task::ready(Some((None, old_task))),
-                        None => {
-                            let new_task = start_default_prettier(node, None, cx);
-                            return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
-                        }
-                    }
-                }
-            }
-        } else if self.remote_id().is_some() {
-            return Task::ready(None);
-        } else {
-            Task::ready(Some((
-                None,
-                Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
-            )))
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    fn install_default_formatters(
-        &mut self,
-        _worktree: Option<WorktreeId>,
-        _new_language: &Language,
-        _language_settings: &LanguageSettings,
-        _cx: &mut ModelContext<Self>,
-    ) {
-    }
-
-    #[cfg(not(any(test, feature = "test-support")))]
-    fn install_default_formatters(
-        &mut self,
-        worktree: Option<WorktreeId>,
-        new_language: &Language,
-        language_settings: &LanguageSettings,
-        cx: &mut ModelContext<Self>,
-    ) {
-        match &language_settings.formatter {
-            Formatter::Prettier { .. } | Formatter::Auto => {}
-            Formatter::LanguageServer | Formatter::External { .. } => return,
-        };
-        let Some(node) = self.node.as_ref().cloned() else {
-            return;
-        };
-
-        let mut prettier_plugins = None;
-        if new_language.prettier_parser_name().is_some() {
-            prettier_plugins
-                .get_or_insert_with(|| HashSet::<&'static str>::default())
-                .extend(
-                    new_language
-                        .lsp_adapters()
-                        .iter()
-                        .flat_map(|adapter| adapter.prettier_plugins()),
-                )
-        }
-        let Some(prettier_plugins) = prettier_plugins else {
-            return;
-        };
-
-        let fs = Arc::clone(&self.fs);
-        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
-            self.worktree_for_id(worktree_id, cx)
-                .map(|worktree| worktree.read(cx).abs_path())
-        }) {
-            Some(locate_from) => {
-                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                cx.background().spawn(async move {
-                    Prettier::locate_prettier_installation(
-                        fs.as_ref(),
-                        &installed_prettiers,
-                        locate_from.as_ref(),
-                    )
-                    .await
-                })
-            }
-            None => Task::ready(Ok(ControlFlow::Break(()))),
-        };
-        let mut plugins_to_install = prettier_plugins;
-        let previous_installation_process =
-            if let Some(default_prettier) = &mut self.default_prettier {
-                plugins_to_install
-                    .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
-                if plugins_to_install.is_empty() {
-                    return;
-                }
-                default_prettier.installation_process.clone()
-            } else {
-                None
-            };
-        let fs = Arc::clone(&self.fs);
-        let default_prettier = self
-            .default_prettier
-            .get_or_insert_with(|| DefaultPrettier {
-                instance: None,
-                installation_process: None,
-                installed_plugins: HashSet::default(),
-            });
-        default_prettier.installation_process = Some(
-            cx.spawn(|this, mut cx| async move {
-                match locate_prettier_installation
-                    .await
-                    .context("locate prettier installation")
-                    .map_err(Arc::new)?
-                {
-                    ControlFlow::Break(()) => return Ok(()),
-                    ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
-                    ControlFlow::Continue(None) => {
-                        let mut needs_install = match previous_installation_process {
-                            Some(previous_installation_process) => {
-                                previous_installation_process.await.is_err()
-                            }
-                            None => true,
-                        };
-                        this.update(&mut cx, |this, _| {
-                            if let Some(default_prettier) = &mut this.default_prettier {
-                                plugins_to_install.retain(|plugin| {
-                                    !default_prettier.installed_plugins.contains(plugin)
-                                });
-                                needs_install |= !plugins_to_install.is_empty();
-                            }
-                        });
-                        if needs_install {
-                            let installed_plugins = plugins_to_install.clone();
-                            cx.background()
-                                .spawn(async move {
-                                    install_default_prettier(plugins_to_install, node, fs).await
-                                })
-                                .await
-                                .context("prettier & plugins install")
-                                .map_err(Arc::new)?;
-                            this.update(&mut cx, |this, _| {
-                                let default_prettier =
-                                    this.default_prettier
-                                        .get_or_insert_with(|| DefaultPrettier {
-                                            instance: None,
-                                            installation_process: Some(
-                                                Task::ready(Ok(())).shared(),
-                                            ),
-                                            installed_plugins: HashSet::default(),
-                                        });
-                                default_prettier.instance = None;
-                                default_prettier.installed_plugins.extend(installed_plugins);
-                            });
-                        }
-                    }
-                }
-                Ok(())
-            })
-            .shared(),
-        );
-    }
-}
-
-fn start_default_prettier(
-    node: Arc<dyn NodeRuntime>,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
-    cx.spawn(|project, mut cx| async move {
-        loop {
-            let default_prettier_installing = project.update(&mut cx, |project, _| {
-                project
-                    .default_prettier
-                    .as_ref()
-                    .and_then(|default_prettier| default_prettier.installation_process.clone())
-            });
-            match default_prettier_installing {
-                Some(installation_task) => {
-                    if installation_task.await.is_ok() {
-                        break;
-                    }
-                }
-                None => break,
-            }
-        }
-
-        project.update(&mut cx, |project, cx| {
-            match project
-                .default_prettier
-                .as_mut()
-                .and_then(|default_prettier| default_prettier.instance.as_mut())
-            {
-                Some(default_prettier) => default_prettier.clone(),
-                None => {
-                    let new_default_prettier =
-                        start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
-                    project
-                        .default_prettier
-                        .get_or_insert_with(|| DefaultPrettier {
-                            instance: None,
-                            installation_process: None,
-                            #[cfg(not(any(test, feature = "test-support")))]
-                            installed_plugins: HashSet::default(),
-                        })
-                        .instance = Some(new_default_prettier.clone());
-                    new_default_prettier
-                }
-            }
-        })
-    })
-}
-
-fn start_prettier(
-    node: Arc<dyn NodeRuntime>,
-    prettier_dir: PathBuf,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
-    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<Project>,
-    prettier: &Prettier,
-    worktree_id: Option<WorktreeId>,
-    new_server_id: LanguageServerId,
-    cx: &mut AsyncAppContext,
-) {
-    let prettier_dir = prettier.prettier_dir();
-    let is_default = prettier.is_default();
-    if is_default {
-        log::info!("Started default prettier in {prettier_dir:?}");
-    } else {
-        log::info!("Started prettier in {prettier_dir:?}");
-    }
-    if let Some(prettier_server) = prettier.server() {
-        project.update(cx, |project, cx| {
-            let name = if is_default {
-                LanguageServerName(Arc::from("prettier (default)"))
-            } else {
-                let worktree_path = worktree_id
-                    .and_then(|id| project.worktree_for_id(id, cx))
-                    .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
-                let name = match worktree_path {
-                    Some(worktree_path) => {
-                        if prettier_dir == worktree_path.as_ref() {
-                            let name = prettier_dir
-                                .file_name()
-                                .and_then(|name| name.to_str())
-                                .unwrap_or_default();
-                            format!("prettier ({name})")
-                        } else {
-                            let dir_to_display = prettier_dir
-                                .strip_prefix(worktree_path.as_ref())
-                                .ok()
-                                .unwrap_or(prettier_dir);
-                            format!("prettier ({})", dir_to_display.display())
-                        }
-                    }
-                    None => format!("prettier ({})", prettier_dir.display()),
-                };
-                LanguageServerName(Arc::from(name))
-            };
-            project
-                .supplementary_language_servers
-                .insert(new_server_id, (name, Arc::clone(prettier_server)));
-            cx.emit(Event::LanguageServerAdded(new_server_id));
-        });
-    }
-}
-
-#[cfg(not(any(test, feature = "test-support")))]
-async fn install_default_prettier(
-    plugins_to_install: HashSet<&'static str>,
-    node: Arc<dyn NodeRuntime>,
-    fs: Arc<dyn Fs>,
-) -> 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::<Vec<_>>();
-    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
-        .await
-        .context("fetching formatter packages")?;
-    anyhow::Ok(())
 }
 
 fn subscribe_for_copilot_events(

crates/project2/src/prettier_support.rs 🔗

@@ -0,0 +1,772 @@
+use std::{
+    ops::ControlFlow,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+use anyhow::Context;
+use collections::HashSet;
+use fs::Fs;
+use futures::{
+    future::{self, Shared},
+    FutureExt,
+};
+use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
+use language::{
+    language_settings::{Formatter, LanguageSettings},
+    Buffer, Language, LanguageServerName, LocalFile,
+};
+use lsp::LanguageServerId;
+use node_runtime::NodeRuntime;
+use prettier::Prettier;
+use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
+
+use crate::{
+    Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
+};
+
+pub fn prettier_plugins_for_language(
+    language: &Language,
+    language_settings: &LanguageSettings,
+) -> Option<HashSet<&'static str>> {
+    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<Project>,
+    buffer: &Model<Buffer>,
+    cx: &mut AsyncAppContext,
+) -> Option<FormatOperation> {
+    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<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
+        not_installed_plugins: HashSet<&'static str>,
+    },
+    Installed(PrettierInstance),
+}
+
+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
+
+#[derive(Clone)]
+pub struct PrettierInstance {
+    attempt: usize,
+    prettier: Option<PrettierTask>,
+}
+
+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<dyn NodeRuntime>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        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<dyn NodeRuntime>,
+        prettier_dir: Option<&Path>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        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<dyn NodeRuntime>,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> Task<anyhow::Result<PrettierTask>> {
+    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<dyn NodeRuntime>,
+    prettier_dir: PathBuf,
+    worktree_id: Option<WorktreeId>,
+    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<Project>,
+    prettier: &Prettier,
+    worktree_id: Option<WorktreeId>,
+    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<dyn NodeRuntime>,
+) -> 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::<Vec<_>>();
+    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<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<'_, Project>,
+    ) {
+        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+            .iter()
+            .map(Path::new)
+            .collect::<HashSet<_>>();
+
+        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(&current_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::<Vec<_>>();
+
+            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<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<(Option<PathBuf>, 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<WorktreeId>,
+        plugins: HashSet<&'static str>,
+        _cx: &mut ModelContext<Self>,
+    ) {
+        // suppress unused code warnings
+        let _ = install_prettier_packages;
+        let _ = save_prettier_server_file;
+
+        self.default_prettier.installed_plugins.extend(plugins);
+        self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
+            attempt: 0,
+            prettier: None,
+        });
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    pub fn install_default_prettier(
+        &mut self,
+        worktree: Option<WorktreeId>,
+        mut new_plugins: HashSet<&'static str>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let Some(node) = self.node.as_ref().cloned() else {
+            return;
+        };
+        log::info!("Initializing default prettier with plugins {new_plugins:?}");
+        let fs = Arc::clone(&self.fs);
+        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
+            self.worktree_for_id(worktree_id, cx)
+                .map(|worktree| worktree.read(cx).abs_path())
+        }) {
+            Some(locate_from) => {
+                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                cx.background_executor().spawn(async move {
+                    Prettier::locate_prettier_installation(
+                        fs.as_ref(),
+                        &installed_prettiers,
+                        locate_from.as_ref(),
+                    )
+                    .await
+                })
+            }
+            None => Task::ready(Ok(ControlFlow::Continue(None))),
+        };
+        new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
+        let mut installation_attempt = 0;
+        let previous_installation_task = match &mut self.default_prettier.prettier {
+            PrettierInstallation::NotInstalled {
+                installation_task,
+                attempts,
+                not_installed_plugins,
+            } => {
+                installation_attempt = *attempts;
+                if installation_attempt > prettier::FAIL_THRESHOLD {
+                    *installation_task = None;
+                    log::warn!(
+                        "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                    );
+                    return;
+                }
+                new_plugins.extend(not_installed_plugins.iter());
+                installation_task.clone()
+            }
+            PrettierInstallation::Installed { .. } => {
+                if new_plugins.is_empty() {
+                    return;
+                }
+                None
+            }
+        };
+
+        let plugins_to_install = new_plugins.clone();
+        let fs = Arc::clone(&self.fs);
+        let new_installation_task = cx
+            .spawn(|project, mut cx| async move {
+                match locate_prettier_installation
+                    .await
+                    .context("locate prettier installation")
+                    .map_err(Arc::new)?
+                {
+                    ControlFlow::Break(()) => return Ok(()),
+                    ControlFlow::Continue(prettier_path) => {
+                        if prettier_path.is_some() {
+                            new_plugins.clear();
+                        }
+                        let mut needs_install = false;
+                        if let Some(previous_installation_task) = previous_installation_task {
+                            if let Err(e) = previous_installation_task.await {
+                                log::error!("Failed to install default prettier: {e:#}");
+                                project.update(&mut cx, |project, _| {
+                                    if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                        *attempts += 1;
+                                        new_plugins.extend(not_installed_plugins.iter());
+                                        installation_attempt = *attempts;
+                                        needs_install = true;
+                                    };
+                                })?;
+                            }
+                        };
+                        if installation_attempt > prettier::FAIL_THRESHOLD {
+                            project.update(&mut cx, |project, _| {
+                                if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
+                                    *installation_task = None;
+                                };
+                            })?;
+                            log::warn!(
+                                "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                            );
+                            return Ok(());
+                        }
+                        project.update(&mut cx, |project, _| {
+                            new_plugins.retain(|plugin| {
+                                !project.default_prettier.installed_plugins.contains(plugin)
+                            });
+                            if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                not_installed_plugins.retain(|plugin| {
+                                    !project.default_prettier.installed_plugins.contains(plugin)
+                                });
+                                not_installed_plugins.extend(new_plugins.iter());
+                            }
+                            needs_install |= !new_plugins.is_empty();
+                        })?;
+                        if needs_install {
+                            let installed_plugins = new_plugins.clone();
+                            cx.background_executor()
+                                .spawn(async move {
+                                    save_prettier_server_file(fs.as_ref()).await?;
+                                    install_prettier_packages(new_plugins, node).await
+                                })
+                                .await
+                                .context("prettier & plugins install")
+                                .map_err(Arc::new)?;
+                            log::info!("Initialized prettier with plugins: {installed_plugins:?}");
+                            project.update(&mut cx, |project, _| {
+                                project.default_prettier.prettier =
+                                    PrettierInstallation::Installed(PrettierInstance {
+                                        attempt: 0,
+                                        prettier: None,
+                                    });
+                                project.default_prettier
+                                    .installed_plugins
+                                    .extend(installed_plugins);
+                            })?;
+                        }
+                    }
+                }
+                Ok(())
+            })
+            .shared();
+        self.default_prettier.prettier = PrettierInstallation::NotInstalled {
+            attempts: installation_attempt,
+            installation_task: Some(new_installation_task),
+            not_installed_plugins: plugins_to_install,
+        };
+    }
+}

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<lsp::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     node: Option<Arc<dyn NodeRuntime>>,
-    default_prettier: Option<DefaultPrettier>,
+    default_prettier: DefaultPrettier,
     prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
-    prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-}
-
-struct DefaultPrettier {
-    instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-    installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
-    #[cfg(not(any(test, feature = "test-support")))]
-    installed_plugins: HashSet<&'static str>,
+    prettier_instances: HashMap<PathBuf, PrettierInstance>,
 }
 
 struct DelayedDebounced {
@@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate {
     http_client: Arc<dyn HttpClient>,
 }
 
+// Currently, formatting operations are represented differently depending on
+// whether they come from a language server or an external command.
+enum FormatOperation {
+    Lsp(Vec<(Range<Anchor>, 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<Anchor>, 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<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
-        cx: &mut ModelContext<'_, Project>,
-    ) {
-        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
-            .iter()
-            .map(Path::new)
-            .collect::<HashSet<_>>();
-
-        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(&current_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::<Vec<_>>();
-
-            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<ProjectPath>, cx: &mut ModelContext<Self>) {
         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<Buffer>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<
-        Option<(
-            Option<PathBuf>,
-            Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
-        )>,
-    > {
-        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<WorktreeId>,
-        _: &Language,
-        _: &LanguageSettings,
-        _: &mut ModelContext<Self>,
-    ) {
-    }
-
-    #[cfg(not(any(test, feature = "test-support")))]
-    fn install_default_formatters(
-        &mut self,
-        worktree: Option<WorktreeId>,
-        new_language: &Language,
-        language_settings: &LanguageSettings,
-        cx: &mut ModelContext<Self>,
-    ) {
-        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<dyn NodeRuntime>,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
-    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<dyn NodeRuntime>,
-    prettier_dir: PathBuf,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
-    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<Project>,
-    prettier: &Prettier,
-    worktree_id: Option<WorktreeId>,
-    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<dyn NodeRuntime>,
-    fs: Arc<dyn Fs>,
-) -> 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::<Vec<_>>();
-    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
-        .await
-        .context("fetching formatter packages")?;
-    anyhow::Ok(())
 }
 
 fn subscribe_for_copilot_events(

crates/project_panel2/src/file_associations.rs 🔗

@@ -41,56 +41,47 @@ impl FileAssociations {
             })
     }
 
-    pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            // FIXME: Associate a type with the languages and have the file's langauge
-            //        override these associations
-            maybe!({
-                let suffix = path.icon_suffix()?;
+        // FIXME: Associate a type with the languages and have the file's langauge
+        //        override these associations
+        maybe!({
+            let suffix = path.icon_suffix()?;
 
-                this.suffixes
-                    .get(suffix)
-                    .and_then(|type_str| this.types.get(type_str))
-                    .map(|type_config| type_config.icon.clone())
-            })
-            .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+            this.suffixes
+                .get(suffix)
+                .and_then(|type_str| this.types.get(type_str))
+                .map(|type_config| type_config.icon.clone())
         })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
     }
 
-    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            let key = if expanded {
-                EXPANDED_DIRECTORY_TYPE
-            } else {
-                COLLAPSED_DIRECTORY_TYPE
-            };
+        let key = if expanded {
+            EXPANDED_DIRECTORY_TYPE
+        } else {
+            COLLAPSED_DIRECTORY_TYPE
+        };
 
-            this.types
-                .get(key)
-                .map(|type_config| type_config.icon.clone())
-        })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        this.types
+            .get(key)
+            .map(|type_config| type_config.icon.clone())
     }
 
-    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            let key = if expanded {
-                EXPANDED_CHEVRON_TYPE
-            } else {
-                COLLAPSED_CHEVRON_TYPE
-            };
+        let key = if expanded {
+            EXPANDED_CHEVRON_TYPE
+        } else {
+            COLLAPSED_CHEVRON_TYPE
+        };
 
-            this.types
-                .get(key)
-                .map(|type_config| type_config.icon.clone())
-        })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        this.types
+            .get(key)
+            .map(|type_config| type_config.icon.clone())
     }
 }

crates/project_panel2/src/project_panel.rs 🔗

@@ -1283,16 +1283,16 @@ impl ProjectPanel {
                     let icon = match entry.kind {
                         EntryKind::File(_) => {
                             if show_file_icons {
-                                Some(FileAssociations::get_icon(&entry.path, cx))
+                                FileAssociations::get_icon(&entry.path, cx)
                             } else {
                                 None
                             }
                         }
                         _ => {
                             if show_folder_icons {
-                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
+                                FileAssociations::get_folder_icon(is_expanded, cx)
                             } else {
-                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
+                                FileAssociations::get_chevron_icon(is_expanded, cx)
                             }
                         }
                     };

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

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);
 

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 {

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;

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(),

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<Item = SharedString> + '_ {
         self.themes.keys().cloned()
     }

crates/theme_selector2/Cargo.toml 🔗

@@ -0,0 +1,29 @@
+[package]
+name = "theme_selector2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/theme_selector.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+fs = { package = "fs2", path = "../fs2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+picker = { package = "picker2", path = "../picker2" }
+theme = { package = "theme2", path = "../theme2" }
+settings = { package = "settings2", path = "../settings2" }
+feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -0,0 +1,276 @@
+use feature_flags::FeatureFlagAppExt;
+use fs::Fs;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render,
+    SharedString, View, ViewContext, VisualContext, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use settings::{update_settings_file, SettingsStore};
+use std::sync::Arc;
+use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
+use ui::ListItem;
+use util::ResultExt;
+use workspace::{ui::HighlightedLabel, Workspace};
+
+actions!(Toggle, Reload);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(toggle);
+        },
+    )
+    .detach();
+}
+
+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    let fs = workspace.app_state().fs.clone();
+    workspace.toggle_modal(cx, |cx| {
+        ThemeSelector::new(
+            ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+            cx,
+        )
+    });
+}
+
+#[cfg(debug_assertions)]
+pub fn reload(cx: &mut AppContext) {
+    let current_theme_name = cx.theme().name.clone();
+    let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
+        registry.clear();
+        registry.get(&current_theme_name)
+    });
+    match current_theme {
+        Ok(theme) => {
+            ThemeSelectorDelegate::set_theme(theme, cx);
+            log::info!("reloaded theme {}", current_theme_name);
+        }
+        Err(error) => {
+            log::error!("failed to load theme {}: {:?}", current_theme_name, error)
+        }
+    }
+}
+
+pub struct ThemeSelector {
+    picker: View<Picker<ThemeSelectorDelegate>>,
+}
+
+impl EventEmitter<DismissEvent> 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<Picker<ThemeSelectorDelegate>>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        self.picker.clone()
+    }
+}
+
+impl ThemeSelector {
+    pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        Self { picker }
+    }
+}
+
+pub struct ThemeSelectorDelegate {
+    fs: Arc<dyn Fs>,
+    theme_names: Vec<SharedString>,
+    matches: Vec<StringMatch>,
+    original_theme: Arc<Theme>,
+    selection_completed: bool,
+    selected_index: usize,
+    view: WeakView<ThemeSelector>,
+}
+
+impl ThemeSelectorDelegate {
+    fn new(
+        weak_view: WeakView<ThemeSelector>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ViewContext<ThemeSelector>,
+    ) -> Self {
+        let original_theme = cx.theme().clone();
+
+        let staff_mode = cx.is_staff();
+        let registry = cx.global::<Arc<ThemeRegistry>>();
+        let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
+        //todo!(theme sorting)
+        // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
+        let matches = theme_names
+            .iter()
+            .map(|meta| StringMatch {
+                candidate_id: 0,
+                score: 0.0,
+                positions: Default::default(),
+                string: meta.to_string(),
+            })
+            .collect();
+        let mut this = Self {
+            fs,
+            theme_names,
+            matches,
+            original_theme: original_theme.clone(),
+            selected_index: 0,
+            selection_completed: false,
+            view: weak_view,
+        };
+        this.select_if_matching(&original_theme.name);
+        this
+    }
+
+    fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
+        if let Some(mat) = self.matches.get(self.selected_index) {
+            let registry = cx.global::<Arc<ThemeRegistry>>();
+            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<Theme>, cx: &mut AppContext) {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
+            theme_settings.active_theme = theme;
+            store.override_global(theme_settings);
+            cx.refresh();
+        });
+    }
+}
+
+impl PickerDelegate for ThemeSelectorDelegate {
+    type ListItem = ui::ListItem;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select Theme...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
+        self.selection_completed = true;
+
+        let theme_name = cx.theme().name.clone();
+        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
+            settings.theme = Some(theme_name.to_string());
+        });
+
+        self.view
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
+        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<Picker<ThemeSelectorDelegate>>,
+    ) {
+        self.selected_index = ix;
+        self.show_selected_theme(cx);
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
+    ) -> gpui::Task<()> {
+        let background = cx.background_executor().clone();
+        let candidates = self
+            .theme_names
+            .iter()
+            .enumerate()
+            .map(|(id, meta)| StringMatchCandidate {
+                id,
+                char_bag: meta.as_ref().into(),
+                string: meta.to_string(),
+            })
+            .collect::<Vec<_>>();
+
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, cx| {
+                this.delegate.matches = matches;
+                this.delegate.selected_index = this
+                    .delegate
+                    .selected_index
+                    .min(this.delegate.matches.len().saturating_sub(1));
+                this.delegate.show_selected_theme(cx);
+            })
+            .log_err();
+        })
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let theme_match = &self.matches[ix];
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(HighlightedLabel::new(
+                    theme_match.string.clone(),
+                    theme_match.positions.clone(),
+                )),
+        )
+    }
+}

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;
+}

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::*;

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<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
+    click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
     icon: Option<Icon>,
     icon_position: Option<IconPosition>,
     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
     }

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>,
+//     icon_color: Option<Color>,
+//     icon_position: Option<IconPosition2>,
+//     label: Option<Label>,
+//     label_color: Option<Color>,
+//     appearance: ButtonAppearance2,
+//     state: InteractionState,
+//     selected: bool,
+//     disabled: bool,
+//     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+//     width: Option<DefiniteLength>,
+//     action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+//     secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+//     /// Used to pass down some content to the button
+//     /// to enable creating custom buttons.
+//     children: SmallVec<[AnyElement; 2]>,
+// }
+
+pub trait ButtonCommon: Clickable {
+    fn id(&self) -> &ElementId;
+    fn style(self, style: ButtonStyle2) -> Self;
+    fn disabled(self, disabled: bool) -> Self;
+    fn size(self, size: ButtonSize2) -> Self;
+    fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
+    // fn width(&mut self, width: DefiniteLength) -> &mut Self;
+}
+
+// pub struct LabelButton {
+//     // Base properties...
+//     id: ElementId,
+//     appearance: ButtonAppearance,
+//     state: InteractionState,
+//     disabled: bool,
+//     size: ButtonSize,
+//     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+//     width: Option<DefiniteLength>,
+//     // Button-specific properties...
+//     label: Option<SharedString>,
+//     label_color: Option<Color>,
+//     icon: Option<Icon>,
+//     icon_color: Option<Color>,
+//     icon_position: Option<IconPosition>,
+//     // Define more fields for additional properties as needed
+// }
+
+// impl ButtonCommon for LabelButton {
+//     fn id(&self) -> &ElementId {
+//         &self.id
+//     }
+
+//     fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self {
+//         self.style= style;
+//         self
+//     }
+//     // implement methods from ButtonCommon trait...
+// }
+
+// impl LabelButton {
+//     pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
+//         Self {
+//             id: id.into(),
+//             label: Some(label.into()),
+//             // initialize other fields with default values...
+//         }
+//     }
+
+//     // ... Define other builder methods specific to Button type...
+// }
+
+// TODO: Icon Button
+
+#[derive(IntoElement)]
+pub struct ButtonLike {
+    id: ElementId,
+    style: ButtonStyle2,
+    disabled: bool,
+    size: ButtonSize2,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl ButtonLike {
+    pub fn children(
+        &mut self,
+        children: impl IntoIterator<Item = impl Into<AnyElement>>,
+    ) -> &mut Self {
+        self.children = children.into_iter().map(Into::into).collect();
+        self
+    }
+
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            style: ButtonStyle2::default(),
+            disabled: false,
+            size: ButtonSize2::Default,
+            tooltip: None,
+            children: SmallVec::new(),
+            on_click: None,
+        }
+    }
+}
+
+impl Clickable for ButtonLike {
+    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+        self.on_click = Some(Box::new(handler));
+        self
+    }
+}
+
+// impl Selectable for ButtonLike {
+//     fn selected(&mut self, selected: bool) -> &mut Self {
+//         todo!()
+//     }
+
+//     fn selected_tooltip(
+//         &mut self,
+//         tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
+//     ) -> &mut Self {
+//         todo!()
+//     }
+// }
+
+impl ButtonCommon for ButtonLike {
+    fn id(&self) -> &ElementId {
+        &self.id
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.style = style;
+        self
+    }
+
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.size = size;
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+}
+
+impl RenderOnce for ButtonLike {
+    type Rendered = Stateful<Div>;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        h_stack()
+            .id(self.id.clone())
+            .h(self.size.height())
+            .rounded_md()
+            .cursor_pointer()
+            .gap_1()
+            .px_1()
+            .bg(self.style.enabled(cx).background)
+            .hover(|hover| hover.bg(self.style.hovered(cx).background))
+            .active(|active| active.bg(self.style.active(cx).background))
+            .when_some(
+                self.on_click.filter(|_| !self.disabled),
+                |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)),
+            )
+            .when_some(self.tooltip, |this, tooltip| {
+                this.tooltip(move |cx| tooltip(cx))
+            })
+            .children(self.children)
+    }
+}
+
+impl ParentElement for ButtonLike {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+// pub struct ToggleButton {
+//     // based on either IconButton2 or Button, with additional 'selected: bool' property
+// }
+
+// impl ButtonCommon for ToggleButton {
+//     fn id(&self) -> &ElementId {
+//         &self.id
+//     }
+//     // ... Implement other methods from ButtonCommon trait with builder patterns...
+// }
+
+// impl ToggleButton {
+//     pub fn new() -> Self {
+//         // Initialize with default values
+//         Self {
+//             // ... initialize fields, possibly with defaults or required parameters...
+//         }
+//     }
+
+//     // ... Define other builder methods specific to ToggleButton type...
+// }
+
+// pub struct SplitButton {
+//     // Base properties...
+//     id: ElementId,
+//     // Button-specific properties, possibly including a DefaultButton
+//     secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
+//     // More fields as necessary...
+// }
+
+// impl ButtonCommon for SplitButton {
+//     fn id(&self) -> &ElementId {
+//         &self.id
+//     }
+//     // ... Implement other methods from ButtonCommon trait with builder patterns...
+// }
+
+// impl SplitButton {
+//     pub fn new(id: impl Into<ElementId>) -> Self {
+//         Self {
+//             id: id.into(),
+//             // ... initialize other fields with default values...
+//         }
+//     }
+
+//     // ... Define other builder methods specific to SplitButton type...
+// }

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, ClickEvent, Element, IntoElement, 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<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+) -> 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))
+                }),
         ),
     }
 }

crates/ui2/src/components/icon_button.rs 🔗

@@ -1,25 +1,26 @@
-use crate::{h_stack, prelude::*, Icon, IconElement};
-use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
+use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
+use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
 
 #[derive(IntoElement)]
 pub struct IconButton {
     id: ElementId,
     icon: Icon,
     color: Color,
+    size: IconSize,
     variant: ButtonVariant,
-    state: InteractionState,
+    disabled: bool,
     selected: bool,
     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
-    on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
 }
 
 impl RenderOnce for IconButton {
     type Rendered = Stateful<Div>;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let icon_color = match (self.state, self.color) {
-            (InteractionState::Disabled, _) => Color::Disabled,
-            (InteractionState::Active, _) => Color::Selected,
+        let icon_color = match (self.disabled, self.selected, self.color) {
+            (true, _, _) => Color::Disabled,
+            (false, true, _) => Color::Selected,
             _ => self.color,
         };
 
@@ -50,10 +51,14 @@ 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));
-
-        if let Some(click_handler) = self.on_mouse_down {
-            button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
+            .child(
+                IconElement::new(self.icon)
+                    .size(self.size)
+                    .color(icon_color),
+            );
+
+        if let Some(click_handler) = self.on_click {
+            button = button.on_click(move |event, cx| {
                 cx.stop_propagation();
                 click_handler(event, cx);
             })
@@ -65,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
     }
 }
 
@@ -76,11 +80,12 @@ impl IconButton {
             id: id.into(),
             icon,
             color: Color::default(),
+            size: Default::default(),
             variant: ButtonVariant::default(),
-            state: InteractionState::default(),
             selected: false,
+            disabled: false,
             tooltip: None,
-            on_mouse_down: None,
+            on_click: None,
         }
     }
 
@@ -94,13 +99,13 @@ impl IconButton {
         self
     }
 
-    pub fn variant(mut self, variant: ButtonVariant) -> Self {
-        self.variant = variant;
+    pub fn size(mut self, size: IconSize) -> Self {
+        self.size = size;
         self
     }
 
-    pub fn state(mut self, state: InteractionState) -> Self {
-        self.state = state;
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
         self
     }
 
@@ -109,16 +114,18 @@ impl IconButton {
         self
     }
 
+    pub fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+
     pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
         self.tooltip = Some(Box::new(tooltip));
         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
     }
 

crates/ui2/src/components/input.rs 🔗

@@ -1,108 +0,0 @@
-use crate::{prelude::*, Label};
-use gpui::{prelude::*, Div, IntoElement, Stateful};
-
-#[derive(Default, PartialEq)]
-pub enum InputVariant {
-    #[default]
-    Ghost,
-    Filled,
-}
-
-#[derive(IntoElement)]
-pub struct Input {
-    placeholder: SharedString,
-    value: String,
-    state: InteractionState,
-    variant: InputVariant,
-    disabled: bool,
-    is_active: bool,
-}
-
-impl RenderOnce for Input {
-    type Rendered = Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
-            InputVariant::Ghost => (
-                cx.theme().colors().ghost_element_background,
-                cx.theme().colors().ghost_element_hover,
-                cx.theme().colors().ghost_element_active,
-            ),
-            InputVariant::Filled => (
-                cx.theme().colors().element_background,
-                cx.theme().colors().element_hover,
-                cx.theme().colors().element_active,
-            ),
-        };
-
-        let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
-            Color::Disabled
-        } else {
-            Color::Placeholder
-        });
-
-        let label = Label::new(self.value.clone()).color(if self.disabled {
-            Color::Disabled
-        } else {
-            Color::Default
-        });
-
-        div()
-            .id("input")
-            .h_7()
-            .w_full()
-            .px_2()
-            .border()
-            .border_color(cx.theme().styles.system.transparent)
-            .bg(input_bg)
-            .hover(|style| style.bg(input_hover_bg))
-            .active(|style| style.bg(input_active_bg))
-            .flex()
-            .items_center()
-            .child(div().flex().items_center().text_ui_sm().map(move |this| {
-                if self.value.is_empty() {
-                    this.child(placeholder_label)
-                } else {
-                    this.child(label)
-                }
-            }))
-    }
-}
-
-impl Input {
-    pub fn new(placeholder: impl Into<SharedString>) -> Self {
-        Self {
-            placeholder: placeholder.into(),
-            value: "".to_string(),
-            state: InteractionState::default(),
-            variant: InputVariant::default(),
-            disabled: false,
-            is_active: false,
-        }
-    }
-
-    pub fn value(mut self, value: String) -> Self {
-        self.value = value;
-        self
-    }
-
-    pub fn state(mut self, state: InteractionState) -> Self {
-        self.state = state;
-        self
-    }
-
-    pub fn variant(mut self, variant: InputVariant) -> Self {
-        self.variant = variant;
-        self
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn is_active(mut self, is_active: bool) -> Self {
-        self.is_active = is_active;
-        self
-    }
-}

crates/ui2/src/components/list.rs 🔗

@@ -25,7 +25,9 @@ pub struct ListHeader {
     left_icon: Option<Icon>,
     meta: Option<ListHeaderMeta>,
     toggle: Toggle,
+    on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     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<Icon>) -> 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);
+        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()
@@ -177,6 +197,7 @@ pub struct ListItem {
     toggle: Toggle,
     inset: bool,
     on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
     on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
     children: SmallVec<[AnyElement; 2]>,
 }
@@ -193,6 +214,7 @@ impl ListItem {
             inset: false,
             on_click: None,
             on_secondary_mouse_down: None,
+            on_toggle: None,
             children: SmallVec::new(),
         }
     }
@@ -230,6 +252,14 @@ impl ListItem {
         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 selected(mut self, selected: bool) -> Self {
         self.selected = selected;
         self
@@ -255,19 +285,6 @@ impl RenderOnce for ListItem {
     type Rendered = Stateful<Div>;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let left_content = match self.left_slot.clone() {
-            Some(GraphicSlot::Icon(i)) => Some(
-                h_stack().child(
-                    IconElement::new(i)
-                        .size(IconSize::Small)
-                        .color(Color::Muted),
-                ),
-            ),
-            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
-            Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
-            None => None,
-        };
-
         div()
             .id(self.id)
             .relative()
@@ -282,8 +299,8 @@ impl RenderOnce for ListItem {
             .when(self.selected, |this| {
                 this.bg(cx.theme().colors().ghost_element_selected)
             })
-            .when_some(self.on_click.clone(), |this, on_click| {
-                this.on_click(move |event, cx| {
+            .when_some(self.on_click, |this, on_click| {
+                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,23 +321,18 @@ impl RenderOnce for ListItem {
                     .gap_1()
                     .items_center()
                     .relative()
-                    .child(disclosure_control(self.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)
-                            }
-                        })
-                    }),
+                    .child(disclosure_control(self.toggle, self.on_toggle))
+                    .map(|this| match self.left_slot {
+                        Some(GraphicSlot::Icon(i)) => this.child(
+                            IconElement::new(i)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        ),
+                        Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)),
+                        Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)),
+                        None => this,
+                    })
+                    .children(self.children),
             )
     }
 }

crates/ui2/src/components/stories.rs 🔗

@@ -4,18 +4,15 @@ mod checkbox;
 mod context_menu;
 mod icon;
 mod icon_button;
-mod input;
 mod keybinding;
 mod label;
 mod list_item;
-
 pub use avatar::*;
 pub use button::*;
 pub use checkbox::*;
 pub use context_menu::*;
 pub use icon::*;
 pub use icon_button::*;
-pub use input::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list_item::*;

crates/ui2/src/components/stories/button.rs 🔗

@@ -1,9 +1,8 @@
-use gpui::{rems, Div, Render};
+use gpui::{Div, Render};
 use story::Story;
-use strum::IntoEnumIterator;
 
 use crate::prelude::*;
-use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
+use crate::{h_stack, Button, Icon, IconPosition};
 
 pub struct ButtonStory;
 
@@ -11,8 +10,6 @@ impl Render for ButtonStory {
     type Element = Div;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        let states = InteractionState::iter();
-
         Story::container()
             .child(Story::title_for::<Button>())
             .child(
@@ -20,121 +17,56 @@ impl Render for ButtonStory {
                     .flex()
                     .gap_8()
                     .child(
-                        div()
-                            .child(Story::label("Ghost (Default)"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Ghost – Left Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Ghost)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Ghost – Right Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Ghost)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right), // .state(state),
-                                    )
-                            }))),
-                    )
-                    .child(
-                        div()
-                            .child(Story::label("Filled"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Filled – Left Button"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Filled – Right Button"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right), // .state(state),
-                                    )
-                            }))),
+                        div().child(Story::label("Ghost (Default)")).child(
+                            h_stack()
+                                .gap_2()
+                                .child(Button::new("Label").variant(ButtonVariant::Ghost)),
+                        ),
                     )
+                    .child(Story::label("Ghost – Left Icon"))
                     .child(
-                        div()
-                            .child(Story::label("Fixed With"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            })))
-                            .child(Story::label("Fixed With – Left Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            })))
-                            .child(Story::label("Fixed With – Right Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            }))),
+                        h_stack().gap_2().child(
+                            Button::new("Label")
+                                .variant(ButtonVariant::Ghost)
+                                .icon(Icon::Plus)
+                                .icon_position(IconPosition::Left),
+                        ),
                     ),
             )
+            .child(Story::label("Ghost – Right Icon"))
+            .child(
+                h_stack().gap_2().child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Ghost)
+                        .icon(Icon::Plus)
+                        .icon_position(IconPosition::Right),
+                ),
+            )
+            .child(
+                div().child(Story::label("Filled")).child(
+                    h_stack()
+                        .gap_2()
+                        .child(Button::new("Label").variant(ButtonVariant::Filled)),
+                ),
+            )
+            .child(Story::label("Filled – Left Button"))
+            .child(
+                h_stack().gap_2().child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Filled)
+                        .icon(Icon::Plus)
+                        .icon_position(IconPosition::Left),
+                ),
+            )
+            .child(Story::label("Filled – Right Button"))
+            .child(
+                h_stack().gap_2().child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Filled)
+                        .icon(Icon::Plus)
+                        .icon_position(IconPosition::Right),
+                ),
+            )
             .child(Story::label("Button with `on_click`"))
             .child(
                 Button::new("Label")

crates/ui2/src/components/stories/input.rs 🔗

@@ -1,18 +0,0 @@
-use gpui::{Div, Render};
-use story::Story;
-
-use crate::prelude::*;
-use crate::Input;
-
-pub struct InputStory;
-
-impl Render for InputStory {
-    type Element = Div;
-
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        Story::container()
-            .child(Story::title_for::<Input>())
-            .child(Story::label("Default"))
-            .child(div().flex().child(Input::new("Search")))
-    }
-}

crates/ui2/src/components/stories/list_item.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{Div, Render};
 use story::Story;
 
 use crate::prelude::*;
-use crate::ListItem;
+use crate::{Icon, ListItem};
 
 pub struct ListItemStory;
 
@@ -14,6 +14,20 @@ impl Render for ListItemStory {
             .child(Story::title_for::<ListItem>())
             .child(Story::label("Default"))
             .child(ListItem::new("hello_world").child("Hello, world!"))
+            .child(Story::label("With left icon"))
+            .child(
+                ListItem::new("with_left_icon")
+                    .child("Hello, world!")
+                    .left_icon(Icon::Bell),
+            )
+            .child(Story::label("With left avatar"))
+            .child(
+                ListItem::new("with_left_avatar")
+                    .child("Hello, world!")
+                    .left_avatar(SharedString::from(
+                        "https://avatars.githubusercontent.com/u/1714999?v=4",
+                    )),
+            )
             .child(Story::label("With `on_click`"))
             .child(
                 ListItem::new("with_on_click")
@@ -24,11 +38,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!");
-                    },
-                ),
+                    }),
             )
     }
 }

crates/ui2/src/fixed.rs 🔗

@@ -0,0 +1,6 @@
+use gpui::DefiniteLength;
+
+pub trait FixedWidth {
+    fn width(self, width: DefiniteLength) -> Self;
+    fn full_width(self) -> Self;
+}

crates/ui2/src/prelude.rs 🔗

@@ -3,62 +3,9 @@ pub use gpui::{
     ViewContext, WindowContext,
 };
 
+pub use crate::clickable::*;
+pub use crate::fixed::*;
+pub use crate::selectable::*;
 pub use crate::StyledExt;
 pub use crate::{ButtonVariant, Color};
 pub use theme::ActiveTheme;
-
-use strum::EnumIter;
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum IconSide {
-    #[default]
-    Left,
-    Right,
-}
-
-#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
-pub enum InteractionState {
-    /// An element that is enabled and not hovered, active, focused, or disabled.
-    ///
-    /// This is often referred to as the "default" state.
-    #[default]
-    Enabled,
-    /// An element that is hovered.
-    Hovered,
-    /// An element has an active mouse down or touch start event on it.
-    Active,
-    /// An element that is focused using the keyboard.
-    Focused,
-    /// An element that is disabled.
-    Disabled,
-    /// A toggleable element that is selected, like the active button in a
-    /// button toggle group.
-    Selected,
-}
-
-impl InteractionState {
-    pub fn if_enabled(&self, enabled: bool) -> Self {
-        if enabled {
-            *self
-        } else {
-            InteractionState::Disabled
-        }
-    }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum Selection {
-    #[default]
-    Unselected,
-    Indeterminate,
-    Selected,
-}
-
-impl Selection {
-    pub fn inverse(&self) -> Self {
-        match self {
-            Self::Unselected | Self::Indeterminate => Self::Selected,
-            Self::Selected => Self::Unselected,
-        }
-    }
-}

crates/ui2/src/selectable.rs 🔗

@@ -0,0 +1,26 @@
+use gpui::{AnyView, WindowContext};
+
+pub trait Selectable {
+    fn selected(self, selected: bool) -> Self;
+    fn selected_tooltip(
+        self,
+        tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
+    ) -> Self;
+}
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum Selection {
+    #[default]
+    Unselected,
+    Indeterminate,
+    Selected,
+}
+
+impl Selection {
+    pub fn inverse(&self) -> Self {
+        match self {
+            Self::Unselected | Self::Indeterminate => Self::Selected,
+            Self::Selected => Self::Unselected,
+        }
+    }
+}

crates/ui2/src/styles/color.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{Hsla, WindowContext};
 use theme::ActiveTheme;
 
-#[derive(Default, PartialEq, Copy, Clone)]
+#[derive(Debug, Default, PartialEq, Copy, Clone)]
 pub enum Color {
     #[default]
     Default,

crates/ui2/src/ui2.rs 🔗

@@ -12,13 +12,19 @@
 #![doc = include_str!("../docs/building-ui.md")]
 #![doc = include_str!("../docs/todo.md")]
 
+mod clickable;
 mod components;
+mod fixed;
 pub mod prelude;
+mod selectable;
 mod styled_ext;
 mod styles;
 pub mod utils;
 
+pub use clickable::*;
 pub use components::*;
+pub use fixed::*;
 pub use prelude::*;
+pub use selectable::*;
 pub use styled_ext::*;
 pub use styles::*;

crates/welcome2/Cargo.toml 🔗

@@ -0,0 +1,37 @@
+[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" }
+ui = { package = "ui2", path = "../ui2" }
+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"] }

crates/welcome2/src/base_keymap_picker.rs 🔗

@@ -0,0 +1,208 @@
+use super::base_keymap_setting::BaseKeymap;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
+    View, ViewContext, VisualContext, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use project::Fs;
+use settings::{update_settings_file, Settings};
+use std::sync::Arc;
+use ui::ListItem;
+use util::ResultExt;
+use workspace::{ui::HighlightedLabel, Workspace};
+
+actions!(ToggleBaseKeymapSelector);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace.register_action(toggle);
+    })
+    .detach();
+}
+
+pub fn toggle(
+    workspace: &mut Workspace,
+    _: &ToggleBaseKeymapSelector,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let fs = workspace.app_state().fs.clone();
+    workspace.toggle_modal(cx, |cx| {
+        BaseKeymapSelector::new(
+            BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+            cx,
+        )
+    });
+}
+
+pub struct BaseKeymapSelector {
+    focus_handle: gpui::FocusHandle,
+    picker: View<Picker<BaseKeymapSelectorDelegate>>,
+}
+
+impl FocusableView for BaseKeymapSelector {
+    fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
+
+impl BaseKeymapSelector {
+    pub fn new(
+        delegate: BaseKeymapSelectorDelegate,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> 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<Picker<BaseKeymapSelectorDelegate>>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        self.picker.clone()
+    }
+}
+
+pub struct BaseKeymapSelectorDelegate {
+    view: WeakView<BaseKeymapSelector>,
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+    fs: Arc<dyn Fs>,
+}
+
+impl BaseKeymapSelectorDelegate {
+    fn new(
+        weak_view: WeakView<BaseKeymapSelector>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> 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,
+        }
+    }
+}
+
+impl PickerDelegate for BaseKeymapSelectorDelegate {
+    type ListItem = ui::ListItem;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "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<Picker<BaseKeymapSelectorDelegate>>,
+    ) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
+    ) -> Task<()> {
+        let background = cx.background_executor().clone();
+        let candidates = BaseKeymap::names()
+            .enumerate()
+            .map(|(id, name)| StringMatchCandidate {
+                id,
+                char_bag: name.into(),
+                string: name.into(),
+            })
+            .collect::<Vec<_>>();
+
+        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, _| {
+                this.delegate.matches = matches;
+                this.delegate.selected_index = this
+                    .delegate
+                    .selected_index
+                    .min(this.delegate.matches.len().saturating_sub(1));
+            })
+            .log_err();
+        })
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
+        if let Some(selection) = self.matches.get(self.selected_index) {
+            let base_keymap = BaseKeymap::from_names(&selection.string);
+            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
+                *setting = Some(base_keymap)
+            });
+        }
+
+        self.view
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
+    }
+
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _cx: &mut gpui::ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let keymap_match = &self.matches[ix];
+
+        Some(
+            ListItem::new(ix)
+                .selected(selected)
+                .inset(true)
+                .child(HighlightedLabel::new(
+                    keymap_match.string.clone(),
+                    keymap_match.positions.clone(),
+                )),
+        )
+    }
+}

crates/welcome2/src/base_keymap_setting.rs 🔗

@@ -0,0 +1,65 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+
+#[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<Item = &'static str> {
+        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 Settings for BaseKeymap {
+    const KEY: Option<&'static str> = Some("base_keymap");
+
+    type FileContent = Option<Self>;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Ok(user_values
+            .first()
+            .and_then(|v| **v)
+            .unwrap_or(default_value.unwrap()))
+    }
+}

crates/welcome2/src/welcome.rs 🔗

@@ -0,0 +1,281 @@
+mod base_keymap_picker;
+mod base_keymap_setting;
+
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable,
+    FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View,
+    ViewContext, VisualContext, WeakView, WindowContext,
+};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use workspace::{
+    dock::DockPosition,
+    item::{Item, ItemEvent},
+    open_new, AppState, Welcome, Workspace, WorkspaceId,
+};
+
+pub use base_keymap_setting::BaseKeymap;
+
+pub const FIRST_OPEN: &str = "first_open";
+
+pub fn init(cx: &mut AppContext) {
+    BaseKeymap::register(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);
+}
+
+pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    open_new(&app_state, cx, |workspace, cx| {
+        workspace.toggle_dock(DockPosition::Left, 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_view(&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: WeakView<Workspace>,
+    focus_handle: FocusHandle,
+    _settings_subscription: Subscription,
+}
+
+impl Render for WelcomePage {
+    type Element = Focusable<Div>;
+
+    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+        // todo!(welcome_ui)
+        // let self_handle = cx.handle();
+        // let theme = cx.theme();
+        // let width = theme.welcome.page_width;
+
+        // let telemetry_settings = TelemetrySettings::get(None, cx);
+        // let vim_mode_setting = VimModeSettings::get(cx);
+
+        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::<theme_selector::Toggle, _, _, _>(
+        //                         "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::<ToggleBaseKeymapSelector, _, _, _>(
+        //                         "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_cli::Install, _, _, _>(
+        //                         "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::<Diagnostics, Self, _>(
+        //                             "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::<VimModeSetting>(
+        //                                         fs,
+        //                                         cx,
+        //                                         move |setting| *setting = Some(checked),
+        //                                     )
+        //                                 }
+        //                             },
+        //                         )
+        //                         .contained()
+        //                         .with_style(theme.welcome.checkbox_container),
+        //                     )
+        //                     .with_child(
+        //                         theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
+        //                             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::<TelemetrySettings>(
+        //                                         fs,
+        //                                         cx,
+        //                                         move |setting| setting.metrics = Some(checked),
+        //                                     )
+        //                                 }
+        //                             },
+        //                         )
+        //                         .contained()
+        //                         .with_style(theme.welcome.checkbox_container),
+        //                     )
+        //                     .with_child(
+        //                         theme::ui::checkbox::<Diagnostics, Self, _>(
+        //                             "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::<TelemetrySettings>(
+        //                                         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>) -> Self {
+        WelcomePage {
+            focus_handle: cx.focus_handle(),
+            workspace: workspace.weak_handle(),
+            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+        }
+    }
+}
+
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl FocusableView for WelcomePage {
+    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for WelcomePage {
+    fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
+        "Welcome to Zed!".into_any()
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<View<Self>> {
+        Some(cx.build_view(|cx| WelcomePage {
+            focus_handle: cx.focus_handle(),
+            workspace: self.workspace.clone(),
+            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+        }))
+    }
+}

crates/workspace2/src/dock.rs 🔗

@@ -7,8 +7,8 @@ use gpui::{
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use theme2::ActiveTheme;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
+use ui::prelude::*;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -686,22 +686,26 @@ impl Render for PanelButtons {
                 let name = entry.panel.persistent_name();
                 let panel = entry.panel.clone();
 
-                let mut button: IconButton = if i == active_index && is_open {
+                let is_active_button = i == active_index && is_open;
+
+                let (action, tooltip) = if is_active_button {
                     let action = dock.toggle_action();
+
                     let tooltip: SharedString =
                         format!("Close {} dock", dock.position.to_label()).into();
-                    IconButton::new(name, icon)
-                        .state(InteractionState::Active)
-                        .action(action.boxed_clone())
-                        .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+
+                    (action, tooltip)
                 } else {
                     let action = entry.panel.toggle_action(cx);
 
-                    IconButton::new(name, icon)
-                        .action(action.boxed_clone())
-                        .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
+                    (action, name.into())
                 };
 
+                let button = IconButton::new(name, icon)
+                    .selected(is_active_button)
+                    .action(action.boxed_clone())
+                    .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx));
+
                 Some(
                     menu_handle(name)
                         .menu(move |cx| {

crates/workspace2/src/pane.rs 🔗

@@ -1482,18 +1482,14 @@ impl Pane {
                             .gap_px()
                             .child(
                                 div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_backward", Icon::ArrowLeft).state(
-                                        InteractionState::Enabled
-                                            .if_enabled(self.can_navigate_backward()),
-                                    ),
+                                    IconButton::new("navigate_backward", Icon::ArrowLeft)
+                                        .disabled(!self.can_navigate_backward()),
                                 ),
                             )
                             .child(
                                 div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_forward", Icon::ArrowRight).state(
-                                        InteractionState::Enabled
-                                            .if_enabled(self.can_navigate_forward()),
-                                    ),
+                                    IconButton::new("navigate_forward", Icon::ArrowRight)
+                                        .disabled(!self.can_navigate_forward()),
                                 ),
                             ),
                     ),

crates/workspace2/src/status_bar.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
     div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
     WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::{h_stack, Button, Icon, IconButton};
 use util::ResultExt;
 

crates/workspace2/src/toolbar.rs 🔗

@@ -3,7 +3,7 @@ use gpui::{
     div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
     ViewContext, WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
 
 pub enum ToolbarItemEvent {

crates/workspace2/src/workspace2.rs 🔗

@@ -1808,22 +1808,22 @@ impl Workspace {
         pane
     }
 
-    //     pub fn add_item_to_center(
-    //         &mut self,
-    //         item: Box<dyn ItemHandle>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> 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<dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
         self.active_pane

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.115.0"
+version = "0.116.0"
 publish = false
 
 [lib]

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"] }

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;
@@ -36,7 +36,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicU32, Ordering},
-        Arc,
+        Arc, Weak,
     },
     thread,
 };
@@ -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,
@@ -103,16 +104,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::<Weak<AppState>>() {
-        // if let Some(app_state) = cx.global::<Weak<AppState>>().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::<Weak<AppState>>() {
+            if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
+                workspace::open_new(&app_state, cx, |workspace, cx| {
+                    Editor::new_file(workspace, &Default::default(), cx)
+                })
+                .detach();
+            }
+        }
     });
 
     app.run(move |cx| {
@@ -164,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);
+        watch_file_types(fs.clone(), cx);
 
         languages.set_theme(cx.theme().clone());
-        // cx.observe_global::<SettingsStore, _>({
-        //     let languages = languages.clone();
-        //     move |cx| languages.set_theme(theme::current(cx).clone())
-        // })
-        // .detach();
+        cx.observe_global::<SettingsStore>({
+            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);
@@ -193,7 +192,6 @@ fn main() {
             fs,
             build_window_options,
             call_factory: call::Call::new,
-            // background_actions: todo!("ask Mikayla"),
             workspace_store,
             node_runtime,
         });
@@ -219,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);
@@ -279,17 +276,18 @@ 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;
+                    //todo!()
+                    // 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!()
@@ -340,7 +338,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
         if client::IMPERSONATE_LOGIN.is_some() {
             client.authenticate_and_connect(false, &cx).await?;
         }
-    } else if client.has_keychain_credentials(&cx).await {
+    } else if client.has_keychain_credentials(&cx) {
         client.authenticate_and_connect(true, &cx).await?;
     }
     Ok::<_, anyhow::Error>(())
@@ -368,10 +366,9 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
             cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
                 .await
                 .log_err();
-            // 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))
+                .log_err();
         } else {
             cx.update(|cx| {
                 workspace::open_new(app_state, cx, |workspace, cx| {
@@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) {
         .unwrap();
 }
 
-// #[cfg(debug_assertions)]
-// async fn watch_themes(fs: Arc<dyn Fs>, 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<dyn Fs>, languages: Arc<LanguageRegistry>) -> 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(())
-// }
-
-// #[cfg(debug_assertions)]
-// fn watch_file_types(fs: Arc<dyn Fs>, 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(not(debug_assertions))]
-// async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
-//     None
-// }
-
-// #[cfg(not(debug_assertions))]
-// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
-//     None
-//
-
-// #[cfg(not(debug_assertions))]
-// fn watch_file_types(_fs: Arc<dyn Fs>, _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!()
-    &[]
+#[cfg(debug_assertions)]
+async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
+    use std::time::Duration;
+
+    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(())
+}
+
+#[cfg(debug_assertions)]
+fn watch_file_types(fs: Arc<dyn fs::Fs>, 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<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
+    None
+}
+
+#[cfg(not(debug_assertions))]
+fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}

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