History

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

Max Brunsfeld committed 2 years ago

Commit dbfc7d32 parents 233aac5 c23a61064 files · +4040 −2698Browse files

Unified · Split

64 files
  1. Cargo.lock +49 −1
  2. Cargo.toml +2 −0
  3. crates/call2/src/call2.rs +47 −4
  4. crates/client2/src/client2.rs +4 −4
  5. crates/collab2/src/tests/channel_tests.rs +24 −13
  6. crates/collab2/src/tests/integration_tests.rs +7 −4
  7. crates/collab_ui2/src/collab_panel.rs +482 −719
  8. crates/collab_ui2/src/collab_panel/contact_finder.rs +0 −1
  9. crates/collab_ui2/src/collab_titlebar_item.rs +25 −1
  10. crates/command_palette2/src/command_palette.rs +6 −4
  11. crates/feature_flags2/src/feature_flags2.rs +2 −2
  12. crates/gpui2/src/action.rs +1 −0
  13. crates/gpui2/src/element.rs +32 −12
  14. crates/gpui2/src/elements/uniform_list.rs +2 −4
  15. crates/gpui2/src/window.rs +0 −17
  16. crates/picker2/src/picker2.rs +24 −5
  17. crates/prettier/src/prettier.rs +4 −0
  18. crates/prettier/src/prettier_server.js +3 −0
  19. crates/prettier2/src/prettier2.rs +4 −0
  20. crates/prettier2/src/prettier_server.js +3 −0
  21. crates/project/src/prettier_support.rs +758 −0
  22. crates/project/src/project.rs +49 −637
  23. crates/project2/src/prettier_support.rs +772 −0
  24. crates/project2/src/project2.rs +51 −677
  25. crates/project_panel2/src/file_associations.rs +31 −40
  26. crates/project_panel2/src/project_panel.rs +3 −3
  27. crates/search2/src/search_bar.rs +3 −3
  28. crates/storybook2/src/stories/focus.rs +1 −1
  29. crates/storybook2/src/stories/picker.rs +1 −1
  30. crates/storybook2/src/stories/scroll.rs +1 −1
  31. crates/storybook2/src/story_selector.rs +0 −2
  32. crates/theme2/src/registry.rs +4 −0
  33. crates/theme_selector2/Cargo.toml +29 −0
  34. crates/theme_selector2/src/theme_selector.rs +276 −0
  35. crates/ui2/src/clickable.rs +5 −0
  36. crates/ui2/src/components.rs +2 −2
  37. crates/ui2/src/components/button.rs +5 −10
  38. crates/ui2/src/components/button2.rs +413 −0
  39. crates/ui2/src/components/disclosure.rs +18 −7
  40. crates/ui2/src/components/icon_button.rs +31 −24
  41. crates/ui2/src/components/input.rs +0 −108
  42. crates/ui2/src/components/list.rs +45 −33
  43. crates/ui2/src/components/stories.rs +0 −3
  44. crates/ui2/src/components/stories/button.rs +48 −116
  45. crates/ui2/src/components/stories/input.rs +0 −18
  46. crates/ui2/src/components/stories/list_item.rs +19 −5
  47. crates/ui2/src/fixed.rs +6 −0
  48. crates/ui2/src/prelude.rs +3 −56
  49. crates/ui2/src/selectable.rs +26 −0
  50. crates/ui2/src/styles/color.rs +1 −1
  51. crates/ui2/src/ui2.rs +6 −0
  52. crates/welcome2/Cargo.toml +37 −0
  53. crates/welcome2/src/base_keymap_picker.rs +208 −0
  54. crates/welcome2/src/base_keymap_setting.rs +65 −0
  55. crates/welcome2/src/welcome.rs +281 −0
  56. crates/workspace2/src/dock.rs +14 −10
  57. crates/workspace2/src/pane.rs +4 −8
  58. crates/workspace2/src/status_bar.rs +1 −1
  59. crates/workspace2/src/toolbar.rs +1 −1
  60. crates/workspace2/src/workspace2.rs +16 −16
  61. crates/zed/Cargo.toml +1 −1
  62. crates/zed2/Cargo.toml +2 −2
  63. crates/zed2/src/main.rs +81 −119
  64. script/crate-dep-graph +1 −1
Cargo.lock+49−1
@@ -9471,6 +9471,27 @@ dependencies = [ · 9470 unmodified lines
94719471 "workspace",
94729472 ]
94739473
9474+[[package]]
9475+name = "theme_selector2"
9476+version = "0.1.0"
9477+dependencies = [
9478+ "editor2",
9479+ "feature_flags2",
9480+ "fs2",
9481+ "fuzzy2",
9482+ "gpui2",
9483+ "log",
9484+ "parking_lot 0.11.2",
9485+ "picker2",
9486+ "postage",
9487+ "settings2",
9488+ "smol",
9489+ "theme2",
9490+ "ui2",
9491+ "util",
9492+ "workspace2",
9493+]
9494+
94749495 [[package]]
94759496 name = "thiserror"
94769497 version = "1.0.48"
@@ -11054,6 +11075,31 @@ dependencies = [ · 1577 unmodified lines
1105411075 "workspace",
1105511076 ]
1105611077
11078+[[package]]
11079+name = "welcome2"
11080+version = "0.1.0"
11081+dependencies = [
11082+ "anyhow",
11083+ "client2",
11084+ "db2",
11085+ "editor2",
11086+ "fs2",
11087+ "fuzzy2",
11088+ "gpui2",
11089+ "install_cli2",
11090+ "log",
11091+ "picker2",
11092+ "project2",
11093+ "schemars",
11094+ "serde",
11095+ "settings2",
11096+ "theme2",
11097+ "theme_selector2",
11098+ "ui2",
11099+ "util",
11100+ "workspace2",
11101+]
11102+
1105711103 [[package]]
1105811104 name = "which"
1105911105 version = "4.4.2"
@@ -11508,7 +11554,7 @@ dependencies = [ · 448 unmodified lines
1150811554
1150911555 [[package]]
1151011556 name = "zed"
11511−version = "0.115.0"
11557+version = "0.116.0"
1151211558 dependencies = [
1151311559 "activity_indicator",
1151411560 "ai",
@@ -11720,6 +11766,7 @@ dependencies = [ · 205 unmodified lines
1172011766 "terminal_view2",
1172111767 "text2",
1172211768 "theme2",
11769+ "theme_selector2",
1172311770 "thiserror",
1172411771 "tiny_http",
1172511772 "toml 0.5.11",
@@ -11757,6 +11804,7 @@ dependencies = [ · 31 unmodified lines
1175711804 "urlencoding",
1175811805 "util",
1175911806 "uuid 1.4.1",
11807+ "welcome2",
1176011808 "workspace2",
1176111809 "zed_actions2",
1176211810 ]
Cargo.toml+2−0
@@ -107,6 +107,7 @@ members = [ · 106 unmodified lines
107107 "crates/theme2",
108108 "crates/theme_importer",
109109 "crates/theme_selector",
110+ "crates/theme_selector2",
110111 "crates/ui2",
111112 "crates/util",
112113 "crates/semantic_index",
@@ -115,6 +116,7 @@ members = [ · 2 unmodified lines
115116 "crates/vcs_menu",
116117 "crates/workspace2",
117118 "crates/welcome",
119+ "crates/welcome2",
118120 "crates/xtask",
119121 "crates/zed",
120122 "crates/zed2",
crates/call2/src/call2.rs+47−4
@@ -14,8 +14,8 @@ use client::{ · 13 unmodified lines
1414 use collections::HashSet;
1515 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
1616 use gpui::{
17− AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
18− View, ViewContext, VisualContext, WeakModel, WeakView,
17+ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
18+ Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
1919 };
2020 pub use participant::ParticipantLocation;
2121 use postage::watch;
@@ -334,12 +334,55 @@ impl ActiveCall { · 312 unmodified lines
334334 pub fn join_channel(
335335 &mut self,
336336 channel_id: u64,
337+ requesting_window: Option<WindowHandle<Workspace>>,
337338 cx: &mut ModelContext<Self>,
338339 ) -> Task<Result<Option<Model<Room>>>> {
339340 if let Some(room) = self.room().cloned() {
340341 if room.read(cx).channel_id() == Some(channel_id) {
341− return Task::ready(Ok(Some(room)));
342− } else {
342+ return cx.spawn(|_, _| async move {
343+ todo!();
344+ // let future = room.update(&mut cx, |room, cx| {
345+ // room.most_active_project(cx).map(|(host, project)| {
346+ // room.join_project(project, host, app_state.clone(), cx)
347+ // })
348+ // })
349+
350+ // if let Some(future) = future {
351+ // future.await?;
352+ // }
353+
354+ // Ok(Some(room))
355+ });
356+ }
357+
358+ let should_prompt = room.update(cx, |room, _| {
359+ room.channel_id().is_some()
360+ && room.is_sharing_project()
361+ && room.remote_participants().len() > 0
362+ });
363+ if should_prompt && requesting_window.is_some() {
364+ return cx.spawn(|this, mut cx| async move {
365+ let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
366+ cx.prompt(
367+ PromptLevel::Warning,
368+ "Leaving this call will unshare your current project.\nDo you want to switch channels?",
369+ &["Yes, Join Channel", "Cancel"],
370+ )
371+ })?;
372+ if answer.await? == 1 {
373+ return Ok(None);
374+ }
375+
376+ room.update(&mut cx, |room, cx| room.clear_state(cx))?;
377+
378+ this.update(&mut cx, |this, cx| {
379+ this.join_channel(channel_id, requesting_window, cx)
380+ })?
381+ .await
382+ });
383+ }
384+
385+ if room.read(cx).channel_id().is_some() {
343386 room.update(cx, |room, cx| room.clear_state(cx));
344387 }
345388 }
crates/client2/src/client2.rs+4−4
@@ -693,8 +693,8 @@ impl Client { · 692 unmodified lines
693693 }
694694 }
695695
696− pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
697− read_credentials_from_keychain(cx).await.is_some()
696+ pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
697+ read_credentials_from_keychain(cx).is_some()
698698 }
699699
700700 #[async_recursion(?Send)]
@@ -725,7 +725,7 @@ impl Client { · 24 unmodified lines
725725 let mut read_from_keychain = false;
726726 let mut credentials = self.state.read().credentials.clone();
727727 if credentials.is_none() && try_keychain {
728− credentials = read_credentials_from_keychain(cx).await;
728+ credentials = read_credentials_from_keychain(cx);
729729 read_from_keychain = credentials.is_some();
730730 }
731731 if credentials.is_none() {
@@ -1324,7 +1324,7 @@ impl Client { · 592 unmodified lines
13241324 }
13251325 }
13261326
1327−async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
1327+fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
13281328 if IMPERSONATE_LOGIN.is_some() {
13291329 return None;
13301330 }
crates/collab2/src/tests/channel_tests.rs+24−13
@@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member( · 363 unmodified lines
364364 let active_call_b = cx_b.read(ActiveCall::global);
365365
366366 assert!(active_call_b
367− .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
367+ .update(cx_b, |active_call, cx| active_call
368+ .join_channel(sub_id, None, cx))
368369 .await
369370 .is_ok());
370371 }
@@ -394,7 +395,9 @@ async fn test_channel_room( · 23 unmodified lines
394395 let active_call_b = cx_b.read(ActiveCall::global);
395396
396397 active_call_a
397− .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
398+ .update(cx_a, |active_call, cx| {
399+ active_call.join_channel(zed_id, None, cx)
400+ })
398401 .await
399402 .unwrap();
400403
@@ -442,7 +445,9 @@ async fn test_channel_room( · 41 unmodified lines
442445 });
443446
444447 active_call_b
445− .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
448+ .update(cx_b, |active_call, cx| {
449+ active_call.join_channel(zed_id, None, cx)
450+ })
446451 .await
447452 .unwrap();
448453
@@ -559,12 +564,16 @@ async fn test_channel_room( · 110 unmodified lines
559564 });
560565
561566 active_call_a
562− .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
567+ .update(cx_a, |active_call, cx| {
568+ active_call.join_channel(zed_id, None, cx)
569+ })
563570 .await
564571 .unwrap();
565572
566573 active_call_b
567− .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
574+ .update(cx_b, |active_call, cx| {
575+ active_call.join_channel(zed_id, None, cx)
576+ })
568577 .await
569578 .unwrap();
570579
@@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo · 37 unmodified lines
608617 let active_call_a = cx_a.read(ActiveCall::global);
609618
610619 active_call_a
611− .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
620+ .update(cx_a, |active_call, cx| {
621+ active_call.join_channel(zed_id, None, cx)
622+ })
612623 .await
613624 .unwrap();
614625
@@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo · 12 unmodified lines
627638
628639 active_call_a
629640 .update(cx_a, |active_call, cx| {
630− active_call.join_channel(rust_id, cx)
641+ active_call.join_channel(rust_id, None, cx)
631642 })
632643 .await
633644 .unwrap();
@@ -793,7 +804,7 @@ async fn test_call_from_channel( · 159 unmodified lines
793804 let active_call_b = cx_b.read(ActiveCall::global);
794805
795806 active_call_a
796− .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
807+ .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
797808 .await
798809 .unwrap();
799810
@@ -1286,7 +1297,7 @@ async fn test_guest_access( · 486 unmodified lines
12861297
12871298 // Non-members should not be allowed to join
12881299 assert!(active_call_b
1289− .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
1300+ .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
12901301 .await
12911302 .is_err());
12921303
@@ -1308,7 +1319,7 @@ async fn test_guest_access( · 15 unmodified lines
13081319
13091320 // Client B joins channel A as a guest
13101321 active_call_b
1311− .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
1322+ .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
13121323 .await
13131324 .unwrap();
13141325
@@ -1341,7 +1352,7 @@ async fn test_guest_access( · 26 unmodified lines
13411352 assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
13421353
13431354 active_call_b
1344− .update(cx_b, |call, cx| call.join_channel(channel_b, cx))
1355+ .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
13451356 .await
13461357 .unwrap();
13471358
@@ -1372,7 +1383,7 @@ async fn test_invite_access( · 24 unmodified lines
13721383
13731384 // should not be allowed to join
13741385 assert!(active_call_b
1375− .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
1386+ .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
13761387 .await
13771388 .is_err());
13781389
@@ -1390,7 +1401,7 @@ async fn test_invite_access( · 11 unmodified lines
13901401 .unwrap();
13911402
13921403 active_call_b
1393− .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
1404+ .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
13941405 .await
13951406 .unwrap();
13961407
crates/collab2/src/tests/integration_tests.rs+7−4
@@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 509 unmodified lines
510510
511511 // Simultaneously join channel 1 and then channel 2
512512 active_call_a
513− .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
513+ .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
514514 .detach();
515− let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
515+ let join_channel_2 =
516+ active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
516517
517518 join_channel_2.await.unwrap();
518519
@@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 19 unmodified lines
538539 call.invite(client_c.user_id().unwrap(), None, cx)
539540 });
540541
541− let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
542+ let join_channel =
543+ active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
542544
543545 b_invite.await.unwrap();
544546 c_invite.await.unwrap();
@@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 22 unmodified lines
567569 .unwrap();
568570
569571 // Simultaneously join channel 1 and call user B and user C from client A.
570− let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
572+ let join_channel =
573+ active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
571574
572575 let b_invite = active_call_a.update(cx_a, |call, cx| {
573576 call.invite(client_b.user_id().unwrap(), None, cx)
crates/collab_ui2/src/collab_panel.rs+482−719
@@ -17,6 +17,7 @@ mod contact_finder; · 16 unmodified lines
1717 // Client, Contact, User, UserStore,
1818 // };
1919 use contact_finder::ContactFinder;
20+use menu::Confirm;
2021 use rpc::proto;
2122 // use context_menu::{ContextMenu, ContextMenuItem};
2223 // use db::kvp::KEY_VALUE_STORE;
@@ -90,10 +91,10 @@ use rpc::proto; · 67 unmodified lines
9091 // channel_id: ChannelId,
9192 // }
9293
93−// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94−// pub struct OpenChannelNotes {
95−// pub channel_id: ChannelId,
96−// }
94+#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)]
95+pub struct OpenChannelNotes {
96+ pub channel_id: ChannelId,
97+}
9798
9899 // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99100 // pub struct JoinChannelCall {
@@ -160,26 +161,26 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; · 60 unmodified lines
160161 use std::{iter::once, mem, sync::Arc};
161162
162163 use call::ActiveCall;
163−use channel::{Channel, ChannelId, ChannelStore};
164+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
164165 use client::{Client, Contact, User, UserStore};
165166 use db::kvp::KEY_VALUE_STORE;
166167 use editor::Editor;
167−use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
168+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
168169 use fuzzy::{match_strings, StringMatchCandidate};
169170 use gpui::{
170− actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter,
171− FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement,
172− Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
173− WeakView,
171+ actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div,
172+ EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model,
173+ ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View,
174+ ViewContext, VisualContext, WeakView,
174175 };
175176 use project::Fs;
176177 use serde_derive::{Deserialize, Serialize};
177−use settings::Settings;
178+use settings::{Settings, SettingsStore};
178179 use ui::{
179− h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, Label, List, ListHeader, ListItem,
180− Toggle, Tooltip,
180+ h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List,
181+ ListHeader, ListItem, Toggle, Tooltip,
181182 };
182−use util::{maybe, ResultExt};
183+use util::{maybe, ResultExt, TryFutureExt};
183184 use workspace::{
184185 dock::{DockPosition, Panel, PanelEvent},
185186 notifications::NotifyResultExt,
@@ -293,10 +294,10 @@ pub enum ChannelEditingState { · 107 unmodified lines
293294 }
294295
295296 impl ChannelEditingState {
296− fn pending_name(&self) -> Option<&str> {
297+ fn pending_name(&self) -> Option<String> {
297298 match self {
298− ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
299− ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
299+ ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
300+ ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
300301 }
301302 }
302303 }
@@ -306,10 +307,10 @@ pub struct CollabPanel { · 3 unmodified lines
306307 fs: Arc<dyn Fs>,
307308 focus_handle: FocusHandle,
308309 // channel_clipboard: Option<ChannelMoveClipboard>,
309− // pending_serialization: Task<Option<()>>,
310+ pending_serialization: Task<Option<()>>,
310311 // context_menu: ViewHandle<ContextMenu>,
311312 filter_editor: View<Editor>,
312− // channel_name_editor: ViewHandle<Editor>,
313+ channel_name_editor: View<Editor>,
313314 channel_editing_state: Option<ChannelEditingState>,
314315 entries: Vec<ListEntry>,
315316 selection: Option<usize>,
@@ -322,17 +323,17 @@ pub struct CollabPanel { · 6 unmodified lines
322323 subscriptions: Vec<Subscription>,
323324 collapsed_sections: Vec<Section>,
324325 collapsed_channels: Vec<ChannelId>,
325− // drag_target_channel: ChannelDragTarget,
326+ drag_target_channel: ChannelDragTarget,
326327 workspace: WeakView<Workspace>,
327328 // context_menu_on_selected: bool,
328329 }
329330
330−// #[derive(PartialEq, Eq)]
331−// enum ChannelDragTarget {
332−// None,
333−// Root,
334−// Channel(ChannelId),
335−// }
331+#[derive(PartialEq, Eq)]
332+enum ChannelDragTarget {
333+ None,
334+ Root,
335+ Channel(ChannelId),
336+}
336337
337338 #[derive(Serialize, Deserialize)]
338339 struct SerializedCollabPanel {
@@ -438,28 +439,21 @@ impl CollabPanel { · 99 unmodified lines
438439 // })
439440 // .detach();
440441
441− // let channel_name_editor = cx.add_view(|cx| {
442− // Editor::single_line(
443− // Some(Arc::new(|theme| {
444− // theme.collab_panel.user_query_editor.clone()
445− // })),
446− // cx,
447− // )
448− // });
449−
450− // cx.subscribe(&channel_name_editor, |this, _, event, cx| {
451− // if let editor::Event::Blurred = event {
452− // if let Some(state) = &this.channel_editing_state {
453− // if state.pending_name().is_some() {
454− // return;
455− // }
456− // }
457− // this.take_editing_state(cx);
458− // this.update_entries(false, cx);
459− // cx.notify();
460− // }
461− // })
462− // .detach();
442+ let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
443+
444+ cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
445+ if let editor::EditorEvent::Blurred = event {
446+ if let Some(state) = &this.channel_editing_state {
447+ if state.pending_name().is_some() {
448+ return;
449+ }
450+ }
451+ this.take_editing_state(cx);
452+ this.update_entries(false, cx);
453+ cx.notify();
454+ }
455+ })
456+ .detach();
463457
464458 // let list_state =
465459 // ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
@@ -597,9 +591,9 @@ impl CollabPanel { · 131 unmodified lines
597591 focus_handle: cx.focus_handle(),
598592 // channel_clipboard: None,
599593 fs: workspace.app_state().fs.clone(),
600− // pending_serialization: Task::ready(None),
594+ pending_serialization: Task::ready(None),
601595 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
602− // channel_name_editor,
596+ channel_name_editor,
603597 filter_editor,
604598 entries: Vec::default(),
605599 channel_editing_state: None,
@@ -614,59 +608,58 @@ impl CollabPanel { · 8 unmodified lines
614608 workspace: workspace.weak_handle(),
615609 client: workspace.app_state().client.clone(),
616610 // context_menu_on_selected: true,
617− // drag_target_channel: ChannelDragTarget::None,
611+ drag_target_channel: ChannelDragTarget::None,
618612 // list_state,
619613 };
620614
621615 this.update_entries(false, cx);
622616
623− // // Update the dock position when the setting changes.
624− // let mut old_dock_position = this.position(cx);
625− // this.subscriptions
626− // .push(
627− // cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
628− // let new_dock_position = this.position(cx);
629− // if new_dock_position != old_dock_position {
630− // old_dock_position = new_dock_position;
631− // cx.emit(Event::DockPositionChanged);
632− // }
633− // cx.notify();
634− // }),
635− // );
617+ // Update the dock position when the setting changes.
618+ let mut old_dock_position = this.position(cx);
619+ this.subscriptions.push(cx.observe_global::<SettingsStore>(
620+ move |this: &mut Self, cx| {
621+ let new_dock_position = this.position(cx);
622+ if new_dock_position != old_dock_position {
623+ old_dock_position = new_dock_position;
624+ cx.emit(PanelEvent::ChangePosition);
625+ }
626+ cx.notify();
627+ },
628+ ));
636629
637− // let active_call = ActiveCall::global(cx);
630+ let active_call = ActiveCall::global(cx);
638631 this.subscriptions
639632 .push(cx.observe(&this.user_store, |this, _, cx| {
640633 this.update_entries(true, cx)
641634 }));
642− // this.subscriptions
643− // .push(cx.observe(&this.channel_store, |this, _, cx| {
644− // this.update_entries(true, cx)
645− // }));
646− // this.subscriptions
647− // .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
648− // this.subscriptions
649− // .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
650− // this.update_entries(true, cx)
651− // }));
652− // this.subscriptions.push(cx.subscribe(
653− // &this.channel_store,
654− // |this, _channel_store, e, cx| match e {
655− // ChannelEvent::ChannelCreated(channel_id)
656− // | ChannelEvent::ChannelRenamed(channel_id) => {
657− // if this.take_editing_state(cx) {
658− // this.update_entries(false, cx);
659− // this.selection = this.entries.iter().position(|entry| {
660− // if let ListEntry::Channel { channel, .. } = entry {
661− // channel.id == *channel_id
662− // } else {
663− // false
664− // }
665− // });
666− // }
667− // }
668− // },
669− // ));
635+ this.subscriptions
636+ .push(cx.observe(&this.channel_store, |this, _, cx| {
637+ this.update_entries(true, cx)
638+ }));
639+ this.subscriptions
640+ .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
641+ this.subscriptions
642+ .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
643+ this.update_entries(true, cx)
644+ }));
645+ this.subscriptions.push(cx.subscribe(
646+ &this.channel_store,
647+ |this, _channel_store, e, cx| match e {
648+ ChannelEvent::ChannelCreated(channel_id)
649+ | ChannelEvent::ChannelRenamed(channel_id) => {
650+ if this.take_editing_state(cx) {
651+ this.update_entries(false, cx);
652+ this.selection = this.entries.iter().position(|entry| {
653+ if let ListEntry::Channel { channel, .. } = entry {
654+ channel.id == *channel_id
655+ } else {
656+ false
657+ }
658+ });
659+ }
660+ }
661+ },
662+ ));
670663
671664 this
672665 })
@@ -696,10 +689,9 @@ impl CollabPanel { · 23 unmodified lines
696689 if let Some(serialized_panel) = serialized_panel {
697690 panel.update(cx, |panel, cx| {
698691 panel.width = serialized_panel.width;
699− //todo!(collapsed_channels)
700− // panel.collapsed_channels = serialized_panel
701− // .collapsed_channels
702− // .unwrap_or_else(|| Vec::new());
692+ panel.collapsed_channels = serialized_panel
693+ .collapsed_channels
694+ .unwrap_or_else(|| Vec::new());
703695 cx.notify();
704696 });
705697 }
@@ -707,25 +699,25 @@ impl CollabPanel { · 1 unmodified line
707699 })
708700 }
709701
710− // fn serialize(&mut self, cx: &mut ViewContext<Self>) {
711− // let width = self.width;
712− // let collapsed_channels = self.collapsed_channels.clone();
713− // self.pending_serialization = cx.background().spawn(
714− // async move {
715− // KEY_VALUE_STORE
716− // .write_kvp(
717− // COLLABORATION_PANEL_KEY.into(),
718− // serde_json::to_string(&SerializedCollabPanel {
719− // width,
720− // collapsed_channels: Some(collapsed_channels),
721− // })?,
722− // )
723− // .await?;
724− // anyhow::Ok(())
725− // }
726− // .log_err(),
727− // );
728− // }
702+ fn serialize(&mut self, cx: &mut ViewContext<Self>) {
703+ let width = self.width;
704+ let collapsed_channels = self.collapsed_channels.clone();
705+ self.pending_serialization = cx.background_executor().spawn(
706+ async move {
707+ KEY_VALUE_STORE
708+ .write_kvp(
709+ COLLABORATION_PANEL_KEY.into(),
710+ serde_json::to_string(&SerializedCollabPanel {
711+ width,
712+ collapsed_channels: Some(collapsed_channels),
713+ })?,
714+ )
715+ .await?;
716+ anyhow::Ok(())
717+ }
718+ .log_err(),
719+ );
720+ }
729721
730722 fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
731723 let channel_store = self.channel_store.read(cx);
@@ -1456,16 +1448,16 @@ impl CollabPanel { · 724 unmodified lines
14561448 // .into_any()
14571449 // }
14581450
1459− // fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1460− // if let Some(_) = self.channel_editing_state.take() {
1461− // self.channel_name_editor.update(cx, |editor, cx| {
1462− // editor.set_text("", cx);
1463− // });
1464− // true
1465− // } else {
1466− // false
1467− // }
1468− // }
1451+ fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1452+ if let Some(_) = self.channel_editing_state.take() {
1453+ self.channel_name_editor.update(cx, |editor, cx| {
1454+ editor.set_text("", cx);
1455+ });
1456+ true
1457+ } else {
1458+ false
1459+ }
1460+ }
14691461
14701462 // fn render_contact_placeholder(
14711463 // &self,
@@ -1501,67 +1493,6 @@ impl CollabPanel { · 29 unmodified lines
15011493 // .into_any()
15021494 // }
15031495
1504− // fn render_channel_editor(
1505− // &self,
1506− // theme: &theme::Theme,
1507− // depth: usize,
1508− // cx: &AppContext,
1509− // ) -> AnyElement<Self> {
1510− // Flex::row()
1511− // .with_child(
1512− // Empty::new()
1513− // .constrained()
1514− // .with_width(theme.collab_panel.disclosure.button_space()),
1515− // )
1516− // .with_child(
1517− // Svg::new("icons/hash.svg")
1518− // .with_color(theme.collab_panel.channel_hash.color)
1519− // .constrained()
1520− // .with_width(theme.collab_panel.channel_hash.width)
1521− // .aligned()
1522− // .left(),
1523− // )
1524− // .with_child(
1525− // if let Some(pending_name) = self
1526− // .channel_editing_state
1527− // .as_ref()
1528− // .and_then(|state| state.pending_name())
1529− // {
1530− // Label::new(
1531− // pending_name.to_string(),
1532− // theme.collab_panel.contact_username.text.clone(),
1533− // )
1534− // .contained()
1535− // .with_style(theme.collab_panel.contact_username.container)
1536− // .aligned()
1537− // .left()
1538− // .flex(1., true)
1539− // .into_any()
1540− // } else {
1541− // ChildView::new(&self.channel_name_editor, cx)
1542− // .aligned()
1543− // .left()
1544− // .contained()
1545− // .with_style(theme.collab_panel.channel_editor)
1546− // .flex(1.0, true)
1547− // .into_any()
1548− // },
1549− // )
1550− // .align_children_center()
1551− // .constrained()
1552− // .with_height(theme.collab_panel.row_height)
1553− // .contained()
1554− // .with_style(ContainerStyle {
1555− // background_color: Some(theme.editor.background),
1556− // ..*theme.collab_panel.contact_row.default_style()
1557− // })
1558− // .with_padding_left(
1559− // theme.collab_panel.contact_row.default_style().padding.left
1560− // + theme.collab_panel.channel_indent * depth as f32,
1561− // )
1562− // .into_any()
1563− // }
1564−
15651496 // fn render_channel_notes(
15661497 // &self,
15671498 // channel_id: ChannelId,
@@ -1754,109 +1685,6 @@ impl CollabPanel { · 186 unmodified lines
17541685 // .into_any()
17551686 // }
17561687
1757− // fn render_contact_request(
1758− // user: Arc<User>,
1759− // user_store: ModelHandle<UserStore>,
1760− // theme: &theme::CollabPanel,
1761− // is_incoming: bool,
1762− // is_selected: bool,
1763− // cx: &mut ViewContext<Self>,
1764− // ) -> AnyElement<Self> {
1765− // enum Decline {}
1766− // enum Accept {}
1767− // enum Cancel {}
1768−
1769− // let mut row = Flex::row()
1770− // .with_children(user.avatar.clone().map(|avatar| {
1771− // Image::from_data(avatar)
1772− // .with_style(theme.contact_avatar)
1773− // .aligned()
1774− // .left()
1775− // }))
1776− // .with_child(
1777− // Label::new(
1778− // user.github_login.clone(),
1779− // theme.contact_username.text.clone(),
1780− // )
1781− // .contained()
1782− // .with_style(theme.contact_username.container)
1783− // .aligned()
1784− // .left()
1785− // .flex(1., true),
1786− // );
1787−
1788− // let user_id = user.id;
1789− // let github_login = user.github_login.clone();
1790− // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1791− // let button_spacing = theme.contact_button_spacing;
1792−
1793− // if is_incoming {
1794− // row.add_child(
1795− // MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
1796− // let button_style = if is_contact_request_pending {
1797− // &theme.disabled_button
1798− // } else {
1799− // theme.contact_button.style_for(mouse_state)
1800− // };
1801− // render_icon_button(button_style, "icons/x.svg").aligned()
1802− // })
1803− // .with_cursor_style(CursorStyle::PointingHand)
1804− // .on_click(MouseButton::Left, move |_, this, cx| {
1805− // this.respond_to_contact_request(user_id, false, cx);
1806− // })
1807− // .contained()
1808− // .with_margin_right(button_spacing),
1809− // );
1810−
1811− // row.add_child(
1812− // MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
1813− // let button_style = if is_contact_request_pending {
1814− // &theme.disabled_button
1815− // } else {
1816− // theme.contact_button.style_for(mouse_state)
1817− // };
1818− // render_icon_button(button_style, "icons/check.svg")
1819− // .aligned()
1820− // .flex_float()
1821− // })
1822− // .with_cursor_style(CursorStyle::PointingHand)
1823− // .on_click(MouseButton::Left, move |_, this, cx| {
1824− // this.respond_to_contact_request(user_id, true, cx);
1825− // }),
1826− // );
1827− // } else {
1828− // row.add_child(
1829− // MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
1830− // let button_style = if is_contact_request_pending {
1831− // &theme.disabled_button
1832− // } else {
1833− // theme.contact_button.style_for(mouse_state)
1834− // };
1835− // render_icon_button(button_style, "icons/x.svg")
1836− // .aligned()
1837− // .flex_float()
1838− // })
1839− // .with_padding(Padding::uniform(2.))
1840− // .with_cursor_style(CursorStyle::PointingHand)
1841− // .on_click(MouseButton::Left, move |_, this, cx| {
1842− // this.remove_contact(user_id, &github_login, cx);
1843− // })
1844− // .flex_float(),
1845− // );
1846− // }
1847−
1848− // row.constrained()
1849− // .with_height(theme.row_height)
1850− // .contained()
1851− // .with_style(
1852− // *theme
1853− // .contact_row
1854− // .in_state(is_selected)
1855− // .style_for(&mut Default::default()),
1856− // )
1857− // .into_any()
1858− // }
1859−
18601688 // fn has_subchannels(&self, ix: usize) -> bool {
18611689 // self.entries.get(ix).map_or(false, |entry| {
18621690 // if let ListEntry::Channel { has_children, .. } = entry {
@@ -2054,148 +1882,148 @@ impl CollabPanel { · 191 unmodified lines
20541882 // cx.notify();
20551883 // }
20561884
2057− // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
2058− // if self.confirm_channel_edit(cx) {
2059− // return;
2060− // }
1885+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1886+ if self.confirm_channel_edit(cx) {
1887+ return;
1888+ }
20611889
2062− // if let Some(selection) = self.selection {
2063− // if let Some(entry) = self.entries.get(selection) {
2064− // match entry {
2065− // ListEntry::Header(section) => match section {
2066− // Section::ActiveCall => Self::leave_call(cx),
2067− // Section::Channels => self.new_root_channel(cx),
2068− // Section::Contacts => self.toggle_contact_finder(cx),
2069− // Section::ContactRequests
2070− // | Section::Online
2071− // | Section::Offline
2072− // | Section::ChannelInvites => {
2073− // self.toggle_section_expanded(*section, cx);
2074− // }
2075− // },
2076− // ListEntry::Contact { contact, calling } => {
2077− // if contact.online && !contact.busy && !calling {
2078− // self.call(contact.user.id, Some(self.project.clone()), cx);
2079− // }
2080− // }
2081− // ListEntry::ParticipantProject {
2082− // project_id,
2083− // host_user_id,
2084− // ..
2085− // } => {
2086− // if let Some(workspace) = self.workspace.upgrade(cx) {
2087− // let app_state = workspace.read(cx).app_state().clone();
2088− // workspace::join_remote_project(
2089− // *project_id,
2090− // *host_user_id,
2091− // app_state,
2092− // cx,
2093− // )
2094− // .detach_and_log_err(cx);
2095− // }
2096− // }
2097− // ListEntry::ParticipantScreen { peer_id, .. } => {
2098− // let Some(peer_id) = peer_id else {
2099− // return;
2100− // };
2101− // if let Some(workspace) = self.workspace.upgrade(cx) {
2102− // workspace.update(cx, |workspace, cx| {
2103− // workspace.open_shared_screen(*peer_id, cx)
2104− // });
2105− // }
2106− // }
2107− // ListEntry::Channel { channel, .. } => {
2108− // let is_active = maybe!({
2109− // let call_channel = ActiveCall::global(cx)
2110− // .read(cx)
2111− // .room()?
2112− // .read(cx)
2113− // .channel_id()?;
2114−
2115− // Some(call_channel == channel.id)
2116− // })
2117− // .unwrap_or(false);
2118− // if is_active {
2119− // self.open_channel_notes(
2120− // &OpenChannelNotes {
2121− // channel_id: channel.id,
2122− // },
2123− // cx,
2124− // )
2125− // } else {
2126− // self.join_channel(channel.id, cx)
2127− // }
2128− // }
2129− // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2130− // _ => {}
2131− // }
2132− // }
2133− // }
2134− // }
1890+ // if let Some(selection) = self.selection {
1891+ // if let Some(entry) = self.entries.get(selection) {
1892+ // match entry {
1893+ // ListEntry::Header(section) => match section {
1894+ // Section::ActiveCall => Self::leave_call(cx),
1895+ // Section::Channels => self.new_root_channel(cx),
1896+ // Section::Contacts => self.toggle_contact_finder(cx),
1897+ // Section::ContactRequests
1898+ // | Section::Online
1899+ // | Section::Offline
1900+ // | Section::ChannelInvites => {
1901+ // self.toggle_section_expanded(*section, cx);
1902+ // }
1903+ // },
1904+ // ListEntry::Contact { contact, calling } => {
1905+ // if contact.online && !contact.busy && !calling {
1906+ // self.call(contact.user.id, Some(self.project.clone()), cx);
1907+ // }
1908+ // }
1909+ // ListEntry::ParticipantProject {
1910+ // project_id,
1911+ // host_user_id,
1912+ // ..
1913+ // } => {
1914+ // if let Some(workspace) = self.workspace.upgrade(cx) {
1915+ // let app_state = workspace.read(cx).app_state().clone();
1916+ // workspace::join_remote_project(
1917+ // *project_id,
1918+ // *host_user_id,
1919+ // app_state,
1920+ // cx,
1921+ // )
1922+ // .detach_and_log_err(cx);
1923+ // }
1924+ // }
1925+ // ListEntry::ParticipantScreen { peer_id, .. } => {
1926+ // let Some(peer_id) = peer_id else {
1927+ // return;
1928+ // };
1929+ // if let Some(workspace) = self.workspace.upgrade(cx) {
1930+ // workspace.update(cx, |workspace, cx| {
1931+ // workspace.open_shared_screen(*peer_id, cx)
1932+ // });
1933+ // }
1934+ // }
1935+ // ListEntry::Channel { channel, .. } => {
1936+ // let is_active = maybe!({
1937+ // let call_channel = ActiveCall::global(cx)
1938+ // .read(cx)
1939+ // .room()?
1940+ // .read(cx)
1941+ // .channel_id()?;
1942+
1943+ // Some(call_channel == channel.id)
1944+ // })
1945+ // .unwrap_or(false);
1946+ // if is_active {
1947+ // self.open_channel_notes(
1948+ // &OpenChannelNotes {
1949+ // channel_id: channel.id,
1950+ // },
1951+ // cx,
1952+ // )
1953+ // } else {
1954+ // self.join_channel(channel.id, cx)
1955+ // }
1956+ // }
1957+ // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
1958+ // _ => {}
1959+ // }
1960+ // }
1961+ // }
1962+ }
21351963
2136− // fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
2137− // if self.channel_editing_state.is_some() {
2138− // self.channel_name_editor.update(cx, |editor, cx| {
2139− // editor.insert(" ", cx);
2140− // });
2141− // }
2142− // }
1964+ fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
1965+ if self.channel_editing_state.is_some() {
1966+ self.channel_name_editor.update(cx, |editor, cx| {
1967+ editor.insert(" ", cx);
1968+ });
1969+ }
1970+ }
21431971
2144− // fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2145− // if let Some(editing_state) = &mut self.channel_editing_state {
2146− // match editing_state {
2147− // ChannelEditingState::Create {
2148− // location,
2149− // pending_name,
2150− // ..
2151− // } => {
2152− // if pending_name.is_some() {
2153− // return false;
2154− // }
2155− // let channel_name = self.channel_name_editor.read(cx).text(cx);
1972+ fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
1973+ if let Some(editing_state) = &mut self.channel_editing_state {
1974+ match editing_state {
1975+ ChannelEditingState::Create {
1976+ location,
1977+ pending_name,
1978+ ..
1979+ } => {
1980+ if pending_name.is_some() {
1981+ return false;
1982+ }
1983+ let channel_name = self.channel_name_editor.read(cx).text(cx);
21561984
2157− // *pending_name = Some(channel_name.clone());
1985+ *pending_name = Some(channel_name.clone());
21581986
2159− // self.channel_store
2160− // .update(cx, |channel_store, cx| {
2161− // channel_store.create_channel(&channel_name, *location, cx)
2162− // })
2163− // .detach();
2164− // cx.notify();
2165− // }
2166− // ChannelEditingState::Rename {
2167− // location,
2168− // pending_name,
2169− // } => {
2170− // if pending_name.is_some() {
2171− // return false;
2172− // }
2173− // let channel_name = self.channel_name_editor.read(cx).text(cx);
2174− // *pending_name = Some(channel_name.clone());
2175−
2176− // self.channel_store
2177− // .update(cx, |channel_store, cx| {
2178− // channel_store.rename(*location, &channel_name, cx)
2179− // })
2180− // .detach();
2181− // cx.notify();
2182− // }
2183− // }
2184− // cx.focus_self();
2185− // true
2186− // } else {
2187− // false
2188− // }
2189− // }
1987+ self.channel_store
1988+ .update(cx, |channel_store, cx| {
1989+ channel_store.create_channel(&channel_name, *location, cx)
1990+ })
1991+ .detach();
1992+ cx.notify();
1993+ }
1994+ ChannelEditingState::Rename {
1995+ location,
1996+ pending_name,
1997+ } => {
1998+ if pending_name.is_some() {
1999+ return false;
2000+ }
2001+ let channel_name = self.channel_name_editor.read(cx).text(cx);
2002+ *pending_name = Some(channel_name.clone());
2003+
2004+ self.channel_store
2005+ .update(cx, |channel_store, cx| {
2006+ channel_store.rename(*location, &channel_name, cx)
2007+ })
2008+ .detach();
2009+ cx.notify();
2010+ }
2011+ }
2012+ cx.focus_self();
2013+ true
2014+ } else {
2015+ false
2016+ }
2017+ }
21902018
2191− // fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2192− // if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2193− // self.collapsed_sections.remove(ix);
2194− // } else {
2195− // self.collapsed_sections.push(section);
2196− // }
2197− // self.update_entries(false, cx);
2198− // }
2019+ fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2020+ if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2021+ self.collapsed_sections.remove(ix);
2022+ } else {
2023+ self.collapsed_sections.push(section);
2024+ }
2025+ self.update_entries(false, cx);
2026+ }
21992027
22002028 // fn collapse_selected_channel(
22012029 // &mut self,
@@ -2233,20 +2061,20 @@ impl CollabPanel { · 31 unmodified lines
22332061 // self.toggle_channel_collapsed(action.location, cx);
22342062 // }
22352063
2236− // fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2237− // match self.collapsed_channels.binary_search(&channel_id) {
2238− // Ok(ix) => {
2239− // self.collapsed_channels.remove(ix);
2240− // }
2241− // Err(ix) => {
2242− // self.collapsed_channels.insert(ix, channel_id);
2243− // }
2244− // };
2245− // self.serialize(cx);
2246− // self.update_entries(true, cx);
2247− // cx.notify();
2248− // cx.focus_self();
2249− // }
2064+ fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2065+ match self.collapsed_channels.binary_search(&channel_id) {
2066+ Ok(ix) => {
2067+ self.collapsed_channels.remove(ix);
2068+ }
2069+ Err(ix) => {
2070+ self.collapsed_channels.insert(ix, channel_id);
2071+ }
2072+ };
2073+ self.serialize(cx);
2074+ self.update_entries(true, cx);
2075+ cx.notify();
2076+ cx.focus_self();
2077+ }
22502078
22512079 fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
22522080 self.collapsed_channels.binary_search(&channel_id).is_ok()
@@ -2270,23 +2098,23 @@ impl CollabPanel { · 17 unmodified lines
22702098 }
22712099 }
22722100
2273− // fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2274− // self.channel_editing_state = Some(ChannelEditingState::Create {
2275− // location: None,
2276− // pending_name: None,
2277− // });
2278− // self.update_entries(false, cx);
2279− // self.select_channel_editor();
2280− // cx.focus(self.channel_name_editor.as_any());
2281− // cx.notify();
2282− // }
2101+ fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2102+ self.channel_editing_state = Some(ChannelEditingState::Create {
2103+ location: None,
2104+ pending_name: None,
2105+ });
2106+ self.update_entries(false, cx);
2107+ self.select_channel_editor();
2108+ cx.focus_view(&self.channel_name_editor);
2109+ cx.notify();
2110+ }
22832111
2284− // fn select_channel_editor(&mut self) {
2285− // self.selection = self.entries.iter().position(|entry| match entry {
2286− // ListEntry::ChannelEditor { .. } => true,
2287− // _ => false,
2288− // });
2289− // }
2112+ fn select_channel_editor(&mut self) {
2113+ self.selection = self.entries.iter().position(|entry| match entry {
2114+ ListEntry::ChannelEditor { .. } => true,
2115+ _ => false,
2116+ });
2117+ }
22902118
22912119 // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
22922120 // self.collapsed_channels
@@ -2346,11 +2174,12 @@ impl CollabPanel { · 53 unmodified lines
23462174 // }
23472175 // }
23482176
2349− // fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
2350− // if let Some(workspace) = self.workspace.upgrade(cx) {
2351− // ChannelView::open(action.channel_id, workspace, cx).detach();
2352− // }
2353− // }
2177+ fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
2178+ if let Some(workspace) = self.workspace.upgrade() {
2179+ todo!();
2180+ // ChannelView::open(action.channel_id, workspace, cx).detach();
2181+ }
2182+ }
23542183
23552184 // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
23562185 // let Some(channel) = self.selected_channel() else {
@@ -2439,44 +2268,38 @@ impl CollabPanel { · 82 unmodified lines
24392268 // // Should move to the filter editor if clicking on it
24402269 // // Should move selection to the channel editor if activating it
24412270
2442− // fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2443− // let user_store = self.user_store.clone();
2444− // let prompt_message = format!(
2445− // "Are you sure you want to remove \"{}\" from your contacts?",
2446− // github_login
2447− // );
2448− // let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2449− // let window = cx.window();
2450− // cx.spawn(|_, mut cx| async move {
2451− // if answer.next().await == Some(0) {
2452− // if let Err(e) = user_store
2453− // .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
2454− // .await
2455− // {
2456− // window.prompt(
2457− // PromptLevel::Info,
2458− // &format!("Failed to remove contact: {}", e),
2459− // &["Ok"],
2460− // &mut cx,
2461− // );
2462− // }
2463− // }
2464− // })
2465− // .detach();
2466− // }
2271+ fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2272+ let user_store = self.user_store.clone();
2273+ let prompt_message = format!(
2274+ "Are you sure you want to remove \"{}\" from your contacts?",
2275+ github_login
2276+ );
2277+ let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2278+ let window = cx.window();
2279+ cx.spawn(|_, mut cx| async move {
2280+ if answer.await? == 0 {
2281+ user_store
2282+ .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
2283+ .await
2284+ .notify_async_err(&mut cx);
2285+ }
2286+ anyhow::Ok(())
2287+ })
2288+ .detach_and_log_err(cx);
2289+ }
24672290
2468− // fn respond_to_contact_request(
2469− // &mut self,
2470− // user_id: u64,
2471− // accept: bool,
2472− // cx: &mut ViewContext<Self>,
2473− // ) {
2474− // self.user_store
2475− // .update(cx, |store, cx| {
2476− // store.respond_to_contact_request(user_id, accept, cx)
2477− // })
2478− // .detach();
2479− // }
2291+ fn respond_to_contact_request(
2292+ &mut self,
2293+ user_id: u64,
2294+ accept: bool,
2295+ cx: &mut ViewContext<Self>,
2296+ ) {
2297+ self.user_store
2298+ .update(cx, |store, cx| {
2299+ store.respond_to_contact_request(user_id, accept, cx)
2300+ })
2301+ .detach_and_log_err(cx);
2302+ }
24802303
24812304 // fn respond_to_channel_invite(
24822305 // &mut self,
@@ -2504,21 +2327,22 @@ impl CollabPanel { · 21 unmodified lines
25042327 // .detach_and_log_err(cx);
25052328 // }
25062329
2507− // fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
2508− // let Some(workspace) = self.workspace.upgrade(cx) else {
2509− // return;
2510− // };
2511− // let Some(handle) = cx.window().downcast::<Workspace>() else {
2512− // return;
2513− // };
2514− // workspace::join_channel(
2515− // channel_id,
2516− // workspace.read(cx).app_state().clone(),
2517− // Some(handle),
2518− // cx,
2519− // )
2520− // .detach_and_log_err(cx)
2521− // }
2330+ fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
2331+ let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
2332+ return;
2333+ };
2334+ let active_call = ActiveCall::global(cx);
2335+ cx.spawn(|_, mut cx| async move {
2336+ active_call
2337+ .update(&mut cx, |active_call, cx| {
2338+ active_call.join_channel(channel_id, Some(handle), cx)
2339+ })
2340+ .log_err()?
2341+ .await
2342+ .notify_async_err(&mut cx)
2343+ })
2344+ .detach()
2345+ }
25222346
25232347 // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
25242348 // let channel_id = action.channel_id;
@@ -2590,7 +2414,9 @@ impl CollabPanel { · 65 unmodified lines
25902414 } => self
25912415 .render_channel(&*channel, depth, has_children, is_selected, cx)
25922416 .into_any_element(),
2593− ListEntry::ChannelEditor { depth } => todo!(),
2417+ ListEntry::ChannelEditor { depth } => {
2418+ self.render_channel_editor(depth, cx).into_any_element()
2419+ }
25942420 }
25952421 }))
25962422 }
@@ -2689,10 +2515,7 @@ impl CollabPanel { · 92 unmodified lines
26892515
26902516 Some(
26912517 IconButton::new("add-channel", Icon::Plus)
2692− .on_click(cx.listener(|this, _, cx| {
2693− todo!()
2694− // this.new_root_channel(cx)
2695− }))
2518+ .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
26962519 .tooltip(|cx| Tooltip::text("Create a channel", cx)),
26972520 )
26982521 }
@@ -2707,18 +2530,16 @@ impl CollabPanel { · 8 unmodified lines
27072530 | Section::Offline => true,
27082531 };
27092532
2710− let mut header = ListHeader::new(text);
2711− if let Some(button) = button {
2712− header = header.right_button(button)
2713− }
2714− // todo!() is selected
2715− if can_collapse {
2716− // todo!() on click to toggle
2717− header = header.toggle(ui::Toggle::Toggled(is_collapsed));
2718− }
2719−
2720− header
2533+ ListHeader::new(text)
2534+ .when_some(button, |el, button| el.right_button(button))
2535+ .selected(is_selected)
2536+ .when(can_collapse, |el| {
2537+ el.toggle(ui::Toggle::Toggled(is_collapsed)).on_toggle(
2538+ cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)),
2539+ )
2540+ })
27212541 }
2542+
27222543 fn render_contact(
27232544 &mut self,
27242545 contact: &Contact,
@@ -2742,10 +2563,32 @@ impl CollabPanel { · 17 unmodified lines
27422563 })
27432564 .log_err();
27442565 }))
2745− .child(Label::new(github_login.clone()));
2566+ .child(
2567+ h_stack()
2568+ .w_full()
2569+ .justify_between()
2570+ .child(Label::new(github_login.clone()))
2571+ .child(
2572+ div()
2573+ .id("remove_contact")
2574+ .invisible()
2575+ .group_hover("", |style| style.visible())
2576+ .child(
2577+ IconButton::new("remove_contact", Icon::Close)
2578+ .color(Color::Muted)
2579+ .tooltip(|cx| Tooltip::text("Remove Contact", cx))
2580+ .on_click(cx.listener(move |this, _, cx| {
2581+ this.remove_contact(user_id, &github_login, cx);
2582+ })),
2583+ ),
2584+ ),
2585+ );
2586+
27462587 if let Some(avatar) = contact.user.avatar.clone() {
2747− //item = item.left_avatar(avatar);
2588+ item = item.left_avatar(avatar);
27482589 }
2590+
2591+ div().group("").child(item)
27492592 // let event_handler =
27502593 // MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
27512594 // Flex::row()
@@ -2774,40 +2617,7 @@ impl CollabPanel { · 22 unmodified lines
27742617 // )
27752618 // .with_children(status_badge)
27762619 // }))
2777− // .with_child(
2778− // Label::new(
2779− // contact.user.github_login.clone(),
2780− // collab_theme.contact_username.text.clone(),
2781− // )
2782− // .contained()
2783− // .with_style(collab_theme.contact_username.container)
2784− // .aligned()
2785− // .left()
2786− // .flex(1., true),
2787− // )
2788− // .with_children(if state.hovered() {
2789− // Some(
2790− // MouseEventHandler::new::<Cancel, _>(
2791− // contact.user.id as usize,
2792− // cx,
2793− // |mouse_state, _| {
2794− // let button_style =
2795− // collab_theme.contact_button.style_for(mouse_state);
2796− // render_icon_button(button_style, "icons/x.svg")
2797− // .aligned()
2798− // .flex_float()
2799− // },
2800− // )
2801− // .with_padding(Padding::uniform(2.))
2802− // .with_cursor_style(CursorStyle::PointingHand)
2803− // .on_click(MouseButton::Left, move |_, this, cx| {
2804− // this.remove_contact(user_id, &github_login, cx);
2805− // })
2806− // .flex_float(),
2807− // )
2808− // } else {
2809− // None
2810− // })
2620+
28112621 // .with_children(if calling {
28122622 // Some(
28132623 // Label::new("Calling", collab_theme.calling_indicator.text.clone())
@@ -2865,8 +2675,6 @@ impl CollabPanel { · 51 unmodified lines
28652675 // )
28662676 // .into_any()
28672677 // };
2868−
2869− item
28702678 }
28712679
28722680 fn render_contact_request(
@@ -2877,104 +2685,48 @@ impl CollabPanel { · 4 unmodified lines
28772685 cx: &mut ViewContext<Self>,
28782686 ) -> impl IntoElement {
28792687 let github_login = SharedString::from(user.github_login.clone());
2688+ let user_id = user.id;
2689+ let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
2690+ let color = if is_contact_request_pending {
2691+ Color::Muted
2692+ } else {
2693+ Color::Default
2694+ };
28802695
2881− let mut item = ListItem::new(github_login.clone())
2882− .child(Label::new(github_login.clone()))
2883− .on_click(cx.listener(|this, _, cx| {
2884− todo!();
2885− }));
2886− if let Some(avatar) = user.avatar.clone() {
2887− item = item.left_avatar(avatar);
2888− }
2889− // .with_children(user.avatar.clone().map(|avatar| {
2890− // Image::from_data(avatar)
2891− // .with_style(theme.contact_avatar)
2892− // .aligned()
2893− // .left()
2894− // }))
2895− // .with_child(
2896− // Label::new(
2897− // user.github_login.clone(),
2898− // theme.contact_username.text.clone(),
2899− // )
2900− // .contained()
2901− // .with_style(theme.contact_username.container)
2902− // .aligned()
2903− // .left()
2904− // .flex(1., true),
2905− // );
2906−
2907− // let user_id = user.id;
2908− // let github_login = user.github_login.clone();
2909− // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
2910− // let button_spacing = theme.contact_button_spacing;
2911−
2912− // if is_incoming {
2913− // row.add_child(
2914− // MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
2915− // let button_style = if is_contact_request_pending {
2916− // &theme.disabled_button
2917− // } else {
2918− // theme.contact_button.style_for(mouse_state)
2919− // };
2920− // render_icon_button(button_style, "icons/x.svg").aligned()
2921− // })
2922− // .with_cursor_style(CursorStyle::PointingHand)
2923− // .on_click(MouseButton::Left, move |_, this, cx| {
2924− // this.respond_to_contact_request(user_id, false, cx);
2925− // })
2926− // .contained()
2927− // .with_margin_right(button_spacing),
2928− // );
2929−
2930− // row.add_child(
2931− // MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
2932− // let button_style = if is_contact_request_pending {
2933− // &theme.disabled_button
2934− // } else {
2935− // theme.contact_button.style_for(mouse_state)
2936− // };
2937− // render_icon_button(button_style, "icons/check.svg")
2938− // .aligned()
2939− // .flex_float()
2940− // })
2941− // .with_cursor_style(CursorStyle::PointingHand)
2942− // .on_click(MouseButton::Left, move |_, this, cx| {
2943− // this.respond_to_contact_request(user_id, true, cx);
2944− // }),
2945− // );
2946− // } else {
2947− // row.add_child(
2948− // MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
2949− // let button_style = if is_contact_request_pending {
2950− // &theme.disabled_button
2951− // } else {
2952− // theme.contact_button.style_for(mouse_state)
2953− // };
2954− // render_icon_button(button_style, "icons/x.svg")
2955− // .aligned()
2956− // .flex_float()
2957− // })
2958− // .with_padding(Padding::uniform(2.))
2959− // .with_cursor_style(CursorStyle::PointingHand)
2960− // .on_click(MouseButton::Left, move |_, this, cx| {
2961− // this.remove_contact(user_id, &github_login, cx);
2962− // })
2963− // .flex_float(),
2964− // );
2965− // }
2696+ let controls = if is_incoming {
2697+ vec![
2698+ IconButton::new("remove_contact", Icon::Close)
2699+ .on_click(cx.listener(move |this, _, cx| {
2700+ this.respond_to_contact_request(user_id, false, cx);
2701+ }))
2702+ .color(color)
2703+ .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2704+ IconButton::new("remove_contact", Icon::Check)
2705+ .on_click(cx.listener(move |this, _, cx| {
2706+ this.respond_to_contact_request(user_id, true, cx);
2707+ }))
2708+ .color(color)
2709+ .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2710+ ]
2711+ } else {
2712+ let github_login = github_login.clone();
2713+ vec![IconButton::new("remove_contact", Icon::Close)
2714+ .on_click(cx.listener(move |this, _, cx| {
2715+ this.remove_contact(user_id, &github_login, cx);
2716+ }))
2717+ .color(color)
2718+ .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2719+ };
29662720
2967− // row.constrained()
2968− // .with_height(theme.row_height)
2969− // .contained()
2970− // .with_style(
2971− // *theme
2972− // .contact_row
2973− // .in_state(is_selected)
2974− // .style_for(&mut Default::default()),
2975− // )
2976− // .into_any()
2977− item
2721+ ListItem::new(github_login.clone())
2722+ .child(
2723+ h_stack()
2724+ .w_full()
2725+ .justify_between()
2726+ .child(Label::new(github_login.clone()))
2727+ .child(h_stack().children(controls)),
2728+ )
2729+ .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar))
29782730 }
29792731
29802732 fn render_contact_placeholder(
@@ -2983,34 +2735,10 @@ impl CollabPanel { · 2 unmodified lines
29832735 cx: &mut ViewContext<Self>,
29842736 ) -> impl IntoElement {
29852737 ListItem::new("contact-placeholder")
2738+ .child(IconElement::new(Icon::Plus))
29862739 .child(Label::new("Add a Contact"))
2987− .on_click(cx.listener(|this, _, cx| todo!()))
2988− // enum AddContacts {}
2989− // MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
2990− // let style = theme.list_empty_state.style_for(is_selected, state);
2991− // Flex::row()
2992− // .with_child(
2993− // Svg::new("icons/plus.svg")
2994− // .with_color(theme.list_empty_icon.color)
2995− // .constrained()
2996− // .with_width(theme.list_empty_icon.width)
2997− // .aligned()
2998− // .left(),
2999− // )
3000− // .with_child(
3001− // Label::new("Add a contact", style.text.clone())
3002− // .contained()
3003− // .with_style(theme.list_empty_label_container),
3004− // )
3005− // .align_children_center()
3006− // .contained()
3007− // .with_style(style.container)
3008− // .into_any()
3009− // })
3010− // .on_click(MouseButton::Left, |_, this, cx| {
3011− // this.toggle_contact_finder(cx);
3012− // })
3013− // .into_any()
2740+ .selected(is_selected)
2741+ .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
30142742 }
30152743
30162744 fn render_channel(
@@ -3023,6 +2751,15 @@ impl CollabPanel { · 6 unmodified lines
30232751 ) -> impl IntoElement {
30242752 let channel_id = channel.id;
30252753
2754+ let is_active = maybe!({
2755+ let call_channel = ActiveCall::global(cx)
2756+ .read(cx)
2757+ .room()?
2758+ .read(cx)
2759+ .channel_id()?;
2760+ Some(call_channel == channel_id)
2761+ })
2762+ .unwrap_or(false);
30262763 let is_public = self
30272764 .channel_store
30282765 .read(cx)
@@ -3034,17 +2771,7 @@ impl CollabPanel { · 5 unmodified lines
30342771 .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok())
30352772 .unwrap_or(false);
30362773
3037− let is_active = maybe!({
3038− let call_channel = ActiveCall::global(cx)
3039− .read(cx)
3040− .room()?
3041− .read(cx)
3042− .channel_id()?;
3043− Some(call_channel == channel_id)
3044− })
3045− .unwrap_or(false);
3046−
3047− let has_messages_notification = channel.unseen_message_id.is_some() || true;
2774+ let has_messages_notification = channel.unseen_message_id.is_some();
30482775 let has_notes_notification = channel.unseen_note_version.is_some();
30492776
30502777 const FACEPILE_LIMIT: usize = 3;
@@ -3052,6 +2779,7 @@ impl CollabPanel { · 1 unmodified line
30522779
30532780 let face_pile = if !participants.is_empty() {
30542781 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2782+ let user = &participants[0];
30552783
30562784 let result = FacePile {
30572785 faces: participants
@@ -3059,6 +2787,7 @@ impl CollabPanel { · 1 unmodified line
30592787 .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
30602788 .take(FACEPILE_LIMIT)
30612789 .chain(if extra_count > 0 {
2790+ // todo!() @nate - this label looks wrong.
30622791 Some(Label::new(format!("+{}", extra_count)).into_any_element())
30632792 } else {
30642793 None
@@ -3081,7 +2810,7 @@ impl CollabPanel { · 16 unmodified lines
30812810 .w_full()
30822811 .justify_between()
30832812 .child(
3084− div()
2813+ h_stack()
30852814 .id(channel_id as usize)
30862815 .child(Label::new(channel.name.clone()))
30872816 .children(face_pile.map(|face_pile| face_pile.render(cx)))
@@ -3092,11 +2821,10 @@ impl CollabPanel { · 4 unmodified lines
30922821 .child(
30932822 div()
30942823 .id("channel_chat")
3095− .bg(gpui::blue())
30962824 .when(!has_messages_notification, |el| el.invisible())
30972825 .group_hover("", |style| style.visible())
30982826 .child(
3099− IconButton::new("test_chat", Icon::MessageBubbles)
2827+ IconButton::new("channel_chat", Icon::MessageBubbles)
31002828 .color(if has_messages_notification {
31012829 Color::Default
31022830 } else {
@@ -3111,20 +2839,16 @@ impl CollabPanel { · 8 unmodified lines
31112839 .when(!has_notes_notification, |el| el.invisible())
31122840 .group_hover("", |style| style.visible())
31132841 .child(
3114− div().child("Notes").id("test_notes").tooltip(|cx| {
3115− Tooltip::text("Open channel notes", cx)
3116− }),
3117− ), // .child(
3118− // IconButton::new("channel_notes", Icon::File)
3119− // .color(if has_notes_notification {
3120− // Color::Default
3121− // } else {
3122− // Color::Muted
3123− // })
3124− // .tooltip(|cx| {
3125− // Tooltip::text("Open channel notes", cx)
3126− // }),
3127− // ),
2842+ IconButton::new("channel_notes", Icon::File)
2843+ .color(if has_notes_notification {
2844+ Color::Default
2845+ } else {
2846+ Color::Muted
2847+ })
2848+ .tooltip(|cx| {
2849+ Tooltip::text("Open channel notes", cx)
2850+ }),
2851+ ),
31282852 ),
31292853 ),
31302854 )
@@ -3133,7 +2857,18 @@ impl CollabPanel { · 2 unmodified lines
31332857 } else {
31342858 Toggle::NotToggleable
31352859 })
3136− .on_click(cx.listener(|this, _, cx| todo!()))
2860+ .on_toggle(
2861+ cx.listener(move |this, _, cx| this.toggle_channel_collapsed(channel_id, cx)),
2862+ )
2863+ .on_click(cx.listener(move |this, _, cx| {
2864+ if this.drag_target_channel == ChannelDragTarget::None {
2865+ if is_active {
2866+ this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
2867+ } else {
2868+ this.join_channel(channel_id, cx)
2869+ }
2870+ }
2871+ }))
31372872 .on_secondary_mouse_down(cx.listener(|this, _, cx| {
31382873 todo!() // open context menu
31392874 })),
@@ -3445,6 +3180,32 @@ impl CollabPanel { · 305 unmodified lines
34453180 // .with_cursor_style(CursorStyle::PointingHand)
34463181 // .into_any()
34473182 }
3183+
3184+ fn render_channel_editor(
3185+ &mut self,
3186+ depth: usize,
3187+ cx: &mut ViewContext<Self>,
3188+ ) -> impl IntoElement {
3189+ let item = ListItem::new("channel-editor")
3190+ .inset(false)
3191+ .indent_level(depth)
3192+ .left_icon(Icon::Hash);
3193+
3194+ if let Some(pending_name) = self
3195+ .channel_editing_state
3196+ .as_ref()
3197+ .and_then(|state| state.pending_name())
3198+ {
3199+ item.child(Label::new(pending_name))
3200+ } else {
3201+ item.child(
3202+ div()
3203+ .w_full()
3204+ .py_1() // todo!() @nate this is a px off at the default font size.
3205+ .child(self.channel_name_editor.clone()),
3206+ )
3207+ }
3208+ }
34483209 }
34493210
34503211 // fn render_tree_branch(
@@ -3499,6 +3260,8 @@ impl Render for CollabPanel { · 48 unmodified lines
34993260 div()
35003261 .key_context("CollabPanel")
35013262 .track_focus(&self.focus_handle)
3263+ .on_action(cx.listener(Self::confirm))
3264+ .on_action(cx.listener(Self::insert_space))
35023265 .map(|el| {
35033266 if self.user_store.read(cx).current_user().is_none() {
35043267 el.child(self.render_signed_out(cx))
crates/collab_ui2/src/collab_panel/contact_finder.rs+0−1
@@ -181,7 +181,6 @@ impl PickerDelegate for ContactFinderDelegate { · 180 unmodified lines
181181 ContactRequestStatus::RequestSent => Some("icons/x.svg"),
182182 ContactRequestStatus::RequestAccepted => None,
183183 };
184− dbg!(icon_path);
185184 Some(
186185 div()
187186 .flex_1()
crates/collab_ui2/src/collab_titlebar_item.rs+25−1
@@ -37,7 +37,10 @@ use gpui::{ · 36 unmodified lines
3737 };
3838 use project::Project;
3939 use theme::ActiveTheme;
40−use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
40+use ui::{
41+ h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton,
42+ IconElement, IconSize, KeyBinding, Tooltip,
43+};
4144 use util::ResultExt;
4245 use workspace::{notifications::NotifyResultExt, Workspace};
4346
@@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem { · 254 unmodified lines
298301 })
299302 .detach();
300303 }))
304+ // Temporary, will be removed when the last part of button2 is merged
305+ .child(
306+ div().border().border_color(gpui::blue()).child(
307+ ButtonLike::new("test-button")
308+ .children([
309+ Avatar::uri(
310+ "https://avatars.githubusercontent.com/u/1714999?v=4",
311+ )
312+ .into_element()
313+ .into_any(),
314+ IconElement::new(ui::Icon::ChevronDown)
315+ .size(IconSize::Small)
316+ .into_element()
317+ .into_any(),
318+ ])
319+ .on_click(move |event, _cx| {
320+ dbg!(format!("clicked: {:?}", event.down.position));
321+ })
322+ .tooltip(|cx| Tooltip::text("Test tooltip", cx)),
323+ ),
324+ )
301325 }
302326 })
303327 }
crates/command_palette2/src/command_palette.rs+6−4
@@ -1,3 +1,8 @@
1+use std::{
2+ cmp::{self, Reverse},
3+ sync::Arc,
4+};
5+
16 use collections::{CommandPaletteFilter, HashMap};
27 use fuzzy::{StringMatch, StringMatchCandidate};
38 use gpui::{
@@ -5,10 +10,7 @@ use gpui::{ · 1 unmodified line
510 Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
611 };
712 use picker::{Picker, PickerDelegate};
8−use std::{
9− cmp::{self, Reverse},
10− sync::Arc,
11−};
13+
1214 use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
1315 use util::{
1416 channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
crates/feature_flags2/src/feature_flags2.rs+2−2
@@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> { · 29 unmodified lines
3030
3131 impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
3232 where
33− V: 'static + Send + Sync,
33+ V: 'static,
3434 {
3535 fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
3636 where
37− F: Fn(bool, &mut V, &mut ViewContext<V>) + Send + Sync + 'static,
37+ F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
3838 {
3939 self.observe_global::<FeatureFlags>(move |v, cx| {
4040 let feature_flags = cx.global::<FeatureFlags>();
crates/gpui2/src/action.rs+1−0
@@ -162,6 +162,7 @@ macro_rules! actions { · 161 unmodified lines
162162
163163 ( $name:ident ) => {
164164 #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
165+ #[serde(crate = "gpui::serde")]
165166 pub struct $name;
166167 };
167168
crates/gpui2/src/element.rs+32−12
@@ -111,7 +111,7 @@ pub struct Component<C> { · 110 unmodified lines
111111
112112 pub struct CompositeElementState<C: RenderOnce> {
113113 rendered_element: Option<<C::Rendered as IntoElement>::Element>,
114− rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
114+ rendered_element_state: Option<<<C::Rendered as IntoElement>::Element as Element>::State>,
115115 }
116116
117117 impl<C> Component<C> {
@@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> { · 13 unmodified lines
131131 cx: &mut WindowContext,
132132 ) -> (LayoutId, Self::State) {
133133 let mut element = self.component.take().unwrap().render(cx).into_element();
134− let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
135− let state = CompositeElementState {
136− rendered_element: Some(element),
137− rendered_element_state: state,
138− };
139− (layout_id, state)
134+ if let Some(element_id) = element.element_id() {
135+ let layout_id =
136+ cx.with_element_state(element_id, |state, cx| element.layout(state, cx));
137+ let state = CompositeElementState {
138+ rendered_element: Some(element),
139+ rendered_element_state: None,
140+ };
141+ (layout_id, state)
142+ } else {
143+ let (layout_id, state) =
144+ element.layout(state.and_then(|s| s.rendered_element_state), cx);
145+ let state = CompositeElementState {
146+ rendered_element: Some(element),
147+ rendered_element_state: Some(state),
148+ };
149+ (layout_id, state)
150+ }
140151 }
141152
142153 fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
143− state
144− .rendered_element
145− .take()
146− .unwrap()
147− .paint(bounds, &mut state.rendered_element_state, cx);
154+ let element = state.rendered_element.take().unwrap();
155+ if let Some(element_id) = element.element_id() {
156+ cx.with_element_state(element_id, |element_state, cx| {
157+ let mut element_state = element_state.unwrap();
158+ element.paint(bounds, &mut element_state, cx);
159+ ((), element_state)
160+ });
161+ } else {
162+ element.paint(
163+ bounds,
164+ &mut state.rendered_element_state.as_mut().unwrap(),
165+ cx,
166+ );
167+ }
148168 }
149169 }
150170
crates/gpui2/src/elements/uniform_list.rs+2−4
@@ -173,7 +173,7 @@ impl Element for UniformList { · 172 unmodified lines
173173 let item_size = element_state.item_size;
174174 let content_size = Size {
175175 width: padded_bounds.size.width,
176− height: item_size.height * self.item_count,
176+ height: item_size.height * self.item_count + padding.top + padding.bottom,
177177 };
178178
179179 let shared_scroll_offset = element_state
@@ -221,9 +221,7 @@ impl Element for UniformList { · 41 unmodified lines
221221
222222 let items = (self.render_items)(visible_range.clone(), cx);
223223 cx.with_z_index(1, |cx| {
224− let content_mask = ContentMask {
225− bounds: padded_bounds,
226− };
224+ let content_mask = ContentMask { bounds };
227225 cx.with_content_mask(Some(content_mask), |cx| {
228226 for (item, ix) in items.into_iter().zip(visible_range) {
229227 let item_origin = padded_bounds.origin
crates/gpui2/src/window.rs+0−17
@@ -1939,23 +1939,6 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> { · 1938 unmodified lines
19391939 })
19401940 }
19411941
1942− /// Like `with_element_state`, but for situations where the element_id is optional. If the
1943− /// id is `None`, no state will be retrieved or stored.
1944− fn with_optional_element_state<S, R>(
1945− &mut self,
1946− element_id: Option<ElementId>,
1947− f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
1948− ) -> R
1949− where
1950− S: 'static,
1951− {
1952− if let Some(element_id) = element_id {
1953− self.with_element_state(element_id, f)
1954− } else {
1955− f(None, self).0
1956− }
1957− }
1958−
19591942 /// Obtain the current content mask.
19601943 fn content_mask(&self) -> ContentMask<Pixels> {
19611944 self.window()
crates/picker2/src/picker2.rs+24−5
@@ -1,7 +1,8 @@
11 use editor::Editor;
22 use gpui::{
3− div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
4− MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
3+ div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
4+ MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
5+ WindowContext,
56 };
67 use std::{cmp, sync::Arc};
78 use ui::{prelude::*, v_stack, Color, Divider, Label};
@@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> { · 8 unmodified lines
1617
1718 pub trait PickerDelegate: Sized + 'static {
1819 type ListItem: IntoElement;
19−
2020 fn match_count(&self) -> usize;
2121 fn selected_index(&self) -> usize;
2222 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
@@ -205,7 +205,6 @@ impl<D: PickerDelegate> Render for Picker<D> { · 182 unmodified lines
205205 .when(self.delegate.match_count() > 0, |el| {
206206 el.child(
207207 v_stack()
208− .p_1()
209208 .grow()
210209 .child(
211210 uniform_list(
@@ -239,7 +238,8 @@ impl<D: PickerDelegate> Render for Picker<D> { · 27 unmodified lines
239238 }
240239 },
241240 )
242− .track_scroll(self.scroll_handle.clone()),
241+ .track_scroll(self.scroll_handle.clone())
242+ .p_1()
243243 )
244244 .max_h_72()
245245 .overflow_hidden(),
@@ -256,3 +256,22 @@ impl<D: PickerDelegate> Render for Picker<D> { · 10 unmodified lines
256256 })
257257 }
258258 }
259+
260+pub fn simple_picker_match(
261+ selected: bool,
262+ cx: &mut WindowContext,
263+ children: impl FnOnce(&mut WindowContext) -> AnyElement,
264+) -> AnyElement {
265+ let colors = cx.theme().colors();
266+
267+ div()
268+ .px_1()
269+ .text_color(colors.text)
270+ .text_ui()
271+ .bg(colors.ghost_element_background)
272+ .rounded_md()
273+ .when(selected, |this| this.bg(colors.ghost_element_selected))
274+ .hover(|this| this.bg(colors.ghost_element_hover))
275+ .child((children)(cx))
276+ .into_any()
277+}
crates/prettier/src/prettier.rs+4−0
@@ -13,12 +13,14 @@ use node_runtime::NodeRuntime; · 12 unmodified lines
1313 use serde::{Deserialize, Serialize};
1414 use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
1515
16+#[derive(Clone)]
1617 pub enum Prettier {
1718 Real(RealPrettier),
1819 #[cfg(any(test, feature = "test-support"))]
1920 Test(TestPrettier),
2021 }
2122
23+#[derive(Clone)]
2224 pub struct RealPrettier {
2325 default: bool,
2426 prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier { · 1 unmodified line
2628 }
2729
2830 #[cfg(any(test, feature = "test-support"))]
31+#[derive(Clone)]
2932 pub struct TestPrettier {
3033 prettier_dir: PathBuf,
3134 default: bool,
3235 }
3336
37+pub const FAIL_THRESHOLD: usize = 4;
3438 pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
3539 pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
3640 const PRETTIER_PACKAGE_NAME: &str = "prettier";
crates/prettier/src/prettier_server.js+3−0
@@ -153,7 +153,10 @@ async function handleMessage(message, prettier) { · 152 unmodified lines
153153 const { method, id, params } = message;
154154 if (method === undefined) {
155155 throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
156+ } else if (method == "initialized") {
157+ return;
156158 }
159+
157160 if (id === undefined) {
158161 throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
159162 }
crates/prettier2/src/prettier2.rs+4−0
@@ -13,12 +13,14 @@ use std::{ · 12 unmodified lines
1313 };
1414 use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
1515
16+#[derive(Clone)]
1617 pub enum Prettier {
1718 Real(RealPrettier),
1819 #[cfg(any(test, feature = "test-support"))]
1920 Test(TestPrettier),
2021 }
2122
23+#[derive(Clone)]
2224 pub struct RealPrettier {
2325 default: bool,
2426 prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier { · 1 unmodified line
2628 }
2729
2830 #[cfg(any(test, feature = "test-support"))]
31+#[derive(Clone)]
2932 pub struct TestPrettier {
3033 prettier_dir: PathBuf,
3134 default: bool,
3235 }
3336
37+pub const FAIL_THRESHOLD: usize = 4;
3438 pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
3539 pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
3640 const PRETTIER_PACKAGE_NAME: &str = "prettier";
crates/prettier2/src/prettier_server.js+3−0
@@ -153,7 +153,10 @@ async function handleMessage(message, prettier) { · 152 unmodified lines
153153 const { method, id, params } = message;
154154 if (method === undefined) {
155155 throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
156+ } else if (method == "initialized") {
157+ return;
156158 }
159+
157160 if (id === undefined) {
158161 throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
159162 }
crates/project/src/prettier_support.rs+758−0
@@ -0,0 +1,758 @@
1+use std::{
2+ ops::ControlFlow,
3+ path::{Path, PathBuf},
4+ sync::Arc,
5+};
6+
7+use anyhow::Context;
8+use collections::HashSet;
9+use fs::Fs;
10+use futures::{
11+ future::{self, Shared},
12+ FutureExt,
13+};
14+use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task};
15+use language::{
16+ language_settings::{Formatter, LanguageSettings},
17+ Buffer, Language, LanguageServerName, LocalFile,
18+};
19+use lsp::LanguageServerId;
20+use node_runtime::NodeRuntime;
21+use prettier::Prettier;
22+use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
23+
24+use crate::{
25+ Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
26+};
27+
28+pub fn prettier_plugins_for_language(
29+ language: &Language,
30+ language_settings: &LanguageSettings,
31+) -> Option<HashSet<&'static str>> {
32+ match &language_settings.formatter {
33+ Formatter::Prettier { .. } | Formatter::Auto => {}
34+ Formatter::LanguageServer | Formatter::External { .. } => return None,
35+ };
36+ let mut prettier_plugins = None;
37+ if language.prettier_parser_name().is_some() {
38+ prettier_plugins
39+ .get_or_insert_with(|| HashSet::default())
40+ .extend(
41+ language
42+ .lsp_adapters()
43+ .iter()
44+ .flat_map(|adapter| adapter.prettier_plugins()),
45+ )
46+ }
47+
48+ prettier_plugins
49+}
50+
51+pub(super) async fn format_with_prettier(
52+ project: &ModelHandle<Project>,
53+ buffer: &ModelHandle<Buffer>,
54+ cx: &mut AsyncAppContext,
55+) -> Option<FormatOperation> {
56+ if let Some((prettier_path, prettier_task)) = project
57+ .update(cx, |project, cx| {
58+ project.prettier_instance_for_buffer(buffer, cx)
59+ })
60+ .await
61+ {
62+ match prettier_task.await {
63+ Ok(prettier) => {
64+ let buffer_path = buffer.update(cx, |buffer, cx| {
65+ File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
66+ });
67+ match prettier.format(buffer, buffer_path, cx).await {
68+ Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
69+ Err(e) => {
70+ log::error!(
71+ "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
72+ );
73+ }
74+ }
75+ }
76+ Err(e) => project.update(cx, |project, _| {
77+ let instance_to_update = match prettier_path {
78+ Some(prettier_path) => {
79+ log::error!(
80+ "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
81+ );
82+ project.prettier_instances.get_mut(&prettier_path)
83+ }
84+ None => {
85+ log::error!("Default prettier instance failed to spawn: {e:#}");
86+ match &mut project.default_prettier.prettier {
87+ PrettierInstallation::NotInstalled { .. } => None,
88+ PrettierInstallation::Installed(instance) => Some(instance),
89+ }
90+ }
91+ };
92+
93+ if let Some(instance) = instance_to_update {
94+ instance.attempt += 1;
95+ instance.prettier = None;
96+ }
97+ }),
98+ }
99+ }
100+
101+ None
102+}
103+
104+pub struct DefaultPrettier {
105+ prettier: PrettierInstallation,
106+ installed_plugins: HashSet<&'static str>,
107+}
108+
109+pub enum PrettierInstallation {
110+ NotInstalled {
111+ attempts: usize,
112+ installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
113+ not_installed_plugins: HashSet<&'static str>,
114+ },
115+ Installed(PrettierInstance),
116+}
117+
118+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
119+
120+#[derive(Clone)]
121+pub struct PrettierInstance {
122+ attempt: usize,
123+ prettier: Option<PrettierTask>,
124+}
125+
126+impl Default for DefaultPrettier {
127+ fn default() -> Self {
128+ Self {
129+ prettier: PrettierInstallation::NotInstalled {
130+ attempts: 0,
131+ installation_task: None,
132+ not_installed_plugins: HashSet::default(),
133+ },
134+ installed_plugins: HashSet::default(),
135+ }
136+ }
137+}
138+
139+impl DefaultPrettier {
140+ pub fn instance(&self) -> Option<&PrettierInstance> {
141+ if let PrettierInstallation::Installed(instance) = &self.prettier {
142+ Some(instance)
143+ } else {
144+ None
145+ }
146+ }
147+
148+ pub fn prettier_task(
149+ &mut self,
150+ node: &Arc<dyn NodeRuntime>,
151+ worktree_id: Option<WorktreeId>,
152+ cx: &mut ModelContext<'_, Project>,
153+ ) -> Option<Task<anyhow::Result<PrettierTask>>> {
154+ match &mut self.prettier {
155+ PrettierInstallation::NotInstalled { .. } => {
156+ Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
157+ }
158+ PrettierInstallation::Installed(existing_instance) => {
159+ existing_instance.prettier_task(node, None, worktree_id, cx)
160+ }
161+ }
162+ }
163+}
164+
165+impl PrettierInstance {
166+ pub fn prettier_task(
167+ &mut self,
168+ node: &Arc<dyn NodeRuntime>,
169+ prettier_dir: Option<&Path>,
170+ worktree_id: Option<WorktreeId>,
171+ cx: &mut ModelContext<'_, Project>,
172+ ) -> Option<Task<anyhow::Result<PrettierTask>>> {
173+ if self.attempt > prettier::FAIL_THRESHOLD {
174+ match prettier_dir {
175+ Some(prettier_dir) => log::warn!(
176+ "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
177+ ),
178+ None => log::warn!("Default prettier exceeded launch threshold, not starting"),
179+ }
180+ return None;
181+ }
182+ Some(match &self.prettier {
183+ Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
184+ None => match prettier_dir {
185+ Some(prettier_dir) => {
186+ let new_task = start_prettier(
187+ Arc::clone(node),
188+ prettier_dir.to_path_buf(),
189+ worktree_id,
190+ cx,
191+ );
192+ self.attempt += 1;
193+ self.prettier = Some(new_task.clone());
194+ Task::ready(Ok(new_task))
195+ }
196+ None => {
197+ self.attempt += 1;
198+ let node = Arc::clone(node);
199+ cx.spawn(|project, mut cx| async move {
200+ project
201+ .update(&mut cx, |_, cx| {
202+ start_default_prettier(node, worktree_id, cx)
203+ })
204+ .await
205+ })
206+ }
207+ },
208+ })
209+ }
210+}
211+
212+fn start_default_prettier(
213+ node: Arc<dyn NodeRuntime>,
214+ worktree_id: Option<WorktreeId>,
215+ cx: &mut ModelContext<'_, Project>,
216+) -> Task<anyhow::Result<PrettierTask>> {
217+ cx.spawn(|project, mut cx| async move {
218+ loop {
219+ let installation_task = project.update(&mut cx, |project, _| {
220+ match &project.default_prettier.prettier {
221+ PrettierInstallation::NotInstalled {
222+ installation_task, ..
223+ } => ControlFlow::Continue(installation_task.clone()),
224+ PrettierInstallation::Installed(default_prettier) => {
225+ ControlFlow::Break(default_prettier.clone())
226+ }
227+ }
228+ });
229+ match installation_task {
230+ ControlFlow::Continue(None) => {
231+ anyhow::bail!("Default prettier is not installed and cannot be started")
232+ }
233+ ControlFlow::Continue(Some(installation_task)) => {
234+ log::info!("Waiting for default prettier to install");
235+ if let Err(e) = installation_task.await {
236+ project.update(&mut cx, |project, _| {
237+ if let PrettierInstallation::NotInstalled {
238+ installation_task,
239+ attempts,
240+ ..
241+ } = &mut project.default_prettier.prettier
242+ {
243+ *installation_task = None;
244+ *attempts += 1;
245+ }
246+ });
247+ anyhow::bail!(
248+ "Cannot start default prettier due to its installation failure: {e:#}"
249+ );
250+ }
251+ let new_default_prettier = project.update(&mut cx, |project, cx| {
252+ let new_default_prettier =
253+ start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
254+ project.default_prettier.prettier =
255+ PrettierInstallation::Installed(PrettierInstance {
256+ attempt: 0,
257+ prettier: Some(new_default_prettier.clone()),
258+ });
259+ new_default_prettier
260+ });
261+ return Ok(new_default_prettier);
262+ }
263+ ControlFlow::Break(instance) => match instance.prettier {
264+ Some(instance) => return Ok(instance),
265+ None => {
266+ let new_default_prettier = project.update(&mut cx, |project, cx| {
267+ let new_default_prettier =
268+ start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
269+ project.default_prettier.prettier =
270+ PrettierInstallation::Installed(PrettierInstance {
271+ attempt: instance.attempt + 1,
272+ prettier: Some(new_default_prettier.clone()),
273+ });
274+ new_default_prettier
275+ });
276+ return Ok(new_default_prettier);
277+ }
278+ },
279+ }
280+ }
281+ })
282+}
283+
284+fn start_prettier(
285+ node: Arc<dyn NodeRuntime>,
286+ prettier_dir: PathBuf,
287+ worktree_id: Option<WorktreeId>,
288+ cx: &mut ModelContext<'_, Project>,
289+) -> PrettierTask {
290+ cx.spawn(|project, mut cx| async move {
291+ log::info!("Starting prettier at path {prettier_dir:?}");
292+ let new_server_id = project.update(&mut cx, |project, _| {
293+ project.languages.next_language_server_id()
294+ });
295+
296+ let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
297+ .await
298+ .context("default prettier spawn")
299+ .map(Arc::new)
300+ .map_err(Arc::new)?;
301+ register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
302+ Ok(new_prettier)
303+ })
304+ .shared()
305+}
306+
307+fn register_new_prettier(
308+ project: &ModelHandle<Project>,
309+ prettier: &Prettier,
310+ worktree_id: Option<WorktreeId>,
311+ new_server_id: LanguageServerId,
312+ cx: &mut AsyncAppContext,
313+) {
314+ let prettier_dir = prettier.prettier_dir();
315+ let is_default = prettier.is_default();
316+ if is_default {
317+ log::info!("Started default prettier in {prettier_dir:?}");
318+ } else {
319+ log::info!("Started prettier in {prettier_dir:?}");
320+ }
321+ if let Some(prettier_server) = prettier.server() {
322+ project.update(cx, |project, cx| {
323+ let name = if is_default {
324+ LanguageServerName(Arc::from("prettier (default)"))
325+ } else {
326+ let worktree_path = worktree_id
327+ .and_then(|id| project.worktree_for_id(id, cx))
328+ .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
329+ let name = match worktree_path {
330+ Some(worktree_path) => {
331+ if prettier_dir == worktree_path.as_ref() {
332+ let name = prettier_dir
333+ .file_name()
334+ .and_then(|name| name.to_str())
335+ .unwrap_or_default();
336+ format!("prettier ({name})")
337+ } else {
338+ let dir_to_display = prettier_dir
339+ .strip_prefix(worktree_path.as_ref())
340+ .ok()
341+ .unwrap_or(prettier_dir);
342+ format!("prettier ({})", dir_to_display.display())
343+ }
344+ }
345+ None => format!("prettier ({})", prettier_dir.display()),
346+ };
347+ LanguageServerName(Arc::from(name))
348+ };
349+ project
350+ .supplementary_language_servers
351+ .insert(new_server_id, (name, Arc::clone(prettier_server)));
352+ cx.emit(Event::LanguageServerAdded(new_server_id));
353+ });
354+ }
355+}
356+
357+async fn install_prettier_packages(
358+ plugins_to_install: HashSet<&'static str>,
359+ node: Arc<dyn NodeRuntime>,
360+) -> anyhow::Result<()> {
361+ let packages_to_versions =
362+ future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
363+ |package_name| async {
364+ let returned_package_name = package_name.to_string();
365+ let latest_version = node
366+ .npm_package_latest_version(package_name)
367+ .await
368+ .with_context(|| {
369+ format!("fetching latest npm version for package {returned_package_name}")
370+ })?;
371+ anyhow::Ok((returned_package_name, latest_version))
372+ },
373+ ))
374+ .await
375+ .context("fetching latest npm versions")?;
376+
377+ log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
378+ let borrowed_packages = packages_to_versions
379+ .iter()
380+ .map(|(package, version)| (package.as_str(), version.as_str()))
381+ .collect::<Vec<_>>();
382+ node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
383+ .await
384+ .context("fetching formatter packages")?;
385+ anyhow::Ok(())
386+}
387+
388+async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
389+ let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
390+ fs.save(
391+ &prettier_wrapper_path,
392+ &text::Rope::from(prettier::PRETTIER_SERVER_JS),
393+ text::LineEnding::Unix,
394+ )
395+ .await
396+ .with_context(|| {
397+ format!(
398+ "writing {} file at {prettier_wrapper_path:?}",
399+ prettier::PRETTIER_SERVER_FILE
400+ )
401+ })?;
402+ Ok(())
403+}
404+
405+impl Project {
406+ pub fn update_prettier_settings(
407+ &self,
408+ worktree: &ModelHandle<Worktree>,
409+ changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
410+ cx: &mut ModelContext<'_, Project>,
411+ ) {
412+ let prettier_config_files = Prettier::CONFIG_FILE_NAMES
413+ .iter()
414+ .map(Path::new)
415+ .collect::<HashSet<_>>();
416+
417+ let prettier_config_file_changed = changes
418+ .iter()
419+ .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
420+ .filter(|(path, _, _)| {
421+ !path
422+ .components()
423+ .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
424+ })
425+ .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
426+ let current_worktree_id = worktree.read(cx).id();
427+ if let Some((config_path, _, _)) = prettier_config_file_changed {
428+ log::info!(
429+ "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
430+ );
431+ let prettiers_to_reload =
432+ self.prettiers_per_worktree
433+ .get(&current_worktree_id)
434+ .iter()
435+ .flat_map(|prettier_paths| prettier_paths.iter())
436+ .flatten()
437+ .filter_map(|prettier_path| {
438+ Some((
439+ current_worktree_id,
440+ Some(prettier_path.clone()),
441+ self.prettier_instances.get(prettier_path)?.clone(),
442+ ))
443+ })
444+ .chain(self.default_prettier.instance().map(|default_prettier| {
445+ (current_worktree_id, None, default_prettier.clone())
446+ }))
447+ .collect::<Vec<_>>();
448+
449+ cx.background()
450+ .spawn(async move {
451+ let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
452+ async move {
453+ if let Some(instance) = prettier_instance.prettier {
454+ match instance.await {
455+ Ok(prettier) => {
456+ prettier.clear_cache().log_err().await;
457+ },
458+ Err(e) => {
459+ match prettier_path {
460+ Some(prettier_path) => log::error!(
461+ "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
462+ ),
463+ None => log::error!(
464+ "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
465+ ),
466+ }
467+ },
468+ }
469+ }
470+ }
471+ }))
472+ .await;
473+ })
474+ .detach();
475+ }
476+ }
477+
478+ fn prettier_instance_for_buffer(
479+ &mut self,
480+ buffer: &ModelHandle<Buffer>,
481+ cx: &mut ModelContext<Self>,
482+ ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
483+ let buffer = buffer.read(cx);
484+ let buffer_file = buffer.file();
485+ let Some(buffer_language) = buffer.language() else {
486+ return Task::ready(None);
487+ };
488+ if buffer_language.prettier_parser_name().is_none() {
489+ return Task::ready(None);
490+ }
491+
492+ if self.is_local() {
493+ let Some(node) = self.node.as_ref().map(Arc::clone) else {
494+ return Task::ready(None);
495+ };
496+ match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
497+ {
498+ Some((worktree_id, buffer_path)) => {
499+ let fs = Arc::clone(&self.fs);
500+ let installed_prettiers = self.prettier_instances.keys().cloned().collect();
501+ return cx.spawn(|project, mut cx| async move {
502+ match cx
503+ .background()
504+ .spawn(async move {
505+ Prettier::locate_prettier_installation(
506+ fs.as_ref(),
507+ &installed_prettiers,
508+ &buffer_path,
509+ )
510+ .await
511+ })
512+ .await
513+ {
514+ Ok(ControlFlow::Break(())) => {
515+ return None;
516+ }
517+ Ok(ControlFlow::Continue(None)) => {
518+ let default_instance = project.update(&mut cx, |project, cx| {
519+ project
520+ .prettiers_per_worktree
521+ .entry(worktree_id)
522+ .or_default()
523+ .insert(None);
524+ project.default_prettier.prettier_task(
525+ &node,
526+ Some(worktree_id),
527+ cx,
528+ )
529+ });
530+ Some((None, default_instance?.log_err().await?))
531+ }
532+ Ok(ControlFlow::Continue(Some(prettier_dir))) => {
533+ project.update(&mut cx, |project, _| {
534+ project
535+ .prettiers_per_worktree
536+ .entry(worktree_id)
537+ .or_default()
538+ .insert(Some(prettier_dir.clone()))
539+ });
540+ if let Some(prettier_task) =
541+ project.update(&mut cx, |project, cx| {
542+ project.prettier_instances.get_mut(&prettier_dir).map(
543+ |existing_instance| {
544+ existing_instance.prettier_task(
545+ &node,
546+ Some(&prettier_dir),
547+ Some(worktree_id),
548+ cx,
549+ )
550+ },
551+ )
552+ })
553+ {
554+ log::debug!(
555+ "Found already started prettier in {prettier_dir:?}"
556+ );
557+ return Some((
558+ Some(prettier_dir),
559+ prettier_task?.await.log_err()?,
560+ ));
561+ }
562+
563+ log::info!("Found prettier in {prettier_dir:?}, starting.");
564+ let new_prettier_task = project.update(&mut cx, |project, cx| {
565+ let new_prettier_task = start_prettier(
566+ node,
567+ prettier_dir.clone(),
568+ Some(worktree_id),
569+ cx,
570+ );
571+ project.prettier_instances.insert(
572+ prettier_dir.clone(),
573+ PrettierInstance {
574+ attempt: 0,
575+ prettier: Some(new_prettier_task.clone()),
576+ },
577+ );
578+ new_prettier_task
579+ });
580+ Some((Some(prettier_dir), new_prettier_task))
581+ }
582+ Err(e) => {
583+ log::error!("Failed to determine prettier path for buffer: {e:#}");
584+ return None;
585+ }
586+ }
587+ });
588+ }
589+ None => {
590+ let new_task = self.default_prettier.prettier_task(&node, None, cx);
591+ return cx
592+ .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) });
593+ }
594+ }
595+ } else {
596+ return Task::ready(None);
597+ }
598+ }
599+
600+ #[cfg(any(test, feature = "test-support"))]
601+ pub fn install_default_prettier(
602+ &mut self,
603+ _worktree: Option<WorktreeId>,
604+ plugins: HashSet<&'static str>,
605+ _cx: &mut ModelContext<Self>,
606+ ) {
607+ // suppress unused code warnings
608+ let _ = install_prettier_packages;
609+ let _ = save_prettier_server_file;
610+
611+ self.default_prettier.installed_plugins.extend(plugins);
612+ self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
613+ attempt: 0,
614+ prettier: None,
615+ });
616+ }
617+
618+ #[cfg(not(any(test, feature = "test-support")))]
619+ pub fn install_default_prettier(
620+ &mut self,
621+ worktree: Option<WorktreeId>,
622+ mut new_plugins: HashSet<&'static str>,
623+ cx: &mut ModelContext<Self>,
624+ ) {
625+ let Some(node) = self.node.as_ref().cloned() else {
626+ return;
627+ };
628+ log::info!("Initializing default prettier with plugins {new_plugins:?}");
629+ let fs = Arc::clone(&self.fs);
630+ let locate_prettier_installation = match worktree.and_then(|worktree_id| {
631+ self.worktree_for_id(worktree_id, cx)
632+ .map(|worktree| worktree.read(cx).abs_path())
633+ }) {
634+ Some(locate_from) => {
635+ let installed_prettiers = self.prettier_instances.keys().cloned().collect();
636+ cx.background().spawn(async move {
637+ Prettier::locate_prettier_installation(
638+ fs.as_ref(),
639+ &installed_prettiers,
640+ locate_from.as_ref(),
641+ )
642+ .await
643+ })
644+ }
645+ None => Task::ready(Ok(ControlFlow::Continue(None))),
646+ };
647+ new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
648+ let mut installation_attempt = 0;
649+ let previous_installation_task = match &mut self.default_prettier.prettier {
650+ PrettierInstallation::NotInstalled {
651+ installation_task,
652+ attempts,
653+ not_installed_plugins,
654+ } => {
655+ installation_attempt = *attempts;
656+ if installation_attempt > prettier::FAIL_THRESHOLD {
657+ *installation_task = None;
658+ log::warn!(
659+ "Default prettier installation had failed {installation_attempt} times, not attempting again",
660+ );
661+ return;
662+ }
663+ new_plugins.extend(not_installed_plugins.iter());
664+ installation_task.clone()
665+ }
666+ PrettierInstallation::Installed { .. } => {
667+ if new_plugins.is_empty() {
668+ return;
669+ }
670+ None
671+ }
672+ };
673+
674+ let plugins_to_install = new_plugins.clone();
675+ let fs = Arc::clone(&self.fs);
676+ let new_installation_task = cx
677+ .spawn(|project, mut cx| async move {
678+ match locate_prettier_installation
679+ .await
680+ .context("locate prettier installation")
681+ .map_err(Arc::new)?
682+ {
683+ ControlFlow::Break(()) => return Ok(()),
684+ ControlFlow::Continue(prettier_path) => {
685+ if prettier_path.is_some() {
686+ new_plugins.clear();
687+ }
688+ let mut needs_install = false;
689+ if let Some(previous_installation_task) = previous_installation_task {
690+ if let Err(e) = previous_installation_task.await {
691+ log::error!("Failed to install default prettier: {e:#}");
692+ project.update(&mut cx, |project, _| {
693+ if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
694+ *attempts += 1;
695+ new_plugins.extend(not_installed_plugins.iter());
696+ installation_attempt = *attempts;
697+ needs_install = true;
698+ };
699+ });
700+ }
701+ };
702+ if installation_attempt > prettier::FAIL_THRESHOLD {
703+ project.update(&mut cx, |project, _| {
704+ if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
705+ *installation_task = None;
706+ };
707+ });
708+ log::warn!(
709+ "Default prettier installation had failed {installation_attempt} times, not attempting again",
710+ );
711+ return Ok(());
712+ }
713+ project.update(&mut cx, |project, _| {
714+ new_plugins.retain(|plugin| {
715+ !project.default_prettier.installed_plugins.contains(plugin)
716+ });
717+ if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
718+ not_installed_plugins.retain(|plugin| {
719+ !project.default_prettier.installed_plugins.contains(plugin)
720+ });
721+ not_installed_plugins.extend(new_plugins.iter());
722+ }
723+ needs_install |= !new_plugins.is_empty();
724+ });
725+ if needs_install {
726+ let installed_plugins = new_plugins.clone();
727+ cx.background()
728+ .spawn(async move {
729+ save_prettier_server_file(fs.as_ref()).await?;
730+ install_prettier_packages(new_plugins, node).await
731+ })
732+ .await
733+ .context("prettier & plugins install")
734+ .map_err(Arc::new)?;
735+ log::info!("Initialized prettier with plugins: {installed_plugins:?}");
736+ project.update(&mut cx, |project, _| {
737+ project.default_prettier.prettier =
738+ PrettierInstallation::Installed(PrettierInstance {
739+ attempt: 0,
740+ prettier: None,
741+ });
742+ project.default_prettier
743+ .installed_plugins
744+ .extend(installed_plugins);
745+ });
746+ }
747+ }
748+ }
749+ Ok(())
750+ })
751+ .shared();
752+ self.default_prettier.prettier = PrettierInstallation::NotInstalled {
753+ attempts: installation_attempt,
754+ installation_task: Some(new_installation_task),
755+ not_installed_plugins: plugins_to_install,
756+ };
757+ }
758+}
crates/project/src/project.rs+49−637
@@ -1,5 +1,6 @@
11 mod ignore;
22 mod lsp_command;
3+mod prettier_support;
34 pub mod project_settings;
45 pub mod search;
56 pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{ · 14 unmodified lines
2021 mpsc::{self, UnboundedReceiver},
2122 oneshot,
2223 },
23− future::{self, try_join_all, Shared},
24+ future::{try_join_all, Shared},
2425 stream::FuturesUnordered,
2526 AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
2627 };
@@ -31,9 +32,7 @@ use gpui::{ · 4 unmodified lines
3132 };
3233 use itertools::Itertools;
3334 use language::{
34− language_settings::{
35− language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
36− },
35+ language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
3736 point_to_lsp,
3837 proto::{
3938 deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*; · 14 unmodified lines
5453 use node_runtime::NodeRuntime;
5554 use parking_lot::Mutex;
5655 use postage::watch;
57−use prettier::Prettier;
56+use prettier_support::{DefaultPrettier, PrettierInstance};
5857 use project_settings::{LspSettings, ProjectSettings};
5958 use rand::prelude::*;
6059 use search::SearchQuery;
@@ -72,7 +71,7 @@ use std::{ · 11 unmodified lines
7271 hash::Hash,
7372 mem,
7473 num::NonZeroU32,
75− ops::{ControlFlow, Range},
74+ ops::Range,
7675 path::{self, Component, Path, PathBuf},
7776 process::Stdio,
7877 str,
@@ -85,11 +84,8 @@ use std::{ · 6 unmodified lines
8584 use terminals::Terminals;
8685 use text::Anchor;
8786 use util::{
88− debug_panic, defer,
89− http::HttpClient,
90− merge_json_value_into,
91− paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
92− post_inc, ResultExt, TryFutureExt as _,
87+ debug_panic, defer, http::HttpClient, merge_json_value_into,
88+ paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
9389 };
9490
9591 pub use fs::*;
@@ -168,16 +164,9 @@ pub struct Project { · 72 unmodified lines
168164 copilot_log_subscription: Option<lsp::Subscription>,
169165 current_lsp_settings: HashMap<Arc<str>, LspSettings>,
170166 node: Option<Arc<dyn NodeRuntime>>,
171− default_prettier: Option<DefaultPrettier>,
167+ default_prettier: DefaultPrettier,
172168 prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
173− prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
174−}
175−
176−struct DefaultPrettier {
177− instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
178− installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
179− #[cfg(not(any(test, feature = "test-support")))]
180− installed_plugins: HashSet<&'static str>,
169+ prettier_instances: HashMap<PathBuf, PrettierInstance>,
181170 }
182171
183172 struct DelayedDebounced {
@@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate { · 358 unmodified lines
542531 http_client: Arc<dyn HttpClient>,
543532 }
544533
534+// Currently, formatting operations are represented differently depending on
535+// whether they come from a language server or an external command.
536+enum FormatOperation {
537+ Lsp(Vec<(Range<Anchor>, String)>),
538+ External(Diff),
539+ Prettier(Diff),
540+}
541+
545542 impl FormatTrigger {
546543 fn from_proto(value: i32) -> FormatTrigger {
547544 match value {
@@ -690,7 +687,7 @@ impl Project { · 142 unmodified lines
690687 copilot_log_subscription: None,
691688 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
692689 node: Some(node),
693− default_prettier: None,
690+ default_prettier: DefaultPrettier::default(),
694691 prettiers_per_worktree: HashMap::default(),
695692 prettier_instances: HashMap::default(),
696693 }
@@ -791,7 +788,7 @@ impl Project { · 94 unmodified lines
791788 copilot_log_subscription: None,
792789 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
793790 node: None,
794− default_prettier: None,
791+ default_prettier: DefaultPrettier::default(),
795792 prettiers_per_worktree: HashMap::default(),
796793 prettier_instances: HashMap::default(),
797794 };
@@ -928,8 +925,19 @@ impl Project { · 130 unmodified lines
928925 .detach();
929926 }
930927
928+ let mut prettier_plugins_by_worktree = HashMap::default();
931929 for (worktree, language, settings) in language_formatters_to_check {
932− self.install_default_formatters(worktree, &language, &settings, cx);
930+ if let Some(plugins) =
931+ prettier_support::prettier_plugins_for_language(&language, &settings)
932+ {
933+ prettier_plugins_by_worktree
934+ .entry(worktree)
935+ .or_insert_with(|| HashSet::default())
936+ .extend(plugins);
937+ }
938+ }
939+ for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
940+ self.install_default_prettier(worktree, prettier_plugins, cx);
933941 }
934942
935943 // Start all the newly-enabled language servers.
@@ -2685,8 +2693,11 @@ impl Project { · 1749 unmodified lines
26852693 let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
26862694 let buffer_file = File::from_dyn(buffer_file.as_ref());
26872695 let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
2688−
2689− self.install_default_formatters(worktree, &new_language, &settings, cx);
2696+ if let Some(prettier_plugins) =
2697+ prettier_support::prettier_plugins_for_language(&new_language, &settings)
2698+ {
2699+ self.install_default_prettier(worktree, prettier_plugins, cx);
2700+ };
26902701 if let Some(file) = buffer_file {
26912702 let worktree = file.worktree.clone();
26922703 if let Some(tree) = worktree.read(cx).as_local() {
@@ -4073,8 +4084,6 @@ impl Project { · 1380 unmodified lines
40734084
40744085 let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
40754086 let ensure_final_newline = settings.ensure_final_newline_on_save;
4076− let format_on_save = settings.format_on_save.clone();
4077− let formatter = settings.formatter.clone();
40784087 let tab_size = settings.tab_size;
40794088
40804089 // First, format buffer's whitespace according to the settings.
@@ -4099,18 +4108,10 @@ impl Project { · 18 unmodified lines
40994108 buffer.end_transaction(cx)
41004109 });
41014110
4102− // Currently, formatting operations are represented differently depending on
4103− // whether they come from a language server or an external command.
4104− enum FormatOperation {
4105− Lsp(Vec<(Range<Anchor>, String)>),
4106− External(Diff),
4107− Prettier(Diff),
4108− }
4109−
41104111 // Apply language-specific formatting using either a language server
41114112 // or external command.
41124113 let mut format_operation = None;
4113− match (formatter, format_on_save) {
4114+ match (&settings.formatter, &settings.format_on_save) {
41144115 (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
41154116
41164117 (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4155,46 +4156,11 @@ impl Project { · 38 unmodified lines
41554156 }
41564157 }
41574158 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
4158− if let Some((prettier_path, prettier_task)) = project
4159− .update(&mut cx, |project, cx| {
4160− project.prettier_instance_for_buffer(buffer, cx)
4161− }).await {
4162− match prettier_task.await
4163− {
4164− Ok(prettier) => {
4165− let buffer_path = buffer.update(&mut cx, |buffer, cx| {
4166− File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
4167− });
4168− format_operation = Some(FormatOperation::Prettier(
4169− prettier
4170− .format(buffer, buffer_path, &cx)
4171− .await
4172− .context("formatting via prettier")?,
4173− ));
4174− }
4175− Err(e) => {
4176− project.update(&mut cx, |project, _| {
4177− match &prettier_path {
4178− Some(prettier_path) => {
4179− project.prettier_instances.remove(prettier_path);
4180− },
4181− None => {
4182− if let Some(default_prettier) = project.default_prettier.as_mut() {
4183− default_prettier.instance = None;
4184− }
4185− },
4186− }
4187− });
4188− match &prettier_path {
4189− Some(prettier_path) => {
4190− log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
4191− },
4192− None => {
4193− log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
4194− },
4195− }
4196− }
4197− }
4159+ if let Some(new_operation) =
4160+ prettier_support::format_with_prettier(&project, buffer, &mut cx)
4161+ .await
4162+ {
4163+ format_operation = Some(new_operation);
41984164 } else if let Some((language_server, buffer_abs_path)) =
41994165 language_server.as_ref().zip(buffer_abs_path.as_ref())
42004166 {
@@ -4212,48 +4178,13 @@ impl Project { · 11 unmodified lines
42124178 ));
42134179 }
42144180 }
4215− (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
4216− if let Some((prettier_path, prettier_task)) = project
4217− .update(&mut cx, |project, cx| {
4218− project.prettier_instance_for_buffer(buffer, cx)
4219− }).await {
4220− match prettier_task.await
4221− {
4222− Ok(prettier) => {
4223− let buffer_path = buffer.update(&mut cx, |buffer, cx| {
4224− File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
4225− });
4226− format_operation = Some(FormatOperation::Prettier(
4227− prettier
4228− .format(buffer, buffer_path, &cx)
4229− .await
4230− .context("formatting via prettier")?,
4231− ));
4232− }
4233− Err(e) => {
4234− project.update(&mut cx, |project, _| {
4235− match &prettier_path {
4236− Some(prettier_path) => {
4237− project.prettier_instances.remove(prettier_path);
4238− },
4239− None => {
4240− if let Some(default_prettier) = project.default_prettier.as_mut() {
4241− default_prettier.instance = None;
4242− }
4243− },
4244− }
4245− });
4246− match &prettier_path {
4247− Some(prettier_path) => {
4248− log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
4249− },
4250− None => {
4251− log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
4252− },
4253− }
4254− }
4255− }
4256− }
4181+ (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
4182+ if let Some(new_operation) =
4183+ prettier_support::format_with_prettier(&project, buffer, &mut cx)
4184+ .await
4185+ {
4186+ format_operation = Some(new_operation);
4187+ }
42574188 }
42584189 };
42594190
@@ -6566,85 +6497,6 @@ impl Project { · 2306 unmodified lines
65666497 .detach();
65676498 }
65686499
6569− fn update_prettier_settings(
6570− &self,
6571− worktree: &ModelHandle<Worktree>,
6572− changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
6573− cx: &mut ModelContext<'_, Project>,
6574− ) {
6575− let prettier_config_files = Prettier::CONFIG_FILE_NAMES
6576− .iter()
6577− .map(Path::new)
6578− .collect::<HashSet<_>>();
6579−
6580− let prettier_config_file_changed = changes
6581− .iter()
6582− .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
6583− .filter(|(path, _, _)| {
6584− !path
6585− .components()
6586− .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
6587− })
6588− .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
6589− let current_worktree_id = worktree.read(cx).id();
6590− if let Some((config_path, _, _)) = prettier_config_file_changed {
6591− log::info!(
6592− "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
6593− );
6594− let prettiers_to_reload = self
6595− .prettiers_per_worktree
6596− .get(&current_worktree_id)
6597− .iter()
6598− .flat_map(|prettier_paths| prettier_paths.iter())
6599− .flatten()
6600− .filter_map(|prettier_path| {
6601− Some((
6602− current_worktree_id,
6603− Some(prettier_path.clone()),
6604− self.prettier_instances.get(prettier_path)?.clone(),
6605− ))
6606− })
6607− .chain(self.default_prettier.iter().filter_map(|default_prettier| {
6608− Some((
6609− current_worktree_id,
6610− None,
6611− default_prettier.instance.clone()?,
6612− ))
6613− }))
6614− .collect::<Vec<_>>();
6615−
6616− cx.background()
6617− .spawn(async move {
6618− for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
6619− async move {
6620− prettier_task.await?
6621− .clear_cache()
6622− .await
6623− .with_context(|| {
6624− match prettier_path {
6625− Some(prettier_path) => format!(
6626− "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
6627− ),
6628− None => format!(
6629− "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
6630− ),
6631− }
6632−
6633− })
6634− .map_err(Arc::new)
6635− }
6636− }))
6637− .await
6638− {
6639− if let Err(e) = task_result {
6640− log::error!("Failed to clear cache for prettier: {e:#}");
6641− }
6642− }
6643− })
6644− .detach();
6645− }
6646− }
6647−
66486500 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
66496501 let new_active_entry = entry.and_then(|project_path| {
66506502 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -8536,446 +8388,6 @@ impl Project { · 1885 unmodified lines
85368388 Vec::new()
85378389 }
85388390 }
8539−
8540− fn prettier_instance_for_buffer(
8541− &mut self,
8542− buffer: &ModelHandle<Buffer>,
8543− cx: &mut ModelContext<Self>,
8544− ) -> Task<
8545− Option<(
8546− Option<PathBuf>,
8547− Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
8548− )>,
8549− > {
8550− let buffer = buffer.read(cx);
8551− let buffer_file = buffer.file();
8552− let Some(buffer_language) = buffer.language() else {
8553− return Task::ready(None);
8554− };
8555− if buffer_language.prettier_parser_name().is_none() {
8556− return Task::ready(None);
8557− }
8558−
8559− if self.is_local() {
8560− let Some(node) = self.node.as_ref().map(Arc::clone) else {
8561− return Task::ready(None);
8562− };
8563− match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
8564− {
8565− Some((worktree_id, buffer_path)) => {
8566− let fs = Arc::clone(&self.fs);
8567− let installed_prettiers = self.prettier_instances.keys().cloned().collect();
8568− return cx.spawn(|project, mut cx| async move {
8569− match cx
8570− .background()
8571− .spawn(async move {
8572− Prettier::locate_prettier_installation(
8573− fs.as_ref(),
8574− &installed_prettiers,
8575− &buffer_path,
8576− )
8577− .await
8578− })
8579− .await
8580− {
8581− Ok(ControlFlow::Break(())) => {
8582− return None;
8583− }
8584− Ok(ControlFlow::Continue(None)) => {
8585− let started_default_prettier =
8586− project.update(&mut cx, |project, _| {
8587− project
8588− .prettiers_per_worktree
8589− .entry(worktree_id)
8590− .or_default()
8591− .insert(None);
8592− project.default_prettier.as_ref().and_then(
8593− |default_prettier| default_prettier.instance.clone(),
8594− )
8595− });
8596− match started_default_prettier {
8597− Some(old_task) => return Some((None, old_task)),
8598− None => {
8599− let new_default_prettier = project
8600− .update(&mut cx, |_, cx| {
8601− start_default_prettier(node, Some(worktree_id), cx)
8602− })
8603− .await;
8604− return Some((None, new_default_prettier));
8605− }
8606− }
8607− }
8608− Ok(ControlFlow::Continue(Some(prettier_dir))) => {
8609− project.update(&mut cx, |project, _| {
8610− project
8611− .prettiers_per_worktree
8612− .entry(worktree_id)
8613− .or_default()
8614− .insert(Some(prettier_dir.clone()))
8615− });
8616− if let Some(existing_prettier) =
8617− project.update(&mut cx, |project, _| {
8618− project.prettier_instances.get(&prettier_dir).cloned()
8619− })
8620− {
8621− log::debug!(
8622− "Found already started prettier in {prettier_dir:?}"
8623− );
8624− return Some((Some(prettier_dir), existing_prettier));
8625− }
8626−
8627− log::info!("Found prettier in {prettier_dir:?}, starting.");
8628− let new_prettier_task = project.update(&mut cx, |project, cx| {
8629− let new_prettier_task = start_prettier(
8630− node,
8631− prettier_dir.clone(),
8632− Some(worktree_id),
8633− cx,
8634− );
8635− project
8636− .prettier_instances
8637− .insert(prettier_dir.clone(), new_prettier_task.clone());
8638− new_prettier_task
8639− });
8640− Some((Some(prettier_dir), new_prettier_task))
8641− }
8642− Err(e) => {
8643− return Some((
8644− None,
8645− Task::ready(Err(Arc::new(
8646− e.context("determining prettier path"),
8647− )))
8648− .shared(),
8649− ));
8650− }
8651− }
8652− });
8653− }
8654− None => {
8655− let started_default_prettier = self
8656− .default_prettier
8657− .as_ref()
8658− .and_then(|default_prettier| default_prettier.instance.clone());
8659− match started_default_prettier {
8660− Some(old_task) => return Task::ready(Some((None, old_task))),
8661− None => {
8662− let new_task = start_default_prettier(node, None, cx);
8663− return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
8664− }
8665− }
8666− }
8667− }
8668− } else if self.remote_id().is_some() {
8669− return Task::ready(None);
8670− } else {
8671− Task::ready(Some((
8672− None,
8673− Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
8674− )))
8675− }
8676− }
8677−
8678− #[cfg(any(test, feature = "test-support"))]
8679− fn install_default_formatters(
8680− &mut self,
8681− _worktree: Option<WorktreeId>,
8682− _new_language: &Language,
8683− _language_settings: &LanguageSettings,
8684− _cx: &mut ModelContext<Self>,
8685− ) {
8686− }
8687−
8688− #[cfg(not(any(test, feature = "test-support")))]
8689− fn install_default_formatters(
8690− &mut self,
8691− worktree: Option<WorktreeId>,
8692− new_language: &Language,
8693− language_settings: &LanguageSettings,
8694− cx: &mut ModelContext<Self>,
8695− ) {
8696− match &language_settings.formatter {
8697− Formatter::Prettier { .. } | Formatter::Auto => {}
8698− Formatter::LanguageServer | Formatter::External { .. } => return,
8699− };
8700− let Some(node) = self.node.as_ref().cloned() else {
8701− return;
8702− };
8703−
8704− let mut prettier_plugins = None;
8705− if new_language.prettier_parser_name().is_some() {
8706− prettier_plugins
8707− .get_or_insert_with(|| HashSet::<&'static str>::default())
8708− .extend(
8709− new_language
8710− .lsp_adapters()
8711− .iter()
8712− .flat_map(|adapter| adapter.prettier_plugins()),
8713− )
8714− }
8715− let Some(prettier_plugins) = prettier_plugins else {
8716− return;
8717− };
8718−
8719− let fs = Arc::clone(&self.fs);
8720− let locate_prettier_installation = match worktree.and_then(|worktree_id| {
8721− self.worktree_for_id(worktree_id, cx)
8722− .map(|worktree| worktree.read(cx).abs_path())
8723− }) {
8724− Some(locate_from) => {
8725− let installed_prettiers = self.prettier_instances.keys().cloned().collect();
8726− cx.background().spawn(async move {
8727− Prettier::locate_prettier_installation(
8728− fs.as_ref(),
8729− &installed_prettiers,
8730− locate_from.as_ref(),
8731− )
8732− .await
8733− })
8734− }
8735− None => Task::ready(Ok(ControlFlow::Break(()))),
8736− };
8737− let mut plugins_to_install = prettier_plugins;
8738− let previous_installation_process =
8739− if let Some(default_prettier) = &mut self.default_prettier {
8740− plugins_to_install
8741− .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
8742− if plugins_to_install.is_empty() {
8743− return;
8744− }
8745− default_prettier.installation_process.clone()
8746− } else {
8747− None
8748− };
8749− let fs = Arc::clone(&self.fs);
8750− let default_prettier = self
8751− .default_prettier
8752− .get_or_insert_with(|| DefaultPrettier {
8753− instance: None,
8754− installation_process: None,
8755− installed_plugins: HashSet::default(),
8756− });
8757− default_prettier.installation_process = Some(
8758− cx.spawn(|this, mut cx| async move {
8759− match locate_prettier_installation
8760− .await
8761− .context("locate prettier installation")
8762− .map_err(Arc::new)?
8763− {
8764− ControlFlow::Break(()) => return Ok(()),
8765− ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
8766− ControlFlow::Continue(None) => {
8767− let mut needs_install = match previous_installation_process {
8768− Some(previous_installation_process) => {
8769− previous_installation_process.await.is_err()
8770− }
8771− None => true,
8772− };
8773− this.update(&mut cx, |this, _| {
8774− if let Some(default_prettier) = &mut this.default_prettier {
8775− plugins_to_install.retain(|plugin| {
8776− !default_prettier.installed_plugins.contains(plugin)
8777− });
8778− needs_install |= !plugins_to_install.is_empty();
8779− }
8780− });
8781− if needs_install {
8782− let installed_plugins = plugins_to_install.clone();
8783− cx.background()
8784− .spawn(async move {
8785− install_default_prettier(plugins_to_install, node, fs).await
8786− })
8787− .await
8788− .context("prettier & plugins install")
8789− .map_err(Arc::new)?;
8790− this.update(&mut cx, |this, _| {
8791− let default_prettier =
8792− this.default_prettier
8793− .get_or_insert_with(|| DefaultPrettier {
8794− instance: None,
8795− installation_process: Some(
8796− Task::ready(Ok(())).shared(),
8797− ),
8798− installed_plugins: HashSet::default(),
8799− });
8800− default_prettier.instance = None;
8801− default_prettier.installed_plugins.extend(installed_plugins);
8802− });
8803− }
8804− }
8805− }
8806− Ok(())
8807− })
8808− .shared(),
8809− );
8810− }
8811−}
8812−
8813−fn start_default_prettier(
8814− node: Arc<dyn NodeRuntime>,
8815− worktree_id: Option<WorktreeId>,
8816− cx: &mut ModelContext<'_, Project>,
8817−) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
8818− cx.spawn(|project, mut cx| async move {
8819− loop {
8820− let default_prettier_installing = project.update(&mut cx, |project, _| {
8821− project
8822− .default_prettier
8823− .as_ref()
8824− .and_then(|default_prettier| default_prettier.installation_process.clone())
8825− });
8826− match default_prettier_installing {
8827− Some(installation_task) => {
8828− if installation_task.await.is_ok() {
8829− break;
8830− }
8831− }
8832− None => break,
8833− }
8834− }
8835−
8836− project.update(&mut cx, |project, cx| {
8837− match project
8838− .default_prettier
8839− .as_mut()
8840− .and_then(|default_prettier| default_prettier.instance.as_mut())
8841− {
8842− Some(default_prettier) => default_prettier.clone(),
8843− None => {
8844− let new_default_prettier =
8845− start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
8846− project
8847− .default_prettier
8848− .get_or_insert_with(|| DefaultPrettier {
8849− instance: None,
8850− installation_process: None,
8851− #[cfg(not(any(test, feature = "test-support")))]
8852− installed_plugins: HashSet::default(),
8853− })
8854− .instance = Some(new_default_prettier.clone());
8855− new_default_prettier
8856− }
8857− }
8858− })
8859− })
8860−}
8861−
8862−fn start_prettier(
8863− node: Arc<dyn NodeRuntime>,
8864− prettier_dir: PathBuf,
8865− worktree_id: Option<WorktreeId>,
8866− cx: &mut ModelContext<'_, Project>,
8867−) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
8868− cx.spawn(|project, mut cx| async move {
8869− let new_server_id = project.update(&mut cx, |project, _| {
8870− project.languages.next_language_server_id()
8871− });
8872− let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
8873− .await
8874− .context("default prettier spawn")
8875− .map(Arc::new)
8876− .map_err(Arc::new)?;
8877− register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
8878− Ok(new_prettier)
8879− })
8880− .shared()
8881−}
8882−
8883−fn register_new_prettier(
8884− project: &ModelHandle<Project>,
8885− prettier: &Prettier,
8886− worktree_id: Option<WorktreeId>,
8887− new_server_id: LanguageServerId,
8888− cx: &mut AsyncAppContext,
8889−) {
8890− let prettier_dir = prettier.prettier_dir();
8891− let is_default = prettier.is_default();
8892− if is_default {
8893− log::info!("Started default prettier in {prettier_dir:?}");
8894− } else {
8895− log::info!("Started prettier in {prettier_dir:?}");
8896− }
8897− if let Some(prettier_server) = prettier.server() {
8898− project.update(cx, |project, cx| {
8899− let name = if is_default {
8900− LanguageServerName(Arc::from("prettier (default)"))
8901− } else {
8902− let worktree_path = worktree_id
8903− .and_then(|id| project.worktree_for_id(id, cx))
8904− .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
8905− let name = match worktree_path {
8906− Some(worktree_path) => {
8907− if prettier_dir == worktree_path.as_ref() {
8908− let name = prettier_dir
8909− .file_name()
8910− .and_then(|name| name.to_str())
8911− .unwrap_or_default();
8912− format!("prettier ({name})")
8913− } else {
8914− let dir_to_display = prettier_dir
8915− .strip_prefix(worktree_path.as_ref())
8916− .ok()
8917− .unwrap_or(prettier_dir);
8918− format!("prettier ({})", dir_to_display.display())
8919− }
8920− }
8921− None => format!("prettier ({})", prettier_dir.display()),
8922− };
8923− LanguageServerName(Arc::from(name))
8924− };
8925− project
8926− .supplementary_language_servers
8927− .insert(new_server_id, (name, Arc::clone(prettier_server)));
8928− cx.emit(Event::LanguageServerAdded(new_server_id));
8929− });
8930− }
8931−}
8932−
8933−#[cfg(not(any(test, feature = "test-support")))]
8934−async fn install_default_prettier(
8935− plugins_to_install: HashSet<&'static str>,
8936− node: Arc<dyn NodeRuntime>,
8937− fs: Arc<dyn Fs>,
8938−) -> anyhow::Result<()> {
8939− let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
8940− // method creates parent directory if it doesn't exist
8941− fs.save(
8942− &prettier_wrapper_path,
8943− &text::Rope::from(prettier::PRETTIER_SERVER_JS),
8944− text::LineEnding::Unix,
8945− )
8946− .await
8947− .with_context(|| {
8948− format!(
8949− "writing {} file at {prettier_wrapper_path:?}",
8950− prettier::PRETTIER_SERVER_FILE
8951− )
8952− })?;
8953−
8954− let packages_to_versions =
8955− future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
8956− |package_name| async {
8957− let returned_package_name = package_name.to_string();
8958− let latest_version = node
8959− .npm_package_latest_version(package_name)
8960− .await
8961− .with_context(|| {
8962− format!("fetching latest npm version for package {returned_package_name}")
8963− })?;
8964− anyhow::Ok((returned_package_name, latest_version))
8965− },
8966− ))
8967− .await
8968− .context("fetching latest npm versions")?;
8969−
8970− log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
8971− let borrowed_packages = packages_to_versions
8972− .iter()
8973− .map(|(package, version)| (package.as_str(), version.as_str()))
8974− .collect::<Vec<_>>();
8975− node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
8976− .await
8977− .context("fetching formatter packages")?;
8978− anyhow::Ok(())
89798391 }
89808392
89818393 fn subscribe_for_copilot_events(
crates/project2/src/prettier_support.rs+772−0
@@ -0,0 +1,772 @@
1+use std::{
2+ ops::ControlFlow,
3+ path::{Path, PathBuf},
4+ sync::Arc,
5+};
6+
7+use anyhow::Context;
8+use collections::HashSet;
9+use fs::Fs;
10+use futures::{
11+ future::{self, Shared},
12+ FutureExt,
13+};
14+use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
15+use language::{
16+ language_settings::{Formatter, LanguageSettings},
17+ Buffer, Language, LanguageServerName, LocalFile,
18+};
19+use lsp::LanguageServerId;
20+use node_runtime::NodeRuntime;
21+use prettier::Prettier;
22+use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
23+
24+use crate::{
25+ Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
26+};
27+
28+pub fn prettier_plugins_for_language(
29+ language: &Language,
30+ language_settings: &LanguageSettings,
31+) -> Option<HashSet<&'static str>> {
32+ match &language_settings.formatter {
33+ Formatter::Prettier { .. } | Formatter::Auto => {}
34+ Formatter::LanguageServer | Formatter::External { .. } => return None,
35+ };
36+ let mut prettier_plugins = None;
37+ if language.prettier_parser_name().is_some() {
38+ prettier_plugins
39+ .get_or_insert_with(|| HashSet::default())
40+ .extend(
41+ language
42+ .lsp_adapters()
43+ .iter()
44+ .flat_map(|adapter| adapter.prettier_plugins()),
45+ )
46+ }
47+
48+ prettier_plugins
49+}
50+
51+pub(super) async fn format_with_prettier(
52+ project: &WeakModel<Project>,
53+ buffer: &Model<Buffer>,
54+ cx: &mut AsyncAppContext,
55+) -> Option<FormatOperation> {
56+ if let Some((prettier_path, prettier_task)) = project
57+ .update(cx, |project, cx| {
58+ project.prettier_instance_for_buffer(buffer, cx)
59+ })
60+ .ok()?
61+ .await
62+ {
63+ match prettier_task.await {
64+ Ok(prettier) => {
65+ let buffer_path = buffer
66+ .update(cx, |buffer, cx| {
67+ File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
68+ })
69+ .ok()?;
70+ match prettier.format(buffer, buffer_path, cx).await {
71+ Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
72+ Err(e) => {
73+ log::error!(
74+ "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
75+ );
76+ }
77+ }
78+ }
79+ Err(e) => project
80+ .update(cx, |project, _| {
81+ let instance_to_update = match prettier_path {
82+ Some(prettier_path) => {
83+ log::error!(
84+ "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
85+ );
86+ project.prettier_instances.get_mut(&prettier_path)
87+ }
88+ None => {
89+ log::error!("Default prettier instance failed to spawn: {e:#}");
90+ match &mut project.default_prettier.prettier {
91+ PrettierInstallation::NotInstalled { .. } => None,
92+ PrettierInstallation::Installed(instance) => Some(instance),
93+ }
94+ }
95+ };
96+
97+ if let Some(instance) = instance_to_update {
98+ instance.attempt += 1;
99+ instance.prettier = None;
100+ }
101+ })
102+ .ok()?,
103+ }
104+ }
105+
106+ None
107+}
108+
109+pub struct DefaultPrettier {
110+ prettier: PrettierInstallation,
111+ installed_plugins: HashSet<&'static str>,
112+}
113+
114+pub enum PrettierInstallation {
115+ NotInstalled {
116+ attempts: usize,
117+ installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
118+ not_installed_plugins: HashSet<&'static str>,
119+ },
120+ Installed(PrettierInstance),
121+}
122+
123+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
124+
125+#[derive(Clone)]
126+pub struct PrettierInstance {
127+ attempt: usize,
128+ prettier: Option<PrettierTask>,
129+}
130+
131+impl Default for DefaultPrettier {
132+ fn default() -> Self {
133+ Self {
134+ prettier: PrettierInstallation::NotInstalled {
135+ attempts: 0,
136+ installation_task: None,
137+ not_installed_plugins: HashSet::default(),
138+ },
139+ installed_plugins: HashSet::default(),
140+ }
141+ }
142+}
143+
144+impl DefaultPrettier {
145+ pub fn instance(&self) -> Option<&PrettierInstance> {
146+ if let PrettierInstallation::Installed(instance) = &self.prettier {
147+ Some(instance)
148+ } else {
149+ None
150+ }
151+ }
152+
153+ pub fn prettier_task(
154+ &mut self,
155+ node: &Arc<dyn NodeRuntime>,
156+ worktree_id: Option<WorktreeId>,
157+ cx: &mut ModelContext<'_, Project>,
158+ ) -> Option<Task<anyhow::Result<PrettierTask>>> {
159+ match &mut self.prettier {
160+ PrettierInstallation::NotInstalled { .. } => {
161+ Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
162+ }
163+ PrettierInstallation::Installed(existing_instance) => {
164+ existing_instance.prettier_task(node, None, worktree_id, cx)
165+ }
166+ }
167+ }
168+}
169+
170+impl PrettierInstance {
171+ pub fn prettier_task(
172+ &mut self,
173+ node: &Arc<dyn NodeRuntime>,
174+ prettier_dir: Option<&Path>,
175+ worktree_id: Option<WorktreeId>,
176+ cx: &mut ModelContext<'_, Project>,
177+ ) -> Option<Task<anyhow::Result<PrettierTask>>> {
178+ if self.attempt > prettier::FAIL_THRESHOLD {
179+ match prettier_dir {
180+ Some(prettier_dir) => log::warn!(
181+ "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
182+ ),
183+ None => log::warn!("Default prettier exceeded launch threshold, not starting"),
184+ }
185+ return None;
186+ }
187+ Some(match &self.prettier {
188+ Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
189+ None => match prettier_dir {
190+ Some(prettier_dir) => {
191+ let new_task = start_prettier(
192+ Arc::clone(node),
193+ prettier_dir.to_path_buf(),
194+ worktree_id,
195+ cx,
196+ );
197+ self.attempt += 1;
198+ self.prettier = Some(new_task.clone());
199+ Task::ready(Ok(new_task))
200+ }
201+ None => {
202+ self.attempt += 1;
203+ let node = Arc::clone(node);
204+ cx.spawn(|project, mut cx| async move {
205+ project
206+ .update(&mut cx, |_, cx| {
207+ start_default_prettier(node, worktree_id, cx)
208+ })?
209+ .await
210+ })
211+ }
212+ },
213+ })
214+ }
215+}
216+
217+fn start_default_prettier(
218+ node: Arc<dyn NodeRuntime>,
219+ worktree_id: Option<WorktreeId>,
220+ cx: &mut ModelContext<'_, Project>,
221+) -> Task<anyhow::Result<PrettierTask>> {
222+ cx.spawn(|project, mut cx| async move {
223+ loop {
224+ let installation_task = project.update(&mut cx, |project, _| {
225+ match &project.default_prettier.prettier {
226+ PrettierInstallation::NotInstalled {
227+ installation_task, ..
228+ } => ControlFlow::Continue(installation_task.clone()),
229+ PrettierInstallation::Installed(default_prettier) => {
230+ ControlFlow::Break(default_prettier.clone())
231+ }
232+ }
233+ })?;
234+ match installation_task {
235+ ControlFlow::Continue(None) => {
236+ anyhow::bail!("Default prettier is not installed and cannot be started")
237+ }
238+ ControlFlow::Continue(Some(installation_task)) => {
239+ log::info!("Waiting for default prettier to install");
240+ if let Err(e) = installation_task.await {
241+ project.update(&mut cx, |project, _| {
242+ if let PrettierInstallation::NotInstalled {
243+ installation_task,
244+ attempts,
245+ ..
246+ } = &mut project.default_prettier.prettier
247+ {
248+ *installation_task = None;
249+ *attempts += 1;
250+ }
251+ })?;
252+ anyhow::bail!(
253+ "Cannot start default prettier due to its installation failure: {e:#}"
254+ );
255+ }
256+ let new_default_prettier = project.update(&mut cx, |project, cx| {
257+ let new_default_prettier =
258+ start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
259+ project.default_prettier.prettier =
260+ PrettierInstallation::Installed(PrettierInstance {
261+ attempt: 0,
262+ prettier: Some(new_default_prettier.clone()),
263+ });
264+ new_default_prettier
265+ })?;
266+ return Ok(new_default_prettier);
267+ }
268+ ControlFlow::Break(instance) => match instance.prettier {
269+ Some(instance) => return Ok(instance),
270+ None => {
271+ let new_default_prettier = project.update(&mut cx, |project, cx| {
272+ let new_default_prettier =
273+ start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
274+ project.default_prettier.prettier =
275+ PrettierInstallation::Installed(PrettierInstance {
276+ attempt: instance.attempt + 1,
277+ prettier: Some(new_default_prettier.clone()),
278+ });
279+ new_default_prettier
280+ })?;
281+ return Ok(new_default_prettier);
282+ }
283+ },
284+ }
285+ }
286+ })
287+}
288+
289+fn start_prettier(
290+ node: Arc<dyn NodeRuntime>,
291+ prettier_dir: PathBuf,
292+ worktree_id: Option<WorktreeId>,
293+ cx: &mut ModelContext<'_, Project>,
294+) -> PrettierTask {
295+ cx.spawn(|project, mut cx| async move {
296+ log::info!("Starting prettier at path {prettier_dir:?}");
297+ let new_server_id = project.update(&mut cx, |project, _| {
298+ project.languages.next_language_server_id()
299+ })?;
300+
301+ let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
302+ .await
303+ .context("default prettier spawn")
304+ .map(Arc::new)
305+ .map_err(Arc::new)?;
306+ register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
307+ Ok(new_prettier)
308+ })
309+ .shared()
310+}
311+
312+fn register_new_prettier(
313+ project: &WeakModel<Project>,
314+ prettier: &Prettier,
315+ worktree_id: Option<WorktreeId>,
316+ new_server_id: LanguageServerId,
317+ cx: &mut AsyncAppContext,
318+) {
319+ let prettier_dir = prettier.prettier_dir();
320+ let is_default = prettier.is_default();
321+ if is_default {
322+ log::info!("Started default prettier in {prettier_dir:?}");
323+ } else {
324+ log::info!("Started prettier in {prettier_dir:?}");
325+ }
326+ if let Some(prettier_server) = prettier.server() {
327+ project
328+ .update(cx, |project, cx| {
329+ let name = if is_default {
330+ LanguageServerName(Arc::from("prettier (default)"))
331+ } else {
332+ let worktree_path = worktree_id
333+ .and_then(|id| project.worktree_for_id(id, cx))
334+ .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
335+ let name = match worktree_path {
336+ Some(worktree_path) => {
337+ if prettier_dir == worktree_path.as_ref() {
338+ let name = prettier_dir
339+ .file_name()
340+ .and_then(|name| name.to_str())
341+ .unwrap_or_default();
342+ format!("prettier ({name})")
343+ } else {
344+ let dir_to_display = prettier_dir
345+ .strip_prefix(worktree_path.as_ref())
346+ .ok()
347+ .unwrap_or(prettier_dir);
348+ format!("prettier ({})", dir_to_display.display())
349+ }
350+ }
351+ None => format!("prettier ({})", prettier_dir.display()),
352+ };
353+ LanguageServerName(Arc::from(name))
354+ };
355+ project
356+ .supplementary_language_servers
357+ .insert(new_server_id, (name, Arc::clone(prettier_server)));
358+ cx.emit(Event::LanguageServerAdded(new_server_id));
359+ })
360+ .ok();
361+ }
362+}
363+
364+async fn install_prettier_packages(
365+ plugins_to_install: HashSet<&'static str>,
366+ node: Arc<dyn NodeRuntime>,
367+) -> anyhow::Result<()> {
368+ let packages_to_versions =
369+ future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
370+ |package_name| async {
371+ let returned_package_name = package_name.to_string();
372+ let latest_version = node
373+ .npm_package_latest_version(package_name)
374+ .await
375+ .with_context(|| {
376+ format!("fetching latest npm version for package {returned_package_name}")
377+ })?;
378+ anyhow::Ok((returned_package_name, latest_version))
379+ },
380+ ))
381+ .await
382+ .context("fetching latest npm versions")?;
383+
384+ log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
385+ let borrowed_packages = packages_to_versions
386+ .iter()
387+ .map(|(package, version)| (package.as_str(), version.as_str()))
388+ .collect::<Vec<_>>();
389+ node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
390+ .await
391+ .context("fetching formatter packages")?;
392+ anyhow::Ok(())
393+}
394+
395+async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
396+ let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
397+ fs.save(
398+ &prettier_wrapper_path,
399+ &text::Rope::from(prettier::PRETTIER_SERVER_JS),
400+ text::LineEnding::Unix,
401+ )
402+ .await
403+ .with_context(|| {
404+ format!(
405+ "writing {} file at {prettier_wrapper_path:?}",
406+ prettier::PRETTIER_SERVER_FILE
407+ )
408+ })?;
409+ Ok(())
410+}
411+
412+impl Project {
413+ pub fn update_prettier_settings(
414+ &self,
415+ worktree: &Model<Worktree>,
416+ changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
417+ cx: &mut ModelContext<'_, Project>,
418+ ) {
419+ let prettier_config_files = Prettier::CONFIG_FILE_NAMES
420+ .iter()
421+ .map(Path::new)
422+ .collect::<HashSet<_>>();
423+
424+ let prettier_config_file_changed = changes
425+ .iter()
426+ .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
427+ .filter(|(path, _, _)| {
428+ !path
429+ .components()
430+ .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
431+ })
432+ .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
433+ let current_worktree_id = worktree.read(cx).id();
434+ if let Some((config_path, _, _)) = prettier_config_file_changed {
435+ log::info!(
436+ "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
437+ );
438+ let prettiers_to_reload =
439+ self.prettiers_per_worktree
440+ .get(&current_worktree_id)
441+ .iter()
442+ .flat_map(|prettier_paths| prettier_paths.iter())
443+ .flatten()
444+ .filter_map(|prettier_path| {
445+ Some((
446+ current_worktree_id,
447+ Some(prettier_path.clone()),
448+ self.prettier_instances.get(prettier_path)?.clone(),
449+ ))
450+ })
451+ .chain(self.default_prettier.instance().map(|default_prettier| {
452+ (current_worktree_id, None, default_prettier.clone())
453+ }))
454+ .collect::<Vec<_>>();
455+
456+ cx.background_executor()
457+ .spawn(async move {
458+ let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
459+ async move {
460+ if let Some(instance) = prettier_instance.prettier {
461+ match instance.await {
462+ Ok(prettier) => {
463+ prettier.clear_cache().log_err().await;
464+ },
465+ Err(e) => {
466+ match prettier_path {
467+ Some(prettier_path) => log::error!(
468+ "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
469+ ),
470+ None => log::error!(
471+ "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
472+ ),
473+ }
474+ },
475+ }
476+ }
477+ }
478+ }))
479+ .await;
480+ })
481+ .detach();
482+ }
483+ }
484+
485+ fn prettier_instance_for_buffer(
486+ &mut self,
487+ buffer: &Model<Buffer>,
488+ cx: &mut ModelContext<Self>,
489+ ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
490+ let buffer = buffer.read(cx);
491+ let buffer_file = buffer.file();
492+ let Some(buffer_language) = buffer.language() else {
493+ return Task::ready(None);
494+ };
495+ if buffer_language.prettier_parser_name().is_none() {
496+ return Task::ready(None);
497+ }
498+
499+ if self.is_local() {
500+ let Some(node) = self.node.as_ref().map(Arc::clone) else {
501+ return Task::ready(None);
502+ };
503+ match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
504+ {
505+ Some((worktree_id, buffer_path)) => {
506+ let fs = Arc::clone(&self.fs);
507+ let installed_prettiers = self.prettier_instances.keys().cloned().collect();
508+ return cx.spawn(|project, mut cx| async move {
509+ match cx
510+ .background_executor()
511+ .spawn(async move {
512+ Prettier::locate_prettier_installation(
513+ fs.as_ref(),
514+ &installed_prettiers,
515+ &buffer_path,
516+ )
517+ .await
518+ })
519+ .await
520+ {
521+ Ok(ControlFlow::Break(())) => {
522+ return None;
523+ }
524+ Ok(ControlFlow::Continue(None)) => {
525+ let default_instance = project
526+ .update(&mut cx, |project, cx| {
527+ project
528+ .prettiers_per_worktree
529+ .entry(worktree_id)
530+ .or_default()
531+ .insert(None);
532+ project.default_prettier.prettier_task(
533+ &node,
534+ Some(worktree_id),
535+ cx,
536+ )
537+ })
538+ .ok()?;
539+ Some((None, default_instance?.log_err().await?))
540+ }
541+ Ok(ControlFlow::Continue(Some(prettier_dir))) => {
542+ project
543+ .update(&mut cx, |project, _| {
544+ project
545+ .prettiers_per_worktree
546+ .entry(worktree_id)
547+ .or_default()
548+ .insert(Some(prettier_dir.clone()))
549+ })
550+ .ok()?;
551+ if let Some(prettier_task) = project
552+ .update(&mut cx, |project, cx| {
553+ project.prettier_instances.get_mut(&prettier_dir).map(
554+ |existing_instance| {
555+ existing_instance.prettier_task(
556+ &node,
557+ Some(&prettier_dir),
558+ Some(worktree_id),
559+ cx,
560+ )
561+ },
562+ )
563+ })
564+ .ok()?
565+ {
566+ log::debug!(
567+ "Found already started prettier in {prettier_dir:?}"
568+ );
569+ return Some((
570+ Some(prettier_dir),
571+ prettier_task?.await.log_err()?,
572+ ));
573+ }
574+
575+ log::info!("Found prettier in {prettier_dir:?}, starting.");
576+ let new_prettier_task = project
577+ .update(&mut cx, |project, cx| {
578+ let new_prettier_task = start_prettier(
579+ node,
580+ prettier_dir.clone(),
581+ Some(worktree_id),
582+ cx,
583+ );
584+ project.prettier_instances.insert(
585+ prettier_dir.clone(),
586+ PrettierInstance {
587+ attempt: 0,
588+ prettier: Some(new_prettier_task.clone()),
589+ },
590+ );
591+ new_prettier_task
592+ })
593+ .ok()?;
594+ Some((Some(prettier_dir), new_prettier_task))
595+ }
596+ Err(e) => {
597+ log::error!("Failed to determine prettier path for buffer: {e:#}");
598+ return None;
599+ }
600+ }
601+ });
602+ }
603+ None => {
604+ let new_task = self.default_prettier.prettier_task(&node, None, cx);
605+ return cx
606+ .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) });
607+ }
608+ }
609+ } else {
610+ return Task::ready(None);
611+ }
612+ }
613+
614+ #[cfg(any(test, feature = "test-support"))]
615+ pub fn install_default_prettier(
616+ &mut self,
617+ _worktree: Option<WorktreeId>,
618+ plugins: HashSet<&'static str>,
619+ _cx: &mut ModelContext<Self>,
620+ ) {
621+ // suppress unused code warnings
622+ let _ = install_prettier_packages;
623+ let _ = save_prettier_server_file;
624+
625+ self.default_prettier.installed_plugins.extend(plugins);
626+ self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
627+ attempt: 0,
628+ prettier: None,
629+ });
630+ }
631+
632+ #[cfg(not(any(test, feature = "test-support")))]
633+ pub fn install_default_prettier(
634+ &mut self,
635+ worktree: Option<WorktreeId>,
636+ mut new_plugins: HashSet<&'static str>,
637+ cx: &mut ModelContext<Self>,
638+ ) {
639+ let Some(node) = self.node.as_ref().cloned() else {
640+ return;
641+ };
642+ log::info!("Initializing default prettier with plugins {new_plugins:?}");
643+ let fs = Arc::clone(&self.fs);
644+ let locate_prettier_installation = match worktree.and_then(|worktree_id| {
645+ self.worktree_for_id(worktree_id, cx)
646+ .map(|worktree| worktree.read(cx).abs_path())
647+ }) {
648+ Some(locate_from) => {
649+ let installed_prettiers = self.prettier_instances.keys().cloned().collect();
650+ cx.background_executor().spawn(async move {
651+ Prettier::locate_prettier_installation(
652+ fs.as_ref(),
653+ &installed_prettiers,
654+ locate_from.as_ref(),
655+ )
656+ .await
657+ })
658+ }
659+ None => Task::ready(Ok(ControlFlow::Continue(None))),
660+ };
661+ new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
662+ let mut installation_attempt = 0;
663+ let previous_installation_task = match &mut self.default_prettier.prettier {
664+ PrettierInstallation::NotInstalled {
665+ installation_task,
666+ attempts,
667+ not_installed_plugins,
668+ } => {
669+ installation_attempt = *attempts;
670+ if installation_attempt > prettier::FAIL_THRESHOLD {
671+ *installation_task = None;
672+ log::warn!(
673+ "Default prettier installation had failed {installation_attempt} times, not attempting again",
674+ );
675+ return;
676+ }
677+ new_plugins.extend(not_installed_plugins.iter());
678+ installation_task.clone()
679+ }
680+ PrettierInstallation::Installed { .. } => {
681+ if new_plugins.is_empty() {
682+ return;
683+ }
684+ None
685+ }
686+ };
687+
688+ let plugins_to_install = new_plugins.clone();
689+ let fs = Arc::clone(&self.fs);
690+ let new_installation_task = cx
691+ .spawn(|project, mut cx| async move {
692+ match locate_prettier_installation
693+ .await
694+ .context("locate prettier installation")
695+ .map_err(Arc::new)?
696+ {
697+ ControlFlow::Break(()) => return Ok(()),
698+ ControlFlow::Continue(prettier_path) => {
699+ if prettier_path.is_some() {
700+ new_plugins.clear();
701+ }
702+ let mut needs_install = false;
703+ if let Some(previous_installation_task) = previous_installation_task {
704+ if let Err(e) = previous_installation_task.await {
705+ log::error!("Failed to install default prettier: {e:#}");
706+ project.update(&mut cx, |project, _| {
707+ if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
708+ *attempts += 1;
709+ new_plugins.extend(not_installed_plugins.iter());
710+ installation_attempt = *attempts;
711+ needs_install = true;
712+ };
713+ })?;
714+ }
715+ };
716+ if installation_attempt > prettier::FAIL_THRESHOLD {
717+ project.update(&mut cx, |project, _| {
718+ if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
719+ *installation_task = None;
720+ };
721+ })?;
722+ log::warn!(
723+ "Default prettier installation had failed {installation_attempt} times, not attempting again",
724+ );
725+ return Ok(());
726+ }
727+ project.update(&mut cx, |project, _| {
728+ new_plugins.retain(|plugin| {
729+ !project.default_prettier.installed_plugins.contains(plugin)
730+ });
731+ if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
732+ not_installed_plugins.retain(|plugin| {
733+ !project.default_prettier.installed_plugins.contains(plugin)
734+ });
735+ not_installed_plugins.extend(new_plugins.iter());
736+ }
737+ needs_install |= !new_plugins.is_empty();
738+ })?;
739+ if needs_install {
740+ let installed_plugins = new_plugins.clone();
741+ cx.background_executor()
742+ .spawn(async move {
743+ save_prettier_server_file(fs.as_ref()).await?;
744+ install_prettier_packages(new_plugins, node).await
745+ })
746+ .await
747+ .context("prettier & plugins install")
748+ .map_err(Arc::new)?;
749+ log::info!("Initialized prettier with plugins: {installed_plugins:?}");
750+ project.update(&mut cx, |project, _| {
751+ project.default_prettier.prettier =
752+ PrettierInstallation::Installed(PrettierInstance {
753+ attempt: 0,
754+ prettier: None,
755+ });
756+ project.default_prettier
757+ .installed_plugins
758+ .extend(installed_plugins);
759+ })?;
760+ }
761+ }
762+ }
763+ Ok(())
764+ })
765+ .shared();
766+ self.default_prettier.prettier = PrettierInstallation::NotInstalled {
767+ attempts: installation_attempt,
768+ installation_task: Some(new_installation_task),
769+ not_installed_plugins: plugins_to_install,
770+ };
771+ }
772+}
crates/project2/src/project2.rs+51−677
@@ -1,5 +1,6 @@
11 mod ignore;
22 mod lsp_command;
3+mod prettier_support;
34 pub mod project_settings;
45 pub mod search;
56 pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{ · 14 unmodified lines
2021 mpsc::{self, UnboundedReceiver},
2122 oneshot,
2223 },
23− future::{self, try_join_all, Shared},
24+ future::{try_join_all, Shared},
2425 stream::FuturesUnordered,
2526 AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
2627 };
@@ -31,9 +32,7 @@ use gpui::{ · 4 unmodified lines
3132 };
3233 use itertools::Itertools;
3334 use language::{
34− language_settings::{
35− language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
36− },
35+ language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
3736 point_to_lsp,
3837 proto::{
3938 deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*; · 14 unmodified lines
5453 use node_runtime::NodeRuntime;
5554 use parking_lot::Mutex;
5655 use postage::watch;
57−use prettier::Prettier;
56+use prettier_support::{DefaultPrettier, PrettierInstance};
5857 use project_settings::{LspSettings, ProjectSettings};
5958 use rand::prelude::*;
6059 use search::SearchQuery;
@@ -70,7 +69,7 @@ use std::{ · 9 unmodified lines
7069 hash::Hash,
7170 mem,
7271 num::NonZeroU32,
73− ops::{ControlFlow, Range},
72+ ops::Range,
7473 path::{self, Component, Path, PathBuf},
7574 process::Stdio,
7675 str,
@@ -83,11 +82,8 @@ use std::{ · 6 unmodified lines
8382 use terminals::Terminals;
8483 use text::Anchor;
8584 use util::{
86− debug_panic, defer,
87− http::HttpClient,
88− merge_json_value_into,
89− paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
90− post_inc, ResultExt, TryFutureExt as _,
85+ debug_panic, defer, http::HttpClient, merge_json_value_into,
86+ paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
9187 };
9288
9389 pub use fs::*;
@@ -166,16 +162,9 @@ pub struct Project { · 72 unmodified lines
166162 copilot_log_subscription: Option<lsp::Subscription>,
167163 current_lsp_settings: HashMap<Arc<str>, LspSettings>,
168164 node: Option<Arc<dyn NodeRuntime>>,
169− default_prettier: Option<DefaultPrettier>,
165+ default_prettier: DefaultPrettier,
170166 prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
171− prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
172−}
173−
174−struct DefaultPrettier {
175− instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
176− installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
177− #[cfg(not(any(test, feature = "test-support")))]
178− installed_plugins: HashSet<&'static str>,
167+ prettier_instances: HashMap<PathBuf, PrettierInstance>,
179168 }
180169
181170 struct DelayedDebounced {
@@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate { · 358 unmodified lines
540529 http_client: Arc<dyn HttpClient>,
541530 }
542531
532+// Currently, formatting operations are represented differently depending on
533+// whether they come from a language server or an external command.
534+enum FormatOperation {
535+ Lsp(Vec<(Range<Anchor>, String)>),
536+ External(Diff),
537+ Prettier(Diff),
538+}
539+
543540 impl FormatTrigger {
544541 fn from_proto(value: i32) -> FormatTrigger {
545542 match value {
@@ -689,7 +686,7 @@ impl Project { · 143 unmodified lines
689686 copilot_log_subscription: None,
690687 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
691688 node: Some(node),
692− default_prettier: None,
689+ default_prettier: DefaultPrettier::default(),
693690 prettiers_per_worktree: HashMap::default(),
694691 prettier_instances: HashMap::default(),
695692 }
@@ -792,7 +789,7 @@ impl Project { · 96 unmodified lines
792789 copilot_log_subscription: None,
793790 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
794791 node: None,
795− default_prettier: None,
792+ default_prettier: DefaultPrettier::default(),
796793 prettiers_per_worktree: HashMap::default(),
797794 prettier_instances: HashMap::default(),
798795 };
@@ -965,8 +962,19 @@ impl Project { · 166 unmodified lines
965962 .detach();
966963 }
967964
965+ let mut prettier_plugins_by_worktree = HashMap::default();
968966 for (worktree, language, settings) in language_formatters_to_check {
969− self.install_default_formatters(worktree, &language, &settings, cx);
967+ if let Some(plugins) =
968+ prettier_support::prettier_plugins_for_language(&language, &settings)
969+ {
970+ prettier_plugins_by_worktree
971+ .entry(worktree)
972+ .or_insert_with(|| HashSet::default())
973+ .extend(plugins);
974+ }
975+ }
976+ for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
977+ self.install_default_prettier(worktree, prettier_plugins, cx);
970978 }
971979
972980 // Start all the newly-enabled language servers.
@@ -2722,8 +2730,11 @@ impl Project { · 1749 unmodified lines
27222730 let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
27232731 let buffer_file = File::from_dyn(buffer_file.as_ref());
27242732 let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
2725−
2726− self.install_default_formatters(worktree, &new_language, &settings, cx);
2733+ if let Some(prettier_plugins) =
2734+ prettier_support::prettier_plugins_for_language(&new_language, &settings)
2735+ {
2736+ self.install_default_prettier(worktree, prettier_plugins, cx);
2737+ };
27272738 if let Some(file) = buffer_file {
27282739 let worktree = file.worktree.clone();
27292740 if let Some(tree) = worktree.read(cx).as_local() {
@@ -4126,7 +4137,8 @@ impl Project { · 1396 unmodified lines
41264137 this.buffers_being_formatted
41274138 .remove(&buffer.read(cx).remote_id());
41284139 }
4129− }).ok();
4140+ })
4141+ .ok();
41304142 }
41314143 });
41324144
@@ -4138,8 +4150,6 @@ impl Project { · 5 unmodified lines
41384150
41394151 let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
41404152 let ensure_final_newline = settings.ensure_final_newline_on_save;
4141− let format_on_save = settings.format_on_save.clone();
4142− let formatter = settings.formatter.clone();
41434153 let tab_size = settings.tab_size;
41444154
41454155 // First, format buffer's whitespace according to the settings.
@@ -4164,18 +4174,10 @@ impl Project { · 18 unmodified lines
41644174 buffer.end_transaction(cx)
41654175 })?;
41664176
4167− // Currently, formatting operations are represented differently depending on
4168− // whether they come from a language server or an external command.
4169− enum FormatOperation {
4170− Lsp(Vec<(Range<Anchor>, String)>),
4171− External(Diff),
4172− Prettier(Diff),
4173− }
4174−
41754177 // Apply language-specific formatting using either a language server
41764178 // or external command.
41774179 let mut format_operation = None;
4178− match (formatter, format_on_save) {
4180+ match (&settings.formatter, &settings.format_on_save) {
41794181 (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
41804182
41814183 (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4220,46 +4222,11 @@ impl Project { · 38 unmodified lines
42204222 }
42214223 }
42224224 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
4223− if let Some((prettier_path, prettier_task)) = project
4224− .update(&mut cx, |project, cx| {
4225− project.prettier_instance_for_buffer(buffer, cx)
4226− })?.await {
4227− match prettier_task.await
4228− {
4229− Ok(prettier) => {
4230− let buffer_path = buffer.update(&mut cx, |buffer, cx| {
4231− File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
4232− })?;
4233− format_operation = Some(FormatOperation::Prettier(
4234− prettier
4235− .format(buffer, buffer_path, &mut cx)
4236− .await
4237− .context("formatting via prettier")?,
4238− ));
4239− }
4240− Err(e) => {
4241− project.update(&mut cx, |project, _| {
4242− match &prettier_path {
4243− Some(prettier_path) => {
4244− project.prettier_instances.remove(prettier_path);
4245− },
4246− None => {
4247− if let Some(default_prettier) = project.default_prettier.as_mut() {
4248− default_prettier.instance = None;
4249− }
4250− },
4251− }
4252− })?;
4253− match &prettier_path {
4254− Some(prettier_path) => {
4255− log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
4256− },
4257− None => {
4258− log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
4259− },
4260− }
4261− }
4262− }
4225+ if let Some(new_operation) =
4226+ prettier_support::format_with_prettier(&project, buffer, &mut cx)
4227+ .await
4228+ {
4229+ format_operation = Some(new_operation);
42634230 } else if let Some((language_server, buffer_abs_path)) =
42644231 language_server.as_ref().zip(buffer_abs_path.as_ref())
42654232 {
@@ -4277,48 +4244,13 @@ impl Project { · 11 unmodified lines
42774244 ));
42784245 }
42794246 }
4280− (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
4281− if let Some((prettier_path, prettier_task)) = project
4282− .update(&mut cx, |project, cx| {
4283− project.prettier_instance_for_buffer(buffer, cx)
4284− })?.await {
4285− match prettier_task.await
4286− {
4287− Ok(prettier) => {
4288− let buffer_path = buffer.update(&mut cx, |buffer, cx| {
4289− File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
4290− })?;
4291− format_operation = Some(FormatOperation::Prettier(
4292− prettier
4293− .format(buffer, buffer_path, &mut cx)
4294− .await
4295− .context("formatting via prettier")?,
4296− ));
4297− }
4298− Err(e) => {
4299− project.update(&mut cx, |project, _| {
4300− match &prettier_path {
4301− Some(prettier_path) => {
4302− project.prettier_instances.remove(prettier_path);
4303− },
4304− None => {
4305− if let Some(default_prettier) = project.default_prettier.as_mut() {
4306− default_prettier.instance = None;
4307− }
4308− },
4309− }
4310− })?;
4311− match &prettier_path {
4312− Some(prettier_path) => {
4313− log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
4314− },
4315− None => {
4316− log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
4317− },
4318− }
4319− }
4320− }
4321− }
4247+ (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
4248+ if let Some(new_operation) =
4249+ prettier_support::format_with_prettier(&project, buffer, &mut cx)
4250+ .await
4251+ {
4252+ format_operation = Some(new_operation);
4253+ }
43224254 }
43234255 };
43244256
@@ -6638,84 +6570,6 @@ impl Project { · 2313 unmodified lines
66386570 .detach();
66396571 }
66406572
6641− fn update_prettier_settings(
6642− &self,
6643− worktree: &Model<Worktree>,
6644− changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
6645− cx: &mut ModelContext<'_, Project>,
6646− ) {
6647− let prettier_config_files = Prettier::CONFIG_FILE_NAMES
6648− .iter()
6649− .map(Path::new)
6650− .collect::<HashSet<_>>();
6651−
6652− let prettier_config_file_changed = changes
6653− .iter()
6654− .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
6655− .filter(|(path, _, _)| {
6656− !path
6657− .components()
6658− .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
6659− })
6660− .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
6661− let current_worktree_id = worktree.read(cx).id();
6662− if let Some((config_path, _, _)) = prettier_config_file_changed {
6663− log::info!(
6664− "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
6665− );
6666− let prettiers_to_reload = self
6667− .prettiers_per_worktree
6668− .get(&current_worktree_id)
6669− .iter()
6670− .flat_map(|prettier_paths| prettier_paths.iter())
6671− .flatten()
6672− .filter_map(|prettier_path| {
6673− Some((
6674− current_worktree_id,
6675− Some(prettier_path.clone()),
6676− self.prettier_instances.get(prettier_path)?.clone(),
6677− ))
6678− })
6679− .chain(self.default_prettier.iter().filter_map(|default_prettier| {
6680− Some((
6681− current_worktree_id,
6682− None,
6683− default_prettier.instance.clone()?,
6684− ))
6685− }))
6686− .collect::<Vec<_>>();
6687−
6688− cx.background_executor()
6689− .spawn(async move {
6690− for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
6691− async move {
6692− prettier_task.await?
6693− .clear_cache()
6694− .await
6695− .with_context(|| {
6696− match prettier_path {
6697− Some(prettier_path) => format!(
6698− "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
6699− ),
6700− None => format!(
6701− "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
6702− ),
6703− }
6704− })
6705− .map_err(Arc::new)
6706− }
6707− }))
6708− .await
6709− {
6710− if let Err(e) = task_result {
6711− log::error!("Failed to clear cache for prettier: {e:#}");
6712− }
6713− }
6714− })
6715− .detach();
6716− }
6717− }
6718−
67196573 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
67206574 let new_active_entry = entry.and_then(|project_path| {
67216575 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -8579,486 +8433,6 @@ impl Project { · 1857 unmodified lines
85798433 Vec::new()
85808434 }
85818435 }
8582−
8583− fn prettier_instance_for_buffer(
8584− &mut self,
8585− buffer: &Model<Buffer>,
8586− cx: &mut ModelContext<Self>,
8587− ) -> Task<
8588− Option<(
8589− Option<PathBuf>,
8590− Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
8591− )>,
8592− > {
8593− let buffer = buffer.read(cx);
8594− let buffer_file = buffer.file();
8595− let Some(buffer_language) = buffer.language() else {
8596− return Task::ready(None);
8597− };
8598− if buffer_language.prettier_parser_name().is_none() {
8599− return Task::ready(None);
8600− }
8601−
8602− if self.is_local() {
8603− let Some(node) = self.node.as_ref().map(Arc::clone) else {
8604− return Task::ready(None);
8605− };
8606− match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
8607− {
8608− Some((worktree_id, buffer_path)) => {
8609− let fs = Arc::clone(&self.fs);
8610− let installed_prettiers = self.prettier_instances.keys().cloned().collect();
8611− return cx.spawn(|project, mut cx| async move {
8612− match cx
8613− .background_executor()
8614− .spawn(async move {
8615− Prettier::locate_prettier_installation(
8616− fs.as_ref(),
8617− &installed_prettiers,
8618− &buffer_path,
8619− )
8620− .await
8621− })
8622− .await
8623− {
8624− Ok(ControlFlow::Break(())) => {
8625− return None;
8626− }
8627− Ok(ControlFlow::Continue(None)) => {
8628− match project.update(&mut cx, |project, _| {
8629− project
8630− .prettiers_per_worktree
8631− .entry(worktree_id)
8632− .or_default()
8633− .insert(None);
8634− project.default_prettier.as_ref().and_then(
8635− |default_prettier| default_prettier.instance.clone(),
8636− )
8637− }) {
8638− Ok(Some(old_task)) => Some((None, old_task)),
8639− Ok(None) => {
8640− match project.update(&mut cx, |_, cx| {
8641− start_default_prettier(node, Some(worktree_id), cx)
8642− }) {
8643− Ok(new_default_prettier) => {
8644− return Some((None, new_default_prettier.await))
8645− }
8646− Err(e) => {
8647− Some((
8648− None,
8649− Task::ready(Err(Arc::new(e.context("project is gone during default prettier startup"))))
8650− .shared(),
8651− ))
8652− }
8653− }
8654− }
8655− Err(e) => Some((None, Task::ready(Err(Arc::new(e.context("project is gone during default prettier checks"))))
8656− .shared())),
8657− }
8658− }
8659− Ok(ControlFlow::Continue(Some(prettier_dir))) => {
8660− match project.update(&mut cx, |project, _| {
8661− project
8662− .prettiers_per_worktree
8663− .entry(worktree_id)
8664− .or_default()
8665− .insert(Some(prettier_dir.clone()));
8666− project.prettier_instances.get(&prettier_dir).cloned()
8667− }) {
8668− Ok(Some(existing_prettier)) => {
8669− log::debug!(
8670− "Found already started prettier in {prettier_dir:?}"
8671− );
8672− return Some((Some(prettier_dir), existing_prettier));
8673− }
8674− Err(e) => {
8675− return Some((
8676− Some(prettier_dir),
8677− Task::ready(Err(Arc::new(e.context("project is gone during custom prettier checks"))))
8678− .shared(),
8679− ))
8680− }
8681− _ => {},
8682− }
8683−
8684− log::info!("Found prettier in {prettier_dir:?}, starting.");
8685− let new_prettier_task =
8686− match project.update(&mut cx, |project, cx| {
8687− let new_prettier_task = start_prettier(
8688− node,
8689− prettier_dir.clone(),
8690− Some(worktree_id),
8691− cx,
8692− );
8693− project.prettier_instances.insert(
8694− prettier_dir.clone(),
8695− new_prettier_task.clone(),
8696− );
8697− new_prettier_task
8698− }) {
8699− Ok(task) => task,
8700− Err(e) => return Some((
8701− Some(prettier_dir),
8702− Task::ready(Err(Arc::new(e.context("project is gone during custom prettier startup"))))
8703− .shared()
8704− )),
8705− };
8706− Some((Some(prettier_dir), new_prettier_task))
8707− }
8708− Err(e) => {
8709− return Some((
8710− None,
8711− Task::ready(Err(Arc::new(
8712− e.context("determining prettier path"),
8713− )))
8714− .shared(),
8715− ));
8716− }
8717− }
8718− });
8719− }
8720− None => {
8721− let started_default_prettier = self
8722− .default_prettier
8723− .as_ref()
8724− .and_then(|default_prettier| default_prettier.instance.clone());
8725− match started_default_prettier {
8726− Some(old_task) => return Task::ready(Some((None, old_task))),
8727− None => {
8728− let new_task = start_default_prettier(node, None, cx);
8729− return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
8730− }
8731− }
8732− }
8733− }
8734− } else if self.remote_id().is_some() {
8735− return Task::ready(None);
8736− } else {
8737− Task::ready(Some((
8738− None,
8739− Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
8740− )))
8741− }
8742− }
8743−
8744− #[cfg(any(test, feature = "test-support"))]
8745− fn install_default_formatters(
8746− &mut self,
8747− _: Option<WorktreeId>,
8748− _: &Language,
8749− _: &LanguageSettings,
8750− _: &mut ModelContext<Self>,
8751− ) {
8752− }
8753−
8754− #[cfg(not(any(test, feature = "test-support")))]
8755− fn install_default_formatters(
8756− &mut self,
8757− worktree: Option<WorktreeId>,
8758− new_language: &Language,
8759− language_settings: &LanguageSettings,
8760− cx: &mut ModelContext<Self>,
8761− ) {
8762− match &language_settings.formatter {
8763− Formatter::Prettier { .. } | Formatter::Auto => {}
8764− Formatter::LanguageServer | Formatter::External { .. } => return,
8765− };
8766− let Some(node) = self.node.as_ref().cloned() else {
8767− return;
8768− };
8769−
8770− let mut prettier_plugins = None;
8771− if new_language.prettier_parser_name().is_some() {
8772− prettier_plugins
8773− .get_or_insert_with(|| HashSet::<&'static str>::default())
8774− .extend(
8775− new_language
8776− .lsp_adapters()
8777− .iter()
8778− .flat_map(|adapter| adapter.prettier_plugins()),
8779− )
8780− }
8781− let Some(prettier_plugins) = prettier_plugins else {
8782− return;
8783− };
8784−
8785− let fs = Arc::clone(&self.fs);
8786− let locate_prettier_installation = match worktree.and_then(|worktree_id| {
8787− self.worktree_for_id(worktree_id, cx)
8788− .map(|worktree| worktree.read(cx).abs_path())
8789− }) {
8790− Some(locate_from) => {
8791− let installed_prettiers = self.prettier_instances.keys().cloned().collect();
8792− cx.background_executor().spawn(async move {
8793− Prettier::locate_prettier_installation(
8794− fs.as_ref(),
8795− &installed_prettiers,
8796− locate_from.as_ref(),
8797− )
8798− .await
8799− })
8800− }
8801− None => Task::ready(Ok(ControlFlow::Break(()))),
8802− };
8803− let mut plugins_to_install = prettier_plugins;
8804− let previous_installation_process =
8805− if let Some(default_prettier) = &mut self.default_prettier {
8806− plugins_to_install
8807− .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
8808− if plugins_to_install.is_empty() {
8809− return;
8810− }
8811− default_prettier.installation_process.clone()
8812− } else {
8813− None
8814− };
8815−
8816− let fs = Arc::clone(&self.fs);
8817− let default_prettier = self
8818− .default_prettier
8819− .get_or_insert_with(|| DefaultPrettier {
8820− instance: None,
8821− installation_process: None,
8822− installed_plugins: HashSet::default(),
8823− });
8824− default_prettier.installation_process = Some(
8825− cx.spawn(|this, mut cx| async move {
8826− match locate_prettier_installation
8827− .await
8828− .context("locate prettier installation")
8829− .map_err(Arc::new)?
8830− {
8831− ControlFlow::Break(()) => return Ok(()),
8832− ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
8833− ControlFlow::Continue(None) => {
8834− let mut needs_install = match previous_installation_process {
8835− Some(previous_installation_process) => {
8836− previous_installation_process.await.is_err()
8837− }
8838− None => true,
8839− };
8840− this.update(&mut cx, |this, _| {
8841− if let Some(default_prettier) = &mut this.default_prettier {
8842− plugins_to_install.retain(|plugin| {
8843− !default_prettier.installed_plugins.contains(plugin)
8844− });
8845− needs_install |= !plugins_to_install.is_empty();
8846− }
8847− })?;
8848− if needs_install {
8849− let installed_plugins = plugins_to_install.clone();
8850− cx.background_executor()
8851− .spawn(async move {
8852− install_default_prettier(plugins_to_install, node, fs).await
8853− })
8854− .await
8855− .context("prettier & plugins install")
8856− .map_err(Arc::new)?;
8857− this.update(&mut cx, |this, _| {
8858− let default_prettier =
8859− this.default_prettier
8860− .get_or_insert_with(|| DefaultPrettier {
8861− instance: None,
8862− installation_process: Some(
8863− Task::ready(Ok(())).shared(),
8864− ),
8865− installed_plugins: HashSet::default(),
8866− });
8867− default_prettier.instance = None;
8868− default_prettier.installed_plugins.extend(installed_plugins);
8869− })?;
8870− }
8871− }
8872− }
8873− Ok(())
8874− })
8875− .shared(),
8876− );
8877− }
8878−}
8879−
8880−fn start_default_prettier(
8881− node: Arc<dyn NodeRuntime>,
8882− worktree_id: Option<WorktreeId>,
8883− cx: &mut ModelContext<'_, Project>,
8884−) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
8885− cx.spawn(|project, mut cx| async move {
8886− loop {
8887− let default_prettier_installing = match project.update(&mut cx, |project, _| {
8888− project
8889− .default_prettier
8890− .as_ref()
8891− .and_then(|default_prettier| default_prettier.installation_process.clone())
8892− }) {
8893− Ok(installation) => installation,
8894− Err(e) => {
8895− return Task::ready(Err(Arc::new(
8896− e.context("project is gone during default prettier installation"),
8897− )))
8898− .shared()
8899− }
8900− };
8901− match default_prettier_installing {
8902− Some(installation_task) => {
8903− if installation_task.await.is_ok() {
8904− break;
8905− }
8906− }
8907− None => break,
8908− }
8909− }
8910−
8911− match project.update(&mut cx, |project, cx| {
8912− match project
8913− .default_prettier
8914− .as_mut()
8915− .and_then(|default_prettier| default_prettier.instance.as_mut())
8916− {
8917− Some(default_prettier) => default_prettier.clone(),
8918− None => {
8919− let new_default_prettier =
8920− start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
8921− project
8922− .default_prettier
8923− .get_or_insert_with(|| DefaultPrettier {
8924− instance: None,
8925− installation_process: None,
8926− #[cfg(not(any(test, feature = "test-support")))]
8927− installed_plugins: HashSet::default(),
8928− })
8929− .instance = Some(new_default_prettier.clone());
8930− new_default_prettier
8931− }
8932− }
8933− }) {
8934− Ok(task) => task,
8935− Err(e) => Task::ready(Err(Arc::new(
8936− e.context("project is gone during default prettier startup"),
8937− )))
8938− .shared(),
8939− }
8940− })
8941−}
8942−
8943−fn start_prettier(
8944− node: Arc<dyn NodeRuntime>,
8945− prettier_dir: PathBuf,
8946− worktree_id: Option<WorktreeId>,
8947− cx: &mut ModelContext<'_, Project>,
8948−) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
8949− cx.spawn(|project, mut cx| async move {
8950− let new_server_id = project.update(&mut cx, |project, _| {
8951− project.languages.next_language_server_id()
8952− })?;
8953− let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
8954− .await
8955− .context("default prettier spawn")
8956− .map(Arc::new)
8957− .map_err(Arc::new)?;
8958− register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
8959− Ok(new_prettier)
8960− })
8961− .shared()
8962−}
8963−
8964−fn register_new_prettier(
8965− project: &WeakModel<Project>,
8966− prettier: &Prettier,
8967− worktree_id: Option<WorktreeId>,
8968− new_server_id: LanguageServerId,
8969− cx: &mut AsyncAppContext,
8970−) {
8971− let prettier_dir = prettier.prettier_dir();
8972− let is_default = prettier.is_default();
8973− if is_default {
8974− log::info!("Started default prettier in {prettier_dir:?}");
8975− } else {
8976− log::info!("Started prettier in {prettier_dir:?}");
8977− }
8978− if let Some(prettier_server) = prettier.server() {
8979− project
8980− .update(cx, |project, cx| {
8981− let name = if is_default {
8982− LanguageServerName(Arc::from("prettier (default)"))
8983− } else {
8984− let worktree_path = worktree_id
8985− .and_then(|id| project.worktree_for_id(id, cx))
8986− .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
8987− let name = match worktree_path {
8988− Some(worktree_path) => {
8989− if prettier_dir == worktree_path.as_ref() {
8990− let name = prettier_dir
8991− .file_name()
8992− .and_then(|name| name.to_str())
8993− .unwrap_or_default();
8994− format!("prettier ({name})")
8995− } else {
8996− let dir_to_display = prettier_dir
8997− .strip_prefix(worktree_path.as_ref())
8998− .ok()
8999− .unwrap_or(prettier_dir);
9000− format!("prettier ({})", dir_to_display.display())
9001− }
9002− }
9003− None => format!("prettier ({})", prettier_dir.display()),
9004− };
9005− LanguageServerName(Arc::from(name))
9006− };
9007− project
9008− .supplementary_language_servers
9009− .insert(new_server_id, (name, Arc::clone(prettier_server)));
9010− cx.emit(Event::LanguageServerAdded(new_server_id));
9011− })
9012− .ok();
9013− }
9014−}
9015−
9016−#[cfg(not(any(test, feature = "test-support")))]
9017−async fn install_default_prettier(
9018− plugins_to_install: HashSet<&'static str>,
9019− node: Arc<dyn NodeRuntime>,
9020− fs: Arc<dyn Fs>,
9021−) -> anyhow::Result<()> {
9022− let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
9023− // method creates parent directory if it doesn't exist
9024− fs.save(
9025− &prettier_wrapper_path,
9026− &text::Rope::from(prettier::PRETTIER_SERVER_JS),
9027− text::LineEnding::Unix,
9028− )
9029− .await
9030− .with_context(|| {
9031− format!(
9032− "writing {} file at {prettier_wrapper_path:?}",
9033− prettier::PRETTIER_SERVER_FILE
9034− )
9035− })?;
9036−
9037− let packages_to_versions =
9038− future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
9039− |package_name| async {
9040− let returned_package_name = package_name.to_string();
9041− let latest_version = node
9042− .npm_package_latest_version(package_name)
9043− .await
9044− .with_context(|| {
9045− format!("fetching latest npm version for package {returned_package_name}")
9046− })?;
9047− anyhow::Ok((returned_package_name, latest_version))
9048− },
9049− ))
9050− .await
9051− .context("fetching latest npm versions")?;
9052−
9053− log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
9054− let borrowed_packages = packages_to_versions
9055− .iter()
9056− .map(|(package, version)| (package.as_str(), version.as_str()))
9057− .collect::<Vec<_>>();
9058− node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
9059− .await
9060− .context("fetching formatter packages")?;
9061− anyhow::Ok(())
90628436 }
90638437
90648438 fn subscribe_for_copilot_events(
crates/project_panel2/src/file_associations.rs+31−40
@@ -41,56 +41,47 @@ impl FileAssociations { · 40 unmodified lines
4141 })
4242 }
4343
44− pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
45− maybe!({
46− let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
44+ pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
45+ let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
4746
48− // FIXME: Associate a type with the languages and have the file's langauge
49− // override these associations
50− maybe!({
51− let suffix = path.icon_suffix()?;
47+ // FIXME: Associate a type with the languages and have the file's langauge
48+ // override these associations
49+ maybe!({
50+ let suffix = path.icon_suffix()?;
5251
53− this.suffixes
54− .get(suffix)
55− .and_then(|type_str| this.types.get(type_str))
56− .map(|type_config| type_config.icon.clone())
57− })
58− .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
52+ this.suffixes
53+ .get(suffix)
54+ .and_then(|type_str| this.types.get(type_str))
55+ .map(|type_config| type_config.icon.clone())
5956 })
60− .unwrap_or_else(|| Arc::from("".to_string()))
57+ .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
6158 }
6259
63− pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
64− maybe!({
65− let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
60+ pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
61+ let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
6662
67− let key = if expanded {
68− EXPANDED_DIRECTORY_TYPE
69− } else {
70− COLLAPSED_DIRECTORY_TYPE
71− };
63+ let key = if expanded {
64+ EXPANDED_DIRECTORY_TYPE
65+ } else {
66+ COLLAPSED_DIRECTORY_TYPE
67+ };
7268
73− this.types
74− .get(key)
75− .map(|type_config| type_config.icon.clone())
76− })
77− .unwrap_or_else(|| Arc::from("".to_string()))
69+ this.types
70+ .get(key)
71+ .map(|type_config| type_config.icon.clone())
7872 }
7973
80− pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
81− maybe!({
82− let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
74+ pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
75+ let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
8376
84− let key = if expanded {
85− EXPANDED_CHEVRON_TYPE
86− } else {
87− COLLAPSED_CHEVRON_TYPE
88− };
77+ let key = if expanded {
78+ EXPANDED_CHEVRON_TYPE
79+ } else {
80+ COLLAPSED_CHEVRON_TYPE
81+ };
8982
90− this.types
91− .get(key)
92− .map(|type_config| type_config.icon.clone())
93− })
94− .unwrap_or_else(|| Arc::from("".to_string()))
83+ this.types
84+ .get(key)
85+ .map(|type_config| type_config.icon.clone())
9586 }
9687 }
crates/project_panel2/src/project_panel.rs+3−3
@@ -1283,16 +1283,16 @@ impl ProjectPanel { · 1282 unmodified lines
12831283 let icon = match entry.kind {
12841284 EntryKind::File(_) => {
12851285 if show_file_icons {
1286− Some(FileAssociations::get_icon(&entry.path, cx))
1286+ FileAssociations::get_icon(&entry.path, cx)
12871287 } else {
12881288 None
12891289 }
12901290 }
12911291 _ => {
12921292 if show_folder_icons {
1293− Some(FileAssociations::get_folder_icon(is_expanded, cx))
1293+ FileAssociations::get_folder_icon(is_expanded, cx)
12941294 } else {
1295− Some(FileAssociations::get_chevron_icon(is_expanded, cx))
1295+ FileAssociations::get_chevron_icon(is_expanded, cx)
12961296 }
12971297 }
12981298 };
crates/search2/src/search_bar.rs+3−3
@@ -1,4 +1,4 @@
1−use gpui::{IntoElement, MouseDownEvent, WindowContext};
1+use gpui::{ClickEvent, IntoElement, WindowContext};
22 use ui::{Button, ButtonVariant, IconButton};
33
44 use crate::mode::SearchMode;
@@ -6,7 +6,7 @@ use crate::mode::SearchMode; · 1 unmodified line
66 pub(super) fn render_nav_button(
77 icon: ui::Icon,
88 _active: bool,
9− on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
9+ on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
1010 ) -> impl IntoElement {
1111 // let tooltip_style = cx.theme().tooltip.clone();
1212 // let cursor_style = if active {
@@ -21,7 +21,7 @@ pub(super) fn render_nav_button( · 8 unmodified lines
2121 pub(crate) fn render_search_mode_button(
2222 mode: SearchMode,
2323 is_active: bool,
24− on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
24+ on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
2525 ) -> Button {
2626 let button_variant = if is_active {
2727 ButtonVariant::Filled
crates/storybook2/src/stories/focus.rs+1−1
@@ -2,7 +2,7 @@ use gpui::{ · 1 unmodified line
22 actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
33 WindowContext,
44 };
5−use theme2::ActiveTheme;
5+use ui::prelude::*;
66
77 actions!(ActionA, ActionB, ActionC);
88
crates/storybook2/src/stories/picker.rs+1−1
@@ -4,7 +4,7 @@ use gpui::{ · 3 unmodified lines
44 };
55 use picker::{Picker, PickerDelegate};
66 use std::sync::Arc;
7−use theme2::ActiveTheme;
7+use ui::prelude::*;
88 use ui::{Label, ListItem};
99
1010 pub struct PickerStory {
crates/storybook2/src/stories/scroll.rs+1−1
@@ -1,5 +1,5 @@
11 use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
2−use theme2::ActiveTheme;
2+use ui::prelude::*;
33 use ui::Tooltip;
44
55 pub struct ScrollStory;
crates/storybook2/src/story_selector.rs+0−2
@@ -19,7 +19,6 @@ pub enum ComponentStory { · 18 unmodified lines
1919 Focus,
2020 Icon,
2121 IconButton,
22− Input,
2322 Keybinding,
2423 Label,
2524 ListItem,
@@ -39,7 +38,6 @@ impl ComponentStory { · 13 unmodified lines
3938 Self::Focus => FocusStory::view(cx).into(),
4039 Self::Icon => cx.build_view(|_| ui::IconStory).into(),
4140 Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
42− Self::Input => cx.build_view(|_| ui::InputStory).into(),
4341 Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
4442 Self::Label => cx.build_view(|_| ui::LabelStory).into(),
4543 Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
crates/theme2/src/registry.rs+4−0
@@ -86,6 +86,10 @@ impl ThemeRegistry { · 85 unmodified lines
8686 }));
8787 }
8888
89+ pub fn clear(&mut self) {
90+ self.themes.clear();
91+ }
92+
8993 pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
9094 self.themes.keys().cloned()
9195 }
crates/theme_selector2/Cargo.toml+29−0
@@ -0,0 +1,29 @@
1+[package]
2+name = "theme_selector2"
3+version = "0.1.0"
4+edition = "2021"
5+publish = false
6+
7+[lib]
8+path = "src/theme_selector.rs"
9+doctest = false
10+
11+[dependencies]
12+editor = { package = "editor2", path = "../editor2" }
13+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
14+fs = { package = "fs2", path = "../fs2" }
15+gpui = { package = "gpui2", path = "../gpui2" }
16+ui = { package = "ui2", path = "../ui2" }
17+picker = { package = "picker2", path = "../picker2" }
18+theme = { package = "theme2", path = "../theme2" }
19+settings = { package = "settings2", path = "../settings2" }
20+feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
21+workspace = { package = "workspace2", path = "../workspace2" }
22+util = { path = "../util" }
23+log.workspace = true
24+parking_lot.workspace = true
25+postage.workspace = true
26+smol.workspace = true
27+
28+[dev-dependencies]
29+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
crates/theme_selector2/src/theme_selector.rs+276−0
@@ -0,0 +1,276 @@
1+use feature_flags::FeatureFlagAppExt;
2+use fs::Fs;
3+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
4+use gpui::{
5+ actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render,
6+ SharedString, View, ViewContext, VisualContext, WeakView,
7+};
8+use picker::{Picker, PickerDelegate};
9+use settings::{update_settings_file, SettingsStore};
10+use std::sync::Arc;
11+use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
12+use ui::ListItem;
13+use util::ResultExt;
14+use workspace::{ui::HighlightedLabel, Workspace};
15+
16+actions!(Toggle, Reload);
17+
18+pub fn init(cx: &mut AppContext) {
19+ cx.observe_new_views(
20+ |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
21+ workspace.register_action(toggle);
22+ },
23+ )
24+ .detach();
25+}
26+
27+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
28+ let fs = workspace.app_state().fs.clone();
29+ workspace.toggle_modal(cx, |cx| {
30+ ThemeSelector::new(
31+ ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx),
32+ cx,
33+ )
34+ });
35+}
36+
37+#[cfg(debug_assertions)]
38+pub fn reload(cx: &mut AppContext) {
39+ let current_theme_name = cx.theme().name.clone();
40+ let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
41+ registry.clear();
42+ registry.get(&current_theme_name)
43+ });
44+ match current_theme {
45+ Ok(theme) => {
46+ ThemeSelectorDelegate::set_theme(theme, cx);
47+ log::info!("reloaded theme {}", current_theme_name);
48+ }
49+ Err(error) => {
50+ log::error!("failed to load theme {}: {:?}", current_theme_name, error)
51+ }
52+ }
53+}
54+
55+pub struct ThemeSelector {
56+ picker: View<Picker<ThemeSelectorDelegate>>,
57+}
58+
59+impl EventEmitter<DismissEvent> for ThemeSelector {}
60+
61+impl FocusableView for ThemeSelector {
62+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
63+ self.picker.focus_handle(cx)
64+ }
65+}
66+
67+impl Render for ThemeSelector {
68+ type Element = View<Picker<ThemeSelectorDelegate>>;
69+
70+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
71+ self.picker.clone()
72+ }
73+}
74+
75+impl ThemeSelector {
76+ pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
77+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
78+ Self { picker }
79+ }
80+}
81+
82+pub struct ThemeSelectorDelegate {
83+ fs: Arc<dyn Fs>,
84+ theme_names: Vec<SharedString>,
85+ matches: Vec<StringMatch>,
86+ original_theme: Arc<Theme>,
87+ selection_completed: bool,
88+ selected_index: usize,
89+ view: WeakView<ThemeSelector>,
90+}
91+
92+impl ThemeSelectorDelegate {
93+ fn new(
94+ weak_view: WeakView<ThemeSelector>,
95+ fs: Arc<dyn Fs>,
96+ cx: &mut ViewContext<ThemeSelector>,
97+ ) -> Self {
98+ let original_theme = cx.theme().clone();
99+
100+ let staff_mode = cx.is_staff();
101+ let registry = cx.global::<Arc<ThemeRegistry>>();
102+ let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
103+ //todo!(theme sorting)
104+ // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
105+ let matches = theme_names
106+ .iter()
107+ .map(|meta| StringMatch {
108+ candidate_id: 0,
109+ score: 0.0,
110+ positions: Default::default(),
111+ string: meta.to_string(),
112+ })
113+ .collect();
114+ let mut this = Self {
115+ fs,
116+ theme_names,
117+ matches,
118+ original_theme: original_theme.clone(),
119+ selected_index: 0,
120+ selection_completed: false,
121+ view: weak_view,
122+ };
123+ this.select_if_matching(&original_theme.name);
124+ this
125+ }
126+
127+ fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
128+ if let Some(mat) = self.matches.get(self.selected_index) {
129+ let registry = cx.global::<Arc<ThemeRegistry>>();
130+ match registry.get(&mat.string) {
131+ Ok(theme) => {
132+ Self::set_theme(theme, cx);
133+ }
134+ Err(error) => {
135+ log::error!("error loading theme {}: {}", mat.string, error)
136+ }
137+ }
138+ }
139+ }
140+
141+ fn select_if_matching(&mut self, theme_name: &str) {
142+ self.selected_index = self
143+ .matches
144+ .iter()
145+ .position(|mat| mat.string == theme_name)
146+ .unwrap_or(self.selected_index);
147+ }
148+
149+ fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
150+ cx.update_global(|store: &mut SettingsStore, cx| {
151+ let mut theme_settings = store.get::<ThemeSettings>(None).clone();
152+ theme_settings.active_theme = theme;
153+ store.override_global(theme_settings);
154+ cx.refresh();
155+ });
156+ }
157+}
158+
159+impl PickerDelegate for ThemeSelectorDelegate {
160+ type ListItem = ui::ListItem;
161+
162+ fn placeholder_text(&self) -> Arc<str> {
163+ "Select Theme...".into()
164+ }
165+
166+ fn match_count(&self) -> usize {
167+ self.matches.len()
168+ }
169+
170+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
171+ self.selection_completed = true;
172+
173+ let theme_name = cx.theme().name.clone();
174+ update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
175+ settings.theme = Some(theme_name.to_string());
176+ });
177+
178+ self.view
179+ .update(cx, |_, cx| {
180+ cx.emit(DismissEvent);
181+ })
182+ .ok();
183+ }
184+
185+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
186+ if !self.selection_completed {
187+ Self::set_theme(self.original_theme.clone(), cx);
188+ self.selection_completed = true;
189+ }
190+ }
191+
192+ fn selected_index(&self) -> usize {
193+ self.selected_index
194+ }
195+
196+ fn set_selected_index(
197+ &mut self,
198+ ix: usize,
199+ cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
200+ ) {
201+ self.selected_index = ix;
202+ self.show_selected_theme(cx);
203+ }
204+
205+ fn update_matches(
206+ &mut self,
207+ query: String,
208+ cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
209+ ) -> gpui::Task<()> {
210+ let background = cx.background_executor().clone();
211+ let candidates = self
212+ .theme_names
213+ .iter()
214+ .enumerate()
215+ .map(|(id, meta)| StringMatchCandidate {
216+ id,
217+ char_bag: meta.as_ref().into(),
218+ string: meta.to_string(),
219+ })
220+ .collect::<Vec<_>>();
221+
222+ cx.spawn(|this, mut cx| async move {
223+ let matches = if query.is_empty() {
224+ candidates
225+ .into_iter()
226+ .enumerate()
227+ .map(|(index, candidate)| StringMatch {
228+ candidate_id: index,
229+ string: candidate.string,
230+ positions: Vec::new(),
231+ score: 0.0,
232+ })
233+ .collect()
234+ } else {
235+ match_strings(
236+ &candidates,
237+ &query,
238+ false,
239+ 100,
240+ &Default::default(),
241+ background,
242+ )
243+ .await
244+ };
245+
246+ this.update(&mut cx, |this, cx| {
247+ this.delegate.matches = matches;
248+ this.delegate.selected_index = this
249+ .delegate
250+ .selected_index
251+ .min(this.delegate.matches.len().saturating_sub(1));
252+ this.delegate.show_selected_theme(cx);
253+ })
254+ .log_err();
255+ })
256+ }
257+
258+ fn render_match(
259+ &self,
260+ ix: usize,
261+ selected: bool,
262+ _cx: &mut ViewContext<Picker<Self>>,
263+ ) -> Option<Self::ListItem> {
264+ let theme_match = &self.matches[ix];
265+
266+ Some(
267+ ListItem::new(ix)
268+ .inset(true)
269+ .selected(selected)
270+ .child(HighlightedLabel::new(
271+ theme_match.string.clone(),
272+ theme_match.positions.clone(),
273+ )),
274+ )
275+ }
276+}
crates/ui2/src/clickable.rs+5−0
@@ -0,0 +1,5 @@
1+use gpui::{ClickEvent, WindowContext};
2+
3+pub trait Clickable {
4+ fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
5+}
crates/ui2/src/components.rs+2−2
@@ -1,12 +1,12 @@
11 mod avatar;
22 mod button;
3+mod button2;
34 mod checkbox;
45 mod context_menu;
56 mod disclosure;
67 mod divider;
78 mod icon;
89 mod icon_button;
9−mod input;
1010 mod keybinding;
1111 mod label;
1212 mod list;
@@ -21,13 +21,13 @@ mod stories; · 8 unmodified lines
2121
2222 pub use avatar::*;
2323 pub use button::*;
24+pub use button2::*;
2425 pub use checkbox::*;
2526 pub use context_menu::*;
2627 pub use disclosure::*;
2728 pub use divider::*;
2829 pub use icon::*;
2930 pub use icon_button::*;
30−pub use input::*;
3131 pub use keybinding::*;
3232 pub use label::*;
3333 pub use list::*;
crates/ui2/src/components/button.rs+5−10
@@ -1,9 +1,7 @@
1−use std::rc::Rc;
2−
31 use gpui::{
4− DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
5− StatefulInteractiveElement, WindowContext,
2+ ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
63 };
4+use std::rc::Rc;
75
86 use crate::prelude::*;
97 use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
@@ -67,7 +65,7 @@ impl ButtonVariant { · 57 unmodified lines
6765 #[derive(IntoElement)]
6866 pub struct Button {
6967 disabled: bool,
70− click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
68+ click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
7169 icon: Option<Icon>,
7270 icon_position: Option<IconPosition>,
7371 label: SharedString,
@@ -118,7 +116,7 @@ impl RenderOnce for Button { · 44 unmodified lines
118116 }
119117
120118 if let Some(click_handler) = self.click_handler.clone() {
121− button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
119+ button = button.on_click(move |event, cx| {
122120 click_handler(event, cx);
123121 });
124122 }
@@ -168,10 +166,7 @@ impl Button { · 43 unmodified lines
168166 self
169167 }
170168
171− pub fn on_click(
172− mut self,
173− handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
174− ) -> Self {
169+ pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
175170 self.click_handler = Some(Rc::new(handler));
176171 self
177172 }
crates/ui2/src/components/button2.rs+413−0
@@ -0,0 +1,413 @@
1+use gpui::{
2+ rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
3+ StatefulInteractiveElement, WindowContext,
4+};
5+use smallvec::SmallVec;
6+
7+use crate::{h_stack, prelude::*};
8+
9+// 🚧 Heavily WIP 🚧
10+
11+// #[derive(Default, PartialEq, Clone, Copy)]
12+// pub enum ButtonType2 {
13+// #[default]
14+// DefaultButton,
15+// IconButton,
16+// ButtonLike,
17+// SplitButton,
18+// ToggleButton,
19+// }
20+
21+#[derive(Default, PartialEq, Clone, Copy)]
22+pub enum IconPosition2 {
23+ #[default]
24+ Before,
25+ After,
26+}
27+
28+#[derive(Default, PartialEq, Clone, Copy)]
29+pub enum ButtonStyle2 {
30+ #[default]
31+ Filled,
32+ // Tinted,
33+ Subtle,
34+ Transparent,
35+}
36+
37+#[derive(Debug, Clone, Copy)]
38+pub struct ButtonStyle {
39+ pub background: Hsla,
40+ pub border_color: Hsla,
41+ pub label_color: Hsla,
42+ pub icon_color: Hsla,
43+}
44+
45+impl ButtonStyle2 {
46+ pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
47+ match self {
48+ ButtonStyle2::Filled => ButtonStyle {
49+ background: cx.theme().colors().element_background,
50+ border_color: gpui::transparent_black(),
51+ label_color: Color::Default.color(cx),
52+ icon_color: Color::Default.color(cx),
53+ },
54+ ButtonStyle2::Subtle => ButtonStyle {
55+ background: cx.theme().colors().ghost_element_background,
56+ border_color: gpui::transparent_black(),
57+ label_color: Color::Default.color(cx),
58+ icon_color: Color::Default.color(cx),
59+ },
60+ ButtonStyle2::Transparent => ButtonStyle {
61+ background: gpui::transparent_black(),
62+ border_color: gpui::transparent_black(),
63+ label_color: Color::Default.color(cx),
64+ icon_color: Color::Default.color(cx),
65+ },
66+ }
67+ }
68+
69+ pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
70+ match self {
71+ ButtonStyle2::Filled => ButtonStyle {
72+ background: cx.theme().colors().element_hover,
73+ border_color: gpui::transparent_black(),
74+ label_color: Color::Default.color(cx),
75+ icon_color: Color::Default.color(cx),
76+ },
77+ ButtonStyle2::Subtle => ButtonStyle {
78+ background: cx.theme().colors().ghost_element_hover,
79+ border_color: gpui::transparent_black(),
80+ label_color: Color::Default.color(cx),
81+ icon_color: Color::Default.color(cx),
82+ },
83+ ButtonStyle2::Transparent => ButtonStyle {
84+ background: gpui::transparent_black(),
85+ border_color: gpui::transparent_black(),
86+ // TODO: These are not great
87+ label_color: Color::Muted.color(cx),
88+ // TODO: These are not great
89+ icon_color: Color::Muted.color(cx),
90+ },
91+ }
92+ }
93+
94+ pub fn active(self, cx: &mut WindowContext) -> ButtonStyle {
95+ match self {
96+ ButtonStyle2::Filled => ButtonStyle {
97+ background: cx.theme().colors().element_active,
98+ border_color: gpui::transparent_black(),
99+ label_color: Color::Default.color(cx),
100+ icon_color: Color::Default.color(cx),
101+ },
102+ ButtonStyle2::Subtle => ButtonStyle {
103+ background: cx.theme().colors().ghost_element_active,
104+ border_color: gpui::transparent_black(),
105+ label_color: Color::Default.color(cx),
106+ icon_color: Color::Default.color(cx),
107+ },
108+ ButtonStyle2::Transparent => ButtonStyle {
109+ background: gpui::transparent_black(),
110+ border_color: gpui::transparent_black(),
111+ // TODO: These are not great
112+ label_color: Color::Muted.color(cx),
113+ // TODO: These are not great
114+ icon_color: Color::Muted.color(cx),
115+ },
116+ }
117+ }
118+
119+ pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
120+ match self {
121+ ButtonStyle2::Filled => ButtonStyle {
122+ background: cx.theme().colors().element_background,
123+ border_color: cx.theme().colors().border_focused,
124+ label_color: Color::Default.color(cx),
125+ icon_color: Color::Default.color(cx),
126+ },
127+ ButtonStyle2::Subtle => ButtonStyle {
128+ background: cx.theme().colors().ghost_element_background,
129+ border_color: cx.theme().colors().border_focused,
130+ label_color: Color::Default.color(cx),
131+ icon_color: Color::Default.color(cx),
132+ },
133+ ButtonStyle2::Transparent => ButtonStyle {
134+ background: gpui::transparent_black(),
135+ border_color: cx.theme().colors().border_focused,
136+ label_color: Color::Accent.color(cx),
137+ icon_color: Color::Accent.color(cx),
138+ },
139+ }
140+ }
141+
142+ pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
143+ match self {
144+ ButtonStyle2::Filled => ButtonStyle {
145+ background: cx.theme().colors().element_disabled,
146+ border_color: cx.theme().colors().border_disabled,
147+ label_color: Color::Disabled.color(cx),
148+ icon_color: Color::Disabled.color(cx),
149+ },
150+ ButtonStyle2::Subtle => ButtonStyle {
151+ background: cx.theme().colors().ghost_element_disabled,
152+ border_color: cx.theme().colors().border_disabled,
153+ label_color: Color::Disabled.color(cx),
154+ icon_color: Color::Disabled.color(cx),
155+ },
156+ ButtonStyle2::Transparent => ButtonStyle {
157+ background: gpui::transparent_black(),
158+ border_color: gpui::transparent_black(),
159+ label_color: Color::Disabled.color(cx),
160+ icon_color: Color::Disabled.color(cx),
161+ },
162+ }
163+ }
164+}
165+
166+#[derive(Default, PartialEq, Clone, Copy)]
167+pub enum ButtonSize2 {
168+ #[default]
169+ Default,
170+ Compact,
171+ None,
172+}
173+
174+impl ButtonSize2 {
175+ fn height(self) -> Rems {
176+ match self {
177+ ButtonSize2::Default => rems(22. / 16.),
178+ ButtonSize2::Compact => rems(18. / 16.),
179+ ButtonSize2::None => rems(16. / 16.),
180+ }
181+ }
182+}
183+
184+// pub struct Button {
185+// id: ElementId,
186+// icon: Option<Icon>,
187+// icon_color: Option<Color>,
188+// icon_position: Option<IconPosition2>,
189+// label: Option<Label>,
190+// label_color: Option<Color>,
191+// appearance: ButtonAppearance2,
192+// state: InteractionState,
193+// selected: bool,
194+// disabled: bool,
195+// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
196+// width: Option<DefiniteLength>,
197+// action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
198+// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
199+// /// Used to pass down some content to the button
200+// /// to enable creating custom buttons.
201+// children: SmallVec<[AnyElement; 2]>,
202+// }
203+
204+pub trait ButtonCommon: Clickable {
205+ fn id(&self) -> &ElementId;
206+ fn style(self, style: ButtonStyle2) -> Self;
207+ fn disabled(self, disabled: bool) -> Self;
208+ fn size(self, size: ButtonSize2) -> Self;
209+ fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
210+ // fn width(&mut self, width: DefiniteLength) -> &mut Self;
211+}
212+
213+// pub struct LabelButton {
214+// // Base properties...
215+// id: ElementId,
216+// appearance: ButtonAppearance,
217+// state: InteractionState,
218+// disabled: bool,
219+// size: ButtonSize,
220+// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
221+// width: Option<DefiniteLength>,
222+// // Button-specific properties...
223+// label: Option<SharedString>,
224+// label_color: Option<Color>,
225+// icon: Option<Icon>,
226+// icon_color: Option<Color>,
227+// icon_position: Option<IconPosition>,
228+// // Define more fields for additional properties as needed
229+// }
230+
231+// impl ButtonCommon for LabelButton {
232+// fn id(&self) -> &ElementId {
233+// &self.id
234+// }
235+
236+// fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self {
237+// self.style= style;
238+// self
239+// }
240+// // implement methods from ButtonCommon trait...
241+// }
242+
243+// impl LabelButton {
244+// pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
245+// Self {
246+// id: id.into(),
247+// label: Some(label.into()),
248+// // initialize other fields with default values...
249+// }
250+// }
251+
252+// // ... Define other builder methods specific to Button type...
253+// }
254+
255+// TODO: Icon Button
256+
257+#[derive(IntoElement)]
258+pub struct ButtonLike {
259+ id: ElementId,
260+ style: ButtonStyle2,
261+ disabled: bool,
262+ size: ButtonSize2,
263+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
264+ on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
265+ children: SmallVec<[AnyElement; 2]>,
266+}
267+
268+impl ButtonLike {
269+ pub fn children(
270+ &mut self,
271+ children: impl IntoIterator<Item = impl Into<AnyElement>>,
272+ ) -> &mut Self {
273+ self.children = children.into_iter().map(Into::into).collect();
274+ self
275+ }
276+
277+ pub fn new(id: impl Into<ElementId>) -> Self {
278+ Self {
279+ id: id.into(),
280+ style: ButtonStyle2::default(),
281+ disabled: false,
282+ size: ButtonSize2::Default,
283+ tooltip: None,
284+ children: SmallVec::new(),
285+ on_click: None,
286+ }
287+ }
288+}
289+
290+impl Clickable for ButtonLike {
291+ fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
292+ self.on_click = Some(Box::new(handler));
293+ self
294+ }
295+}
296+
297+// impl Selectable for ButtonLike {
298+// fn selected(&mut self, selected: bool) -> &mut Self {
299+// todo!()
300+// }
301+
302+// fn selected_tooltip(
303+// &mut self,
304+// tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
305+// ) -> &mut Self {
306+// todo!()
307+// }
308+// }
309+
310+impl ButtonCommon for ButtonLike {
311+ fn id(&self) -> &ElementId {
312+ &self.id
313+ }
314+
315+ fn style(mut self, style: ButtonStyle2) -> Self {
316+ self.style = style;
317+ self
318+ }
319+
320+ fn disabled(mut self, disabled: bool) -> Self {
321+ self.disabled = disabled;
322+ self
323+ }
324+
325+ fn size(mut self, size: ButtonSize2) -> Self {
326+ self.size = size;
327+ self
328+ }
329+
330+ fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
331+ self.tooltip = Some(Box::new(tooltip));
332+ self
333+ }
334+}
335+
336+impl RenderOnce for ButtonLike {
337+ type Rendered = Stateful<Div>;
338+
339+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
340+ h_stack()
341+ .id(self.id.clone())
342+ .h(self.size.height())
343+ .rounded_md()
344+ .cursor_pointer()
345+ .gap_1()
346+ .px_1()
347+ .bg(self.style.enabled(cx).background)
348+ .hover(|hover| hover.bg(self.style.hovered(cx).background))
349+ .active(|active| active.bg(self.style.active(cx).background))
350+ .when_some(
351+ self.on_click.filter(|_| !self.disabled),
352+ |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)),
353+ )
354+ .when_some(self.tooltip, |this, tooltip| {
355+ this.tooltip(move |cx| tooltip(cx))
356+ })
357+ .children(self.children)
358+ }
359+}
360+
361+impl ParentElement for ButtonLike {
362+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
363+ &mut self.children
364+ }
365+}
366+
367+// pub struct ToggleButton {
368+// // based on either IconButton2 or Button, with additional 'selected: bool' property
369+// }
370+
371+// impl ButtonCommon for ToggleButton {
372+// fn id(&self) -> &ElementId {
373+// &self.id
374+// }
375+// // ... Implement other methods from ButtonCommon trait with builder patterns...
376+// }
377+
378+// impl ToggleButton {
379+// pub fn new() -> Self {
380+// // Initialize with default values
381+// Self {
382+// // ... initialize fields, possibly with defaults or required parameters...
383+// }
384+// }
385+
386+// // ... Define other builder methods specific to ToggleButton type...
387+// }
388+
389+// pub struct SplitButton {
390+// // Base properties...
391+// id: ElementId,
392+// // Button-specific properties, possibly including a DefaultButton
393+// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
394+// // More fields as necessary...
395+// }
396+
397+// impl ButtonCommon for SplitButton {
398+// fn id(&self) -> &ElementId {
399+// &self.id
400+// }
401+// // ... Implement other methods from ButtonCommon trait with builder patterns...
402+// }
403+
404+// impl SplitButton {
405+// pub fn new(id: impl Into<ElementId>) -> Self {
406+// Self {
407+// id: id.into(),
408+// // ... initialize other fields with default values...
409+// }
410+// }
411+
412+// // ... Define other builder methods specific to SplitButton type...
413+// }
crates/ui2/src/components/disclosure.rs+18−7
@@ -1,19 +1,30 @@
1−use gpui::{div, Element, ParentElement};
1+use std::rc::Rc;
22
3−use crate::{Color, Icon, IconElement, IconSize, Toggle};
3+use gpui::{div, ClickEvent, Element, IntoElement, ParentElement, WindowContext};
44
5−pub fn disclosure_control(toggle: Toggle) -> impl Element {
5+use crate::{Color, Icon, IconButton, IconSize, Toggle};
6+
7+pub fn disclosure_control(
8+ toggle: Toggle,
9+ on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
10+) -> impl Element {
611 match (toggle.is_toggleable(), toggle.is_toggled()) {
712 (false, _) => div(),
813 (_, true) => div().child(
9− IconElement::new(Icon::ChevronDown)
14+ IconButton::new("toggle", Icon::ChevronDown)
1015 .color(Color::Muted)
11− .size(IconSize::Small),
16+ .size(IconSize::Small)
17+ .when_some(on_toggle, move |el, on_toggle| {
18+ el.on_click(move |e, cx| on_toggle(e, cx))
19+ }),
1220 ),
1321 (_, false) => div().child(
14− IconElement::new(Icon::ChevronRight)
22+ IconButton::new("toggle", Icon::ChevronRight)
1523 .color(Color::Muted)
16− .size(IconSize::Small),
24+ .size(IconSize::Small)
25+ .when_some(on_toggle, move |el, on_toggle| {
26+ el.on_click(move |e, cx| on_toggle(e, cx))
27+ }),
1728 ),
1829 }
1930 }
crates/ui2/src/components/icon_button.rs+31−24
@@ -1,25 +1,26 @@
1−use crate::{h_stack, prelude::*, Icon, IconElement};
2−use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
1+use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
2+use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
33
44 #[derive(IntoElement)]
55 pub struct IconButton {
66 id: ElementId,
77 icon: Icon,
88 color: Color,
9+ size: IconSize,
910 variant: ButtonVariant,
10− state: InteractionState,
11+ disabled: bool,
1112 selected: bool,
1213 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
13− on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
14+ on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
1415 }
1516
1617 impl RenderOnce for IconButton {
1718 type Rendered = Stateful<Div>;
1819
1920 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
20− let icon_color = match (self.state, self.color) {
21− (InteractionState::Disabled, _) => Color::Disabled,
22− (InteractionState::Active, _) => Color::Selected,
21+ let icon_color = match (self.disabled, self.selected, self.color) {
22+ (true, _, _) => Color::Disabled,
23+ (false, true, _) => Color::Selected,
2324 _ => self.color,
2425 };
2526
@@ -50,10 +51,14 @@ impl RenderOnce for IconButton { · 24 unmodified lines
5051 // place we use an icon button.
5152 // .hover(|style| style.bg(bg_hover_color))
5253 .active(|style| style.bg(bg_active_color))
53− .child(IconElement::new(self.icon).color(icon_color));
54−
55− if let Some(click_handler) = self.on_mouse_down {
56− button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
54+ .child(
55+ IconElement::new(self.icon)
56+ .size(self.size)
57+ .color(icon_color),
58+ );
59+
60+ if let Some(click_handler) = self.on_click {
61+ button = button.on_click(move |event, cx| {
5762 cx.stop_propagation();
5863 click_handler(event, cx);
5964 })
@@ -65,8 +70,7 @@ impl RenderOnce for IconButton { · 5 unmodified lines
6570 }
6671 }
6772
68− // HACK: Add an additional identified element wrapper to fix tooltips not showing up.
69− div().id(self.id.clone()).child(button)
73+ button
7074 }
7175 }
7276
@@ -76,11 +80,12 @@ impl IconButton { · 3 unmodified lines
7680 id: id.into(),
7781 icon,
7882 color: Color::default(),
83+ size: Default::default(),
7984 variant: ButtonVariant::default(),
80− state: InteractionState::default(),
8185 selected: false,
86+ disabled: false,
8287 tooltip: None,
83− on_mouse_down: None,
88+ on_click: None,
8489 }
8590 }
8691
@@ -94,13 +99,13 @@ impl IconButton { · 7 unmodified lines
9499 self
95100 }
96101
97− pub fn variant(mut self, variant: ButtonVariant) -> Self {
98− self.variant = variant;
102+ pub fn size(mut self, size: IconSize) -> Self {
103+ self.size = size;
99104 self
100105 }
101106
102− pub fn state(mut self, state: InteractionState) -> Self {
103− self.state = state;
107+ pub fn variant(mut self, variant: ButtonVariant) -> Self {
108+ self.variant = variant;
104109 self
105110 }
106111
@@ -109,16 +114,18 @@ impl IconButton { · 2 unmodified lines
109114 self
110115 }
111116
117+ pub fn disabled(mut self, disabled: bool) -> Self {
118+ self.disabled = disabled;
119+ self
120+ }
121+
112122 pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
113123 self.tooltip = Some(Box::new(tooltip));
114124 self
115125 }
116126
117− pub fn on_click(
118− mut self,
119− handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
120− ) -> Self {
121− self.on_mouse_down = Some(Box::new(handler));
127+ pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self {
128+ self.on_click = Some(Box::new(handler));
122129 self
123130 }
124131
crates/ui2/src/components/input.rs+0−108
@@ -1,108 +0,0 @@
1−use crate::{prelude::*, Label};
2−use gpui::{prelude::*, Div, IntoElement, Stateful};
3−
4−#[derive(Default, PartialEq)]
5−pub enum InputVariant {
6− #[default]
7− Ghost,
8− Filled,
9−}
10−
11−#[derive(IntoElement)]
12−pub struct Input {
13− placeholder: SharedString,
14− value: String,
15− state: InteractionState,
16− variant: InputVariant,
17− disabled: bool,
18− is_active: bool,
19−}
20−
21−impl RenderOnce for Input {
22− type Rendered = Stateful<Div>;
23−
24− fn render(self, cx: &mut WindowContext) -> Self::Rendered {
25− let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
26− InputVariant::Ghost => (
27− cx.theme().colors().ghost_element_background,
28− cx.theme().colors().ghost_element_hover,
29− cx.theme().colors().ghost_element_active,
30− ),
31− InputVariant::Filled => (
32− cx.theme().colors().element_background,
33− cx.theme().colors().element_hover,
34− cx.theme().colors().element_active,
35− ),
36− };
37−
38− let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
39− Color::Disabled
40− } else {
41− Color::Placeholder
42− });
43−
44− let label = Label::new(self.value.clone()).color(if self.disabled {
45− Color::Disabled
46− } else {
47− Color::Default
48− });
49−
50− div()
51− .id("input")
52− .h_7()
53− .w_full()
54− .px_2()
55− .border()
56− .border_color(cx.theme().styles.system.transparent)
57− .bg(input_bg)
58− .hover(|style| style.bg(input_hover_bg))
59− .active(|style| style.bg(input_active_bg))
60− .flex()
61− .items_center()
62− .child(div().flex().items_center().text_ui_sm().map(move |this| {
63− if self.value.is_empty() {
64− this.child(placeholder_label)
65− } else {
66− this.child(label)
67− }
68− }))
69− }
70−}
71−
72−impl Input {
73− pub fn new(placeholder: impl Into<SharedString>) -> Self {
74− Self {
75− placeholder: placeholder.into(),
76− value: "".to_string(),
77− state: InteractionState::default(),
78− variant: InputVariant::default(),
79− disabled: false,
80− is_active: false,
81− }
82− }
83−
84− pub fn value(mut self, value: String) -> Self {
85− self.value = value;
86− self
87− }
88−
89− pub fn state(mut self, state: InteractionState) -> Self {
90− self.state = state;
91− self
92− }
93−
94− pub fn variant(mut self, variant: InputVariant) -> Self {
95− self.variant = variant;
96− self
97− }
98−
99− pub fn disabled(mut self, disabled: bool) -> Self {
100− self.disabled = disabled;
101− self
102− }
103−
104− pub fn is_active(mut self, is_active: bool) -> Self {
105− self.is_active = is_active;
106− self
107− }
108−}
crates/ui2/src/components/list.rs+45−33
@@ -25,7 +25,9 @@ pub struct ListHeader { · 24 unmodified lines
2525 left_icon: Option<Icon>,
2626 meta: Option<ListHeaderMeta>,
2727 toggle: Toggle,
28+ on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
2829 inset: bool,
30+ selected: bool,
2931 }
3032
3133 impl ListHeader {
@@ -36,6 +38,8 @@ impl ListHeader { · 4 unmodified lines
3638 meta: None,
3739 inset: false,
3840 toggle: Toggle::NotToggleable,
41+ on_toggle: None,
42+ selected: false,
3943 }
4044 }
4145
@@ -44,6 +48,14 @@ impl ListHeader { · 2 unmodified lines
4448 self
4549 }
4650
51+ pub fn on_toggle(
52+ mut self,
53+ on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
54+ ) -> Self {
55+ self.on_toggle = Some(Rc::new(on_toggle));
56+ self
57+ }
58+
4759 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
4860 self.left_icon = left_icon;
4961 self
@@ -57,13 +69,18 @@ impl ListHeader { · 7 unmodified lines
5769 self.meta = meta;
5870 self
5971 }
72+
73+ pub fn selected(mut self, selected: bool) -> Self {
74+ self.selected = selected;
75+ self
76+ }
6077 }
6178
6279 impl RenderOnce for ListHeader {
6380 type Rendered = Div;
6481
6582 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
66− let disclosure_control = disclosure_control(self.toggle);
83+ let disclosure_control = disclosure_control(self.toggle, self.on_toggle);
6784
6885 let meta = match self.meta {
6986 Some(ListHeaderMeta::Tools(icons)) => div().child(
@@ -85,6 +102,9 @@ impl RenderOnce for ListHeader { · 15 unmodified lines
85102 div()
86103 .h_5()
87104 .when(self.inset, |this| this.px_2())
105+ .when(self.selected, |this| {
106+ this.bg(cx.theme().colors().ghost_element_selected)
107+ })
88108 .flex()
89109 .flex_1()
90110 .items_center()
@@ -177,6 +197,7 @@ pub struct ListItem { · 86 unmodified lines
177197 toggle: Toggle,
178198 inset: bool,
179199 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
200+ on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
180201 on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
181202 children: SmallVec<[AnyElement; 2]>,
182203 }
@@ -193,6 +214,7 @@ impl ListItem { · 10 unmodified lines
193214 inset: false,
194215 on_click: None,
195216 on_secondary_mouse_down: None,
217+ on_toggle: None,
196218 children: SmallVec::new(),
197219 }
198220 }
@@ -230,6 +252,14 @@ impl ListItem { · 31 unmodified lines
230252 self
231253 }
232254
255+ pub fn on_toggle(
256+ mut self,
257+ on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
258+ ) -> Self {
259+ self.on_toggle = Some(Rc::new(on_toggle));
260+ self
261+ }
262+
233263 pub fn selected(mut self, selected: bool) -> Self {
234264 self.selected = selected;
235265 self
@@ -255,19 +285,6 @@ impl RenderOnce for ListItem { · 19 unmodified lines
255285 type Rendered = Stateful<Div>;
256286
257287 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
258− let left_content = match self.left_slot.clone() {
259− Some(GraphicSlot::Icon(i)) => Some(
260− h_stack().child(
261− IconElement::new(i)
262− .size(IconSize::Small)
263− .color(Color::Muted),
264− ),
265− ),
266− Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
267− Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
268− None => None,
269− };
270−
271288 div()
272289 .id(self.id)
273290 .relative()
@@ -282,8 +299,8 @@ impl RenderOnce for ListItem { · 8 unmodified lines
282299 .when(self.selected, |this| {
283300 this.bg(cx.theme().colors().ghost_element_selected)
284301 })
285− .when_some(self.on_click.clone(), |this, on_click| {
286− this.on_click(move |event, cx| {
302+ .when_some(self.on_click, |this, on_click| {
303+ this.cursor_pointer().on_click(move |event, cx| {
287304 // HACK: GPUI currently fires `on_click` with any mouse button,
288305 // but we only care about the left button.
289306 if event.down.button == MouseButton::Left {
@@ -304,23 +321,18 @@ impl RenderOnce for ListItem { · 14 unmodified lines
304321 .gap_1()
305322 .items_center()
306323 .relative()
307− .child(disclosure_control(self.toggle))
308− .children(left_content)
309− .children(self.children)
310− // HACK: We need to attach the `on_click` handler to the child element in order to have the click
311− // event actually fire.
312− // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
313− // outer `div`.
314− .id("on_click_hack")
315− .when_some(self.on_click, |this, on_click| {
316− this.on_click(move |event, cx| {
317− // HACK: GPUI currently fires `on_click` with any mouse button,
318− // but we only care about the left button.
319− if event.down.button == MouseButton::Left {
320− (on_click)(event, cx)
321− }
322− })
323− }),
324+ .child(disclosure_control(self.toggle, self.on_toggle))
325+ .map(|this| match self.left_slot {
326+ Some(GraphicSlot::Icon(i)) => this.child(
327+ IconElement::new(i)
328+ .size(IconSize::Small)
329+ .color(Color::Muted),
330+ ),
331+ Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)),
332+ Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)),
333+ None => this,
334+ })
335+ .children(self.children),
324336 )
325337 }
326338 }
crates/ui2/src/components/stories.rs+0−3
@@ -4,18 +4,15 @@ mod checkbox; · 3 unmodified lines
44 mod context_menu;
55 mod icon;
66 mod icon_button;
7−mod input;
87 mod keybinding;
98 mod label;
109 mod list_item;
11−
1210 pub use avatar::*;
1311 pub use button::*;
1412 pub use checkbox::*;
1513 pub use context_menu::*;
1614 pub use icon::*;
1715 pub use icon_button::*;
18−pub use input::*;
1916 pub use keybinding::*;
2017 pub use label::*;
2118 pub use list_item::*;
crates/ui2/src/components/stories/button.rs+48−116
@@ -1,9 +1,8 @@
1−use gpui::{rems, Div, Render};
1+use gpui::{Div, Render};
22 use story::Story;
3−use strum::IntoEnumIterator;
43
54 use crate::prelude::*;
6−use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
5+use crate::{h_stack, Button, Icon, IconPosition};
76
87 pub struct ButtonStory;
98
@@ -11,8 +10,6 @@ impl Render for ButtonStory { · 1 unmodified line
1110 type Element = Div;
1211
1312 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
14− let states = InteractionState::iter();
15−
1613 Story::container()
1714 .child(Story::title_for::<Button>())
1815 .child(
@@ -20,121 +17,56 @@ impl Render for ButtonStory { · 1 unmodified line
2017 .flex()
2118 .gap_8()
2219 .child(
23− div()
24− .child(Story::label("Ghost (Default)"))
25− .child(h_stack().gap_2().children(states.clone().map(|state| {
26− v_stack()
27− .gap_1()
28− .child(Label::new(state.to_string()).color(Color::Muted))
29− .child(
30− Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
31− )
32− })))
33− .child(Story::label("Ghost – Left Icon"))
34− .child(h_stack().gap_2().children(states.clone().map(|state| {
35− v_stack()
36− .gap_1()
37− .child(Label::new(state.to_string()).color(Color::Muted))
38− .child(
39− Button::new("Label")
40− .variant(ButtonVariant::Ghost)
41− .icon(Icon::Plus)
42− .icon_position(IconPosition::Left), // .state(state),
43− )
44− })))
45− .child(Story::label("Ghost – Right Icon"))
46− .child(h_stack().gap_2().children(states.clone().map(|state| {
47− v_stack()
48− .gap_1()
49− .child(Label::new(state.to_string()).color(Color::Muted))
50− .child(
51− Button::new("Label")
52− .variant(ButtonVariant::Ghost)
53− .icon(Icon::Plus)
54− .icon_position(IconPosition::Right), // .state(state),
55− )
56− }))),
57− )
58− .child(
59− div()
60− .child(Story::label("Filled"))
61− .child(h_stack().gap_2().children(states.clone().map(|state| {
62− v_stack()
63− .gap_1()
64− .child(Label::new(state.to_string()).color(Color::Muted))
65− .child(
66− Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
67− )
68− })))
69− .child(Story::label("Filled – Left Button"))
70− .child(h_stack().gap_2().children(states.clone().map(|state| {
71− v_stack()
72− .gap_1()
73− .child(Label::new(state.to_string()).color(Color::Muted))
74− .child(
75− Button::new("Label")
76− .variant(ButtonVariant::Filled)
77− .icon(Icon::Plus)
78− .icon_position(IconPosition::Left), // .state(state),
79− )
80− })))
81− .child(Story::label("Filled – Right Button"))
82− .child(h_stack().gap_2().children(states.clone().map(|state| {
83− v_stack()
84− .gap_1()
85− .child(Label::new(state.to_string()).color(Color::Muted))
86− .child(
87− Button::new("Label")
88− .variant(ButtonVariant::Filled)
89− .icon(Icon::Plus)
90− .icon_position(IconPosition::Right), // .state(state),
91− )
92− }))),
20+ div().child(Story::label("Ghost (Default)")).child(
21+ h_stack()
22+ .gap_2()
23+ .child(Button::new("Label").variant(ButtonVariant::Ghost)),
24+ ),
9325 )
26+ .child(Story::label("Ghost – Left Icon"))
9427 .child(
95− div()
96− .child(Story::label("Fixed With"))
97− .child(h_stack().gap_2().children(states.clone().map(|state| {
98− v_stack()
99− .gap_1()
100− .child(Label::new(state.to_string()).color(Color::Muted))
101− .child(
102− Button::new("Label")
103− .variant(ButtonVariant::Filled)
104− // .state(state)
105− .width(Some(rems(6.).into())),
106− )
107− })))
108− .child(Story::label("Fixed With – Left Icon"))
109− .child(h_stack().gap_2().children(states.clone().map(|state| {
110− v_stack()
111− .gap_1()
112− .child(Label::new(state.to_string()).color(Color::Muted))
113− .child(
114− Button::new("Label")
115− .variant(ButtonVariant::Filled)
116− // .state(state)
117− .icon(Icon::Plus)
118− .icon_position(IconPosition::Left)
119− .width(Some(rems(6.).into())),
120− )
121− })))
122− .child(Story::label("Fixed With – Right Icon"))
123− .child(h_stack().gap_2().children(states.clone().map(|state| {
124− v_stack()
125− .gap_1()
126− .child(Label::new(state.to_string()).color(Color::Muted))
127− .child(
128− Button::new("Label")
129− .variant(ButtonVariant::Filled)
130− // .state(state)
131− .icon(Icon::Plus)
132− .icon_position(IconPosition::Right)
133− .width(Some(rems(6.).into())),
134− )
135− }))),
28+ h_stack().gap_2().child(
29+ Button::new("Label")
30+ .variant(ButtonVariant::Ghost)
31+ .icon(Icon::Plus)
32+ .icon_position(IconPosition::Left),
33+ ),
13634 ),
13735 )
36+ .child(Story::label("Ghost – Right Icon"))
37+ .child(
38+ h_stack().gap_2().child(
39+ Button::new("Label")
40+ .variant(ButtonVariant::Ghost)
41+ .icon(Icon::Plus)
42+ .icon_position(IconPosition::Right),
43+ ),
44+ )
45+ .child(
46+ div().child(Story::label("Filled")).child(
47+ h_stack()
48+ .gap_2()
49+ .child(Button::new("Label").variant(ButtonVariant::Filled)),
50+ ),
51+ )
52+ .child(Story::label("Filled – Left Button"))
53+ .child(
54+ h_stack().gap_2().child(
55+ Button::new("Label")
56+ .variant(ButtonVariant::Filled)
57+ .icon(Icon::Plus)
58+ .icon_position(IconPosition::Left),
59+ ),
60+ )
61+ .child(Story::label("Filled – Right Button"))
62+ .child(
63+ h_stack().gap_2().child(
64+ Button::new("Label")
65+ .variant(ButtonVariant::Filled)
66+ .icon(Icon::Plus)
67+ .icon_position(IconPosition::Right),
68+ ),
69+ )
13870 .child(Story::label("Button with `on_click`"))
13971 .child(
14072 Button::new("Label")
crates/ui2/src/components/stories/input.rs+0−18
@@ -1,18 +0,0 @@
1−use gpui::{Div, Render};
2−use story::Story;
3−
4−use crate::prelude::*;
5−use crate::Input;
6−
7−pub struct InputStory;
8−
9−impl Render for InputStory {
10− type Element = Div;
11−
12− fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
13− Story::container()
14− .child(Story::title_for::<Input>())
15− .child(Story::label("Default"))
16− .child(div().flex().child(Input::new("Search")))
17− }
18−}
crates/ui2/src/components/stories/list_item.rs+19−5
@@ -2,7 +2,7 @@ use gpui::{Div, Render}; · 1 unmodified line
22 use story::Story;
33
44 use crate::prelude::*;
5−use crate::ListItem;
5+use crate::{Icon, ListItem};
66
77 pub struct ListItemStory;
88
@@ -14,6 +14,20 @@ impl Render for ListItemStory { · 5 unmodified lines
1414 .child(Story::title_for::<ListItem>())
1515 .child(Story::label("Default"))
1616 .child(ListItem::new("hello_world").child("Hello, world!"))
17+ .child(Story::label("With left icon"))
18+ .child(
19+ ListItem::new("with_left_icon")
20+ .child("Hello, world!")
21+ .left_icon(Icon::Bell),
22+ )
23+ .child(Story::label("With left avatar"))
24+ .child(
25+ ListItem::new("with_left_avatar")
26+ .child("Hello, world!")
27+ .left_avatar(SharedString::from(
28+ "https://avatars.githubusercontent.com/u/1714999?v=4",
29+ )),
30+ )
1731 .child(Story::label("With `on_click`"))
1832 .child(
1933 ListItem::new("with_on_click")
@@ -24,11 +38,11 @@ impl Render for ListItemStory { · 4 unmodified lines
2438 )
2539 .child(Story::label("With `on_secondary_mouse_down`"))
2640 .child(
27− ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
28− |_event, _cx| {
41+ ListItem::new("with_on_secondary_mouse_down")
42+ .child("Right click me")
43+ .on_secondary_mouse_down(|_event, _cx| {
2944 println!("Right mouse down!");
30− },
31− ),
45+ }),
3246 )
3347 }
3448 }
crates/ui2/src/fixed.rs+6−0
@@ -0,0 +1,6 @@
1+use gpui::DefiniteLength;
2+
3+pub trait FixedWidth {
4+ fn width(self, width: DefiniteLength) -> Self;
5+ fn full_width(self) -> Self;
6+}
crates/ui2/src/prelude.rs+3−56
@@ -3,62 +3,9 @@ pub use gpui::{ · 2 unmodified lines
33 ViewContext, WindowContext,
44 };
55
6+pub use crate::clickable::*;
7+pub use crate::fixed::*;
8+pub use crate::selectable::*;
69 pub use crate::StyledExt;
710 pub use crate::{ButtonVariant, Color};
811 pub use theme::ActiveTheme;
9−
10−use strum::EnumIter;
11−
12−#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
13−pub enum IconSide {
14− #[default]
15− Left,
16− Right,
17−}
18−
19−#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
20−pub enum InteractionState {
21− /// An element that is enabled and not hovered, active, focused, or disabled.
22− ///
23− /// This is often referred to as the "default" state.
24− #[default]
25− Enabled,
26− /// An element that is hovered.
27− Hovered,
28− /// An element has an active mouse down or touch start event on it.
29− Active,
30− /// An element that is focused using the keyboard.
31− Focused,
32− /// An element that is disabled.
33− Disabled,
34− /// A toggleable element that is selected, like the active button in a
35− /// button toggle group.
36− Selected,
37−}
38−
39−impl InteractionState {
40− pub fn if_enabled(&self, enabled: bool) -> Self {
41− if enabled {
42− *self
43− } else {
44− InteractionState::Disabled
45− }
46− }
47−}
48−
49−#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
50−pub enum Selection {
51− #[default]
52− Unselected,
53− Indeterminate,
54− Selected,
55−}
56−
57−impl Selection {
58− pub fn inverse(&self) -> Self {
59− match self {
60− Self::Unselected | Self::Indeterminate => Self::Selected,
61− Self::Selected => Self::Unselected,
62− }
63− }
64−}
crates/ui2/src/selectable.rs+26−0
@@ -0,0 +1,26 @@
1+use gpui::{AnyView, WindowContext};
2+
3+pub trait Selectable {
4+ fn selected(self, selected: bool) -> Self;
5+ fn selected_tooltip(
6+ self,
7+ tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
8+ ) -> Self;
9+}
10+
11+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
12+pub enum Selection {
13+ #[default]
14+ Unselected,
15+ Indeterminate,
16+ Selected,
17+}
18+
19+impl Selection {
20+ pub fn inverse(&self) -> Self {
21+ match self {
22+ Self::Unselected | Self::Indeterminate => Self::Selected,
23+ Self::Selected => Self::Unselected,
24+ }
25+ }
26+}
crates/ui2/src/styles/color.rs+1−1
@@ -1,7 +1,7 @@
11 use gpui::{Hsla, WindowContext};
22 use theme::ActiveTheme;
33
4−#[derive(Default, PartialEq, Copy, Clone)]
4+#[derive(Debug, Default, PartialEq, Copy, Clone)]
55 pub enum Color {
66 #[default]
77 Default,
crates/ui2/src/ui2.rs+6−0
@@ -12,13 +12,19 @@ · 11 unmodified lines
1212 #![doc = include_str!("../docs/building-ui.md")]
1313 #![doc = include_str!("../docs/todo.md")]
1414
15+mod clickable;
1516 mod components;
17+mod fixed;
1618 pub mod prelude;
19+mod selectable;
1720 mod styled_ext;
1821 mod styles;
1922 pub mod utils;
2023
24+pub use clickable::*;
2125 pub use components::*;
26+pub use fixed::*;
2227 pub use prelude::*;
28+pub use selectable::*;
2329 pub use styled_ext::*;
2430 pub use styles::*;
crates/welcome2/Cargo.toml+37−0
@@ -0,0 +1,37 @@
1+[package]
2+name = "welcome2"
3+version = "0.1.0"
4+edition = "2021"
5+publish = false
6+
7+[lib]
8+path = "src/welcome.rs"
9+
10+[features]
11+test-support = []
12+
13+[dependencies]
14+client = { package = "client2", path = "../client2" }
15+editor = { package = "editor2", path = "../editor2" }
16+fs = { package = "fs2", path = "../fs2" }
17+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
18+gpui = { package = "gpui2", path = "../gpui2" }
19+ui = { package = "ui2", path = "../ui2" }
20+db = { package = "db2", path = "../db2" }
21+install_cli = { package = "install_cli2", path = "../install_cli2" }
22+project = { package = "project2", path = "../project2" }
23+settings = { package = "settings2", path = "../settings2" }
24+theme = { package = "theme2", path = "../theme2" }
25+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
26+util = { path = "../util" }
27+picker = { package = "picker2", path = "../picker2" }
28+workspace = { package = "workspace2", path = "../workspace2" }
29+# vim = { package = "vim2", path = "../vim2" }
30+
31+anyhow.workspace = true
32+log.workspace = true
33+schemars.workspace = true
34+serde.workspace = true
35+
36+[dev-dependencies]
37+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
crates/welcome2/src/base_keymap_picker.rs+208−0
@@ -0,0 +1,208 @@
1+use super::base_keymap_setting::BaseKeymap;
2+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
3+use gpui::{
4+ actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
5+ View, ViewContext, VisualContext, WeakView,
6+};
7+use picker::{Picker, PickerDelegate};
8+use project::Fs;
9+use settings::{update_settings_file, Settings};
10+use std::sync::Arc;
11+use ui::ListItem;
12+use util::ResultExt;
13+use workspace::{ui::HighlightedLabel, Workspace};
14+
15+actions!(ToggleBaseKeymapSelector);
16+
17+pub fn init(cx: &mut AppContext) {
18+ cx.observe_new_views(|workspace: &mut Workspace, _cx| {
19+ workspace.register_action(toggle);
20+ })
21+ .detach();
22+}
23+
24+pub fn toggle(
25+ workspace: &mut Workspace,
26+ _: &ToggleBaseKeymapSelector,
27+ cx: &mut ViewContext<Workspace>,
28+) {
29+ let fs = workspace.app_state().fs.clone();
30+ workspace.toggle_modal(cx, |cx| {
31+ BaseKeymapSelector::new(
32+ BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
33+ cx,
34+ )
35+ });
36+}
37+
38+pub struct BaseKeymapSelector {
39+ focus_handle: gpui::FocusHandle,
40+ picker: View<Picker<BaseKeymapSelectorDelegate>>,
41+}
42+
43+impl FocusableView for BaseKeymapSelector {
44+ fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
45+ self.focus_handle.clone()
46+ }
47+}
48+
49+impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
50+
51+impl BaseKeymapSelector {
52+ pub fn new(
53+ delegate: BaseKeymapSelectorDelegate,
54+ cx: &mut ViewContext<BaseKeymapSelector>,
55+ ) -> Self {
56+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
57+ let focus_handle = cx.focus_handle();
58+ Self {
59+ focus_handle,
60+ picker,
61+ }
62+ }
63+}
64+
65+impl Render for BaseKeymapSelector {
66+ type Element = View<Picker<BaseKeymapSelectorDelegate>>;
67+
68+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
69+ self.picker.clone()
70+ }
71+}
72+
73+pub struct BaseKeymapSelectorDelegate {
74+ view: WeakView<BaseKeymapSelector>,
75+ matches: Vec<StringMatch>,
76+ selected_index: usize,
77+ fs: Arc<dyn Fs>,
78+}
79+
80+impl BaseKeymapSelectorDelegate {
81+ fn new(
82+ weak_view: WeakView<BaseKeymapSelector>,
83+ fs: Arc<dyn Fs>,
84+ cx: &mut ViewContext<BaseKeymapSelector>,
85+ ) -> Self {
86+ let base = BaseKeymap::get(None, cx);
87+ let selected_index = BaseKeymap::OPTIONS
88+ .iter()
89+ .position(|(_, value)| value == base)
90+ .unwrap_or(0);
91+ Self {
92+ view: weak_view,
93+ matches: Vec::new(),
94+ selected_index,
95+ fs,
96+ }
97+ }
98+}
99+
100+impl PickerDelegate for BaseKeymapSelectorDelegate {
101+ type ListItem = ui::ListItem;
102+
103+ fn placeholder_text(&self) -> Arc<str> {
104+ "Select a base keymap...".into()
105+ }
106+
107+ fn match_count(&self) -> usize {
108+ self.matches.len()
109+ }
110+
111+ fn selected_index(&self) -> usize {
112+ self.selected_index
113+ }
114+
115+ fn set_selected_index(
116+ &mut self,
117+ ix: usize,
118+ _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
119+ ) {
120+ self.selected_index = ix;
121+ }
122+
123+ fn update_matches(
124+ &mut self,
125+ query: String,
126+ cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
127+ ) -> Task<()> {
128+ let background = cx.background_executor().clone();
129+ let candidates = BaseKeymap::names()
130+ .enumerate()
131+ .map(|(id, name)| StringMatchCandidate {
132+ id,
133+ char_bag: name.into(),
134+ string: name.into(),
135+ })
136+ .collect::<Vec<_>>();
137+
138+ cx.spawn(|this, mut cx| async move {
139+ let matches = if query.is_empty() {
140+ candidates
141+ .into_iter()
142+ .enumerate()
143+ .map(|(index, candidate)| StringMatch {
144+ candidate_id: index,
145+ string: candidate.string,
146+ positions: Vec::new(),
147+ score: 0.0,
148+ })
149+ .collect()
150+ } else {
151+ match_strings(
152+ &candidates,
153+ &query,
154+ false,
155+ 100,
156+ &Default::default(),
157+ background,
158+ )
159+ .await
160+ };
161+
162+ this.update(&mut cx, |this, _| {
163+ this.delegate.matches = matches;
164+ this.delegate.selected_index = this
165+ .delegate
166+ .selected_index
167+ .min(this.delegate.matches.len().saturating_sub(1));
168+ })
169+ .log_err();
170+ })
171+ }
172+
173+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
174+ if let Some(selection) = self.matches.get(self.selected_index) {
175+ let base_keymap = BaseKeymap::from_names(&selection.string);
176+ update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
177+ *setting = Some(base_keymap)
178+ });
179+ }
180+
181+ self.view
182+ .update(cx, |_, cx| {
183+ cx.emit(DismissEvent);
184+ })
185+ .ok();
186+ }
187+
188+ fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
189+
190+ fn render_match(
191+ &self,
192+ ix: usize,
193+ selected: bool,
194+ _cx: &mut gpui::ViewContext<Picker<Self>>,
195+ ) -> Option<Self::ListItem> {
196+ let keymap_match = &self.matches[ix];
197+
198+ Some(
199+ ListItem::new(ix)
200+ .selected(selected)
201+ .inset(true)
202+ .child(HighlightedLabel::new(
203+ keymap_match.string.clone(),
204+ keymap_match.positions.clone(),
205+ )),
206+ )
207+ }
208+}
crates/welcome2/src/base_keymap_setting.rs+65−0
@@ -0,0 +1,65 @@
1+use schemars::JsonSchema;
2+use serde::{Deserialize, Serialize};
3+use settings::Settings;
4+
5+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
6+pub enum BaseKeymap {
7+ #[default]
8+ VSCode,
9+ JetBrains,
10+ SublimeText,
11+ Atom,
12+ TextMate,
13+}
14+
15+impl BaseKeymap {
16+ pub const OPTIONS: [(&'static str, Self); 5] = [
17+ ("VSCode (Default)", Self::VSCode),
18+ ("Atom", Self::Atom),
19+ ("JetBrains", Self::JetBrains),
20+ ("Sublime Text", Self::SublimeText),
21+ ("TextMate", Self::TextMate),
22+ ];
23+
24+ pub fn asset_path(&self) -> Option<&'static str> {
25+ match self {
26+ BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
27+ BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
28+ BaseKeymap::Atom => Some("keymaps/atom.json"),
29+ BaseKeymap::TextMate => Some("keymaps/textmate.json"),
30+ BaseKeymap::VSCode => None,
31+ }
32+ }
33+
34+ pub fn names() -> impl Iterator<Item = &'static str> {
35+ Self::OPTIONS.iter().map(|(name, _)| *name)
36+ }
37+
38+ pub fn from_names(option: &str) -> BaseKeymap {
39+ Self::OPTIONS
40+ .iter()
41+ .copied()
42+ .find_map(|(name, value)| (name == option).then(|| value))
43+ .unwrap_or_default()
44+ }
45+}
46+
47+impl Settings for BaseKeymap {
48+ const KEY: Option<&'static str> = Some("base_keymap");
49+
50+ type FileContent = Option<Self>;
51+
52+ fn load(
53+ default_value: &Self::FileContent,
54+ user_values: &[&Self::FileContent],
55+ _: &mut gpui::AppContext,
56+ ) -> anyhow::Result<Self>
57+ where
58+ Self: Sized,
59+ {
60+ Ok(user_values
61+ .first()
62+ .and_then(|v| **v)
63+ .unwrap_or(default_value.unwrap()))
64+ }
65+}
crates/welcome2/src/welcome.rs+281−0
@@ -0,0 +1,281 @@
1+mod base_keymap_picker;
2+mod base_keymap_setting;
3+
4+use db::kvp::KEY_VALUE_STORE;
5+use gpui::{
6+ div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable,
7+ FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View,
8+ ViewContext, VisualContext, WeakView, WindowContext,
9+};
10+use settings::{Settings, SettingsStore};
11+use std::sync::Arc;
12+use workspace::{
13+ dock::DockPosition,
14+ item::{Item, ItemEvent},
15+ open_new, AppState, Welcome, Workspace, WorkspaceId,
16+};
17+
18+pub use base_keymap_setting::BaseKeymap;
19+
20+pub const FIRST_OPEN: &str = "first_open";
21+
22+pub fn init(cx: &mut AppContext) {
23+ BaseKeymap::register(cx);
24+
25+ cx.observe_new_views(|workspace: &mut Workspace, _cx| {
26+ workspace.register_action(|workspace, _: &Welcome, cx| {
27+ let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx));
28+ workspace.add_item(Box::new(welcome_page), cx)
29+ });
30+ })
31+ .detach();
32+
33+ base_keymap_picker::init(cx);
34+}
35+
36+pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
37+ open_new(&app_state, cx, |workspace, cx| {
38+ workspace.toggle_dock(DockPosition::Left, cx);
39+ let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx));
40+ workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
41+ cx.focus_view(&welcome_page);
42+ cx.notify();
43+ })
44+ .detach();
45+
46+ db::write_and_log(cx, || {
47+ KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
48+ });
49+}
50+
51+pub struct WelcomePage {
52+ workspace: WeakView<Workspace>,
53+ focus_handle: FocusHandle,
54+ _settings_subscription: Subscription,
55+}
56+
57+impl Render for WelcomePage {
58+ type Element = Focusable<Div>;
59+
60+ fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
61+ // todo!(welcome_ui)
62+ // let self_handle = cx.handle();
63+ // let theme = cx.theme();
64+ // let width = theme.welcome.page_width;
65+
66+ // let telemetry_settings = TelemetrySettings::get(None, cx);
67+ // let vim_mode_setting = VimModeSettings::get(cx);
68+
69+ div()
70+ .track_focus(&self.focus_handle)
71+ .child(div().size_full().bg(red()).child("Welcome!"))
72+ //todo!()
73+ // PaneBackdrop::new(
74+ // self_handle.id(),
75+ // Flex::column()
76+ // .with_child(
77+ // Flex::column()
78+ // .with_child(
79+ // theme::ui::svg(&theme.welcome.logo)
80+ // .aligned()
81+ // .contained()
82+ // .aligned(),
83+ // )
84+ // .with_child(
85+ // Label::new(
86+ // "Code at the speed of thought",
87+ // theme.welcome.logo_subheading.text.clone(),
88+ // )
89+ // .aligned()
90+ // .contained()
91+ // .with_style(theme.welcome.logo_subheading.container),
92+ // )
93+ // .contained()
94+ // .with_style(theme.welcome.heading_group)
95+ // .constrained()
96+ // .with_width(width),
97+ // )
98+ // .with_child(
99+ // Flex::column()
100+ // .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
101+ // "Choose a theme",
102+ // width,
103+ // &theme.welcome.button,
104+ // cx,
105+ // |_, this, cx| {
106+ // if let Some(workspace) = this.workspace.upgrade(cx) {
107+ // workspace.update(cx, |workspace, cx| {
108+ // theme_selector::toggle(workspace, &Default::default(), cx)
109+ // })
110+ // }
111+ // },
112+ // ))
113+ // .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
114+ // "Choose a keymap",
115+ // width,
116+ // &theme.welcome.button,
117+ // cx,
118+ // |_, this, cx| {
119+ // if let Some(workspace) = this.workspace.upgrade(cx) {
120+ // workspace.update(cx, |workspace, cx| {
121+ // base_keymap_picker::toggle(
122+ // workspace,
123+ // &Default::default(),
124+ // cx,
125+ // )
126+ // })
127+ // }
128+ // },
129+ // ))
130+ // .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
131+ // "Install the CLI",
132+ // width,
133+ // &theme.welcome.button,
134+ // cx,
135+ // |_, _, cx| {
136+ // cx.app_context()
137+ // .spawn(|cx| async move { install_cli::install_cli(&cx).await })
138+ // .detach_and_log_err(cx);
139+ // },
140+ // ))
141+ // .contained()
142+ // .with_style(theme.welcome.button_group)
143+ // .constrained()
144+ // .with_width(width),
145+ // )
146+ // .with_child(
147+ // Flex::column()
148+ // .with_child(
149+ // theme::ui::checkbox::<Diagnostics, Self, _>(
150+ // "Enable vim mode",
151+ // &theme.welcome.checkbox,
152+ // vim_mode_setting,
153+ // 0,
154+ // cx,
155+ // |this, checked, cx| {
156+ // if let Some(workspace) = this.workspace.upgrade(cx) {
157+ // let fs = workspace.read(cx).app_state().fs.clone();
158+ // update_settings_file::<VimModeSetting>(
159+ // fs,
160+ // cx,
161+ // move |setting| *setting = Some(checked),
162+ // )
163+ // }
164+ // },
165+ // )
166+ // .contained()
167+ // .with_style(theme.welcome.checkbox_container),
168+ // )
169+ // .with_child(
170+ // theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
171+ // Flex::column()
172+ // .with_child(
173+ // Label::new(
174+ // "Send anonymous usage data",
175+ // theme.welcome.checkbox.label.text.clone(),
176+ // )
177+ // .contained()
178+ // .with_style(theme.welcome.checkbox.label.container),
179+ // )
180+ // .with_child(
181+ // Label::new(
182+ // "Help > View Telemetry",
183+ // theme.welcome.usage_note.text.clone(),
184+ // )
185+ // .contained()
186+ // .with_style(theme.welcome.usage_note.container),
187+ // ),
188+ // &theme.welcome.checkbox,
189+ // telemetry_settings.metrics,
190+ // 0,
191+ // cx,
192+ // |this, checked, cx| {
193+ // if let Some(workspace) = this.workspace.upgrade(cx) {
194+ // let fs = workspace.read(cx).app_state().fs.clone();
195+ // update_settings_file::<TelemetrySettings>(
196+ // fs,
197+ // cx,
198+ // move |setting| setting.metrics = Some(checked),
199+ // )
200+ // }
201+ // },
202+ // )
203+ // .contained()
204+ // .with_style(theme.welcome.checkbox_container),
205+ // )
206+ // .with_child(
207+ // theme::ui::checkbox::<Diagnostics, Self, _>(
208+ // "Send crash reports",
209+ // &theme.welcome.checkbox,
210+ // telemetry_settings.diagnostics,
211+ // 1,
212+ // cx,
213+ // |this, checked, cx| {
214+ // if let Some(workspace) = this.workspace.upgrade(cx) {
215+ // let fs = workspace.read(cx).app_state().fs.clone();
216+ // update_settings_file::<TelemetrySettings>(
217+ // fs,
218+ // cx,
219+ // move |setting| setting.diagnostics = Some(checked),
220+ // )
221+ // }
222+ // },
223+ // )
224+ // .contained()
225+ // .with_style(theme.welcome.checkbox_container),
226+ // )
227+ // .contained()
228+ // .with_style(theme.welcome.checkbox_group)
229+ // .constrained()
230+ // .with_width(width),
231+ // )
232+ // .constrained()
233+ // .with_max_width(width)
234+ // .contained()
235+ // .with_uniform_padding(10.)
236+ // .aligned()
237+ // .into_any(),
238+ // )
239+ // .into_any_named("welcome page")
240+ }
241+}
242+
243+impl WelcomePage {
244+ pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
245+ WelcomePage {
246+ focus_handle: cx.focus_handle(),
247+ workspace: workspace.weak_handle(),
248+ _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
249+ }
250+ }
251+}
252+
253+impl EventEmitter<ItemEvent> for WelcomePage {}
254+
255+impl FocusableView for WelcomePage {
256+ fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
257+ self.focus_handle.clone()
258+ }
259+}
260+
261+impl Item for WelcomePage {
262+ fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
263+ "Welcome to Zed!".into_any()
264+ }
265+
266+ fn show_toolbar(&self) -> bool {
267+ false
268+ }
269+
270+ fn clone_on_split(
271+ &self,
272+ _workspace_id: WorkspaceId,
273+ cx: &mut ViewContext<Self>,
274+ ) -> Option<View<Self>> {
275+ Some(cx.build_view(|cx| WelcomePage {
276+ focus_handle: cx.focus_handle(),
277+ workspace: self.workspace.clone(),
278+ _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
279+ }))
280+ }
281+}
crates/workspace2/src/dock.rs+14−10
@@ -7,8 +7,8 @@ use gpui::{ · 6 unmodified lines
77 use schemars::JsonSchema;
88 use serde::{Deserialize, Serialize};
99 use std::sync::Arc;
10−use theme2::ActiveTheme;
11−use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
10+use ui::prelude::*;
11+use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
1212
1313 pub enum PanelEvent {
1414 ChangePosition,
@@ -686,22 +686,26 @@ impl Render for PanelButtons { · 671 unmodified lines
686686 let name = entry.panel.persistent_name();
687687 let panel = entry.panel.clone();
688688
689− let mut button: IconButton = if i == active_index && is_open {
689+ let is_active_button = i == active_index && is_open;
690+
691+ let (action, tooltip) = if is_active_button {
690692 let action = dock.toggle_action();
693+
691694 let tooltip: SharedString =
692695 format!("Close {} dock", dock.position.to_label()).into();
693− IconButton::new(name, icon)
694− .state(InteractionState::Active)
695− .action(action.boxed_clone())
696− .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
696+
697+ (action, tooltip)
697698 } else {
698699 let action = entry.panel.toggle_action(cx);
699700
700− IconButton::new(name, icon)
701− .action(action.boxed_clone())
702− .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
701+ (action, name.into())
703702 };
704703
704+ let button = IconButton::new(name, icon)
705+ .selected(is_active_button)
706+ .action(action.boxed_clone())
707+ .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx));
708+
705709 Some(
706710 menu_handle(name)
707711 .menu(move |cx| {
crates/workspace2/src/pane.rs+4−8
@@ -1482,18 +1482,14 @@ impl Pane { · 1481 unmodified lines
14821482 .gap_px()
14831483 .child(
14841484 div().border().border_color(gpui::red()).child(
1485− IconButton::new("navigate_backward", Icon::ArrowLeft).state(
1486− InteractionState::Enabled
1487− .if_enabled(self.can_navigate_backward()),
1488− ),
1485+ IconButton::new("navigate_backward", Icon::ArrowLeft)
1486+ .disabled(!self.can_navigate_backward()),
14891487 ),
14901488 )
14911489 .child(
14921490 div().border().border_color(gpui::red()).child(
1493− IconButton::new("navigate_forward", Icon::ArrowRight).state(
1494− InteractionState::Enabled
1495− .if_enabled(self.can_navigate_forward()),
1496− ),
1491+ IconButton::new("navigate_forward", Icon::ArrowRight)
1492+ .disabled(!self.can_navigate_forward()),
14971493 ),
14981494 ),
14991495 ),
crates/workspace2/src/status_bar.rs+1−1
@@ -5,7 +5,7 @@ use gpui::{ · 4 unmodified lines
55 div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
66 WindowContext,
77 };
8−use theme2::ActiveTheme;
8+use ui::prelude::*;
99 use ui::{h_stack, Button, Icon, IconButton};
1010 use util::ResultExt;
1111
crates/workspace2/src/toolbar.rs+1−1
@@ -3,7 +3,7 @@ use gpui::{ · 2 unmodified lines
33 div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
44 ViewContext, WindowContext,
55 };
6−use theme2::ActiveTheme;
6+use ui::prelude::*;
77 use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
88
99 pub enum ToolbarItemEvent {
crates/workspace2/src/workspace2.rs+16−16
@@ -1808,22 +1808,22 @@ impl Workspace { · 1807 unmodified lines
18081808 pane
18091809 }
18101810
1811− // pub fn add_item_to_center(
1812− // &mut self,
1813− // item: Box<dyn ItemHandle>,
1814− // cx: &mut ViewContext<Self>,
1815− // ) -> bool {
1816− // if let Some(center_pane) = self.last_active_center_pane.clone() {
1817− // if let Some(center_pane) = center_pane.upgrade(cx) {
1818− // center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1819− // true
1820− // } else {
1821− // false
1822− // }
1823− // } else {
1824− // false
1825− // }
1826− // }
1811+ pub fn add_item_to_center(
1812+ &mut self,
1813+ item: Box<dyn ItemHandle>,
1814+ cx: &mut ViewContext<Self>,
1815+ ) -> bool {
1816+ if let Some(center_pane) = self.last_active_center_pane.clone() {
1817+ if let Some(center_pane) = center_pane.upgrade() {
1818+ center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1819+ true
1820+ } else {
1821+ false
1822+ }
1823+ } else {
1824+ false
1825+ }
1826+ }
18271827
18281828 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
18291829 self.active_pane
crates/zed/Cargo.toml+1−1
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"] · 2 unmodified lines
33 description = "The fast, collaborative code editor."
44 edition = "2021"
55 name = "zed"
6−version = "0.115.0"
6+version = "0.116.0"
77 publish = false
88
99 [lib]
crates/zed2/Cargo.toml+2−2
@@ -66,12 +66,12 @@ shellexpand = "2.1.0" · 65 unmodified lines
6666 text = { package = "text2", path = "../text2" }
6767 terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
6868 theme = { package = "theme2", path = "../theme2" }
69−# theme_selector = { path = "../theme_selector" }
69+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
7070 util = { path = "../util" }
7171 # semantic_index = { path = "../semantic_index" }
7272 # vim = { path = "../vim" }
7373 workspace = { package = "workspace2", path = "../workspace2" }
74−# welcome = { path = "../welcome" }
74+welcome = { package = "welcome2", path = "../welcome2" }
7575 zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
7676 anyhow.workspace = true
7777 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
crates/zed2/src/main.rs+81−119
@@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE; · 12 unmodified lines
1313 use editor::Editor;
1414 use fs::RealFs;
1515 use futures::StreamExt;
16−use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
16+use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
1717 use isahc::{prelude::Configurable, Request};
1818 use language::LanguageRegistry;
1919 use log::LevelFilter;
@@ -36,7 +36,7 @@ use std::{ · 16 unmodified lines
3636 path::{Path, PathBuf},
3737 sync::{
3838 atomic::{AtomicU32, Ordering},
39− Arc,
39+ Arc, Weak,
4040 },
4141 thread,
4242 };
@@ -48,6 +48,7 @@ use util::{ · 5 unmodified lines
4848 paths, ResultExt,
4949 };
5050 use uuid::Uuid;
51+use welcome::{show_welcome_experience, FIRST_OPEN};
5152 use workspace::{AppState, WorkspaceStore};
5253 use zed2::{
5354 build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
@@ -103,16 +104,15 @@ fn main() { · 49 unmodified lines
103104 let listener = Arc::new(listener);
104105 let open_listener = listener.clone();
105106 app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
106− app.on_reopen(move |_cx| {
107− // todo!("workspace")
108− // if cx.has_global::<Weak<AppState>>() {
109− // if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
110− // workspace::open_new(&app_state, cx, |workspace, cx| {
111− // Editor::new_file(workspace, &Default::default(), cx)
112− // })
113− // .detach();
114− // }
115− // }
107+ app.on_reopen(move |cx| {
108+ if cx.has_global::<Weak<AppState>>() {
109+ if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
110+ workspace::open_new(&app_state, cx, |workspace, cx| {
111+ Editor::new_file(workspace, &Default::default(), cx)
112+ })
113+ .detach();
114+ }
115+ }
116116 });
117117
118118 app.run(move |cx| {
@@ -164,17 +164,16 @@ fn main() { · 45 unmodified lines
164164 // assistant::init(cx);
165165 // component_test::init(cx);
166166
167− // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
168167 // cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
169168 // .detach();
170− // watch_file_types(fs.clone(), cx);
169+ watch_file_types(fs.clone(), cx);
171170
172171 languages.set_theme(cx.theme().clone());
173− // cx.observe_global::<SettingsStore, _>({
174− // let languages = languages.clone();
175− // move |cx| languages.set_theme(theme::current(cx).clone())
176− // })
177− // .detach();
172+ cx.observe_global::<SettingsStore>({
173+ let languages = languages.clone();
174+ move |cx| languages.set_theme(cx.theme().clone())
175+ })
176+ .detach();
178177
179178 client.telemetry().start(installation_id, session_id, cx);
180179 let telemetry_settings = *client::TelemetrySettings::get_global(cx);
@@ -193,7 +192,6 @@ fn main() { · 12 unmodified lines
193192 fs,
194193 build_window_options,
195194 call_factory: call::Call::new,
196− // background_actions: todo!("ask Mikayla"),
197195 workspace_store,
198196 node_runtime,
199197 });
@@ -219,14 +217,13 @@ fn main() { · 19 unmodified lines
219217
220218 // journal2::init(app_state.clone(), cx);
221219 // language_selector::init(cx);
222− // theme_selector::init(cx);
220+ theme_selector::init(cx);
223221 // activity_indicator::init(cx);
224222 // language_tools::init(cx);
225223 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
226224 collab_ui::init(&app_state, cx);
227225 // feedback::init(cx);
228− // welcome::init(cx);
229− // zed::init(&app_state, cx);
226+ welcome::init(cx);
230227
231228 // cx.set_menus(menus::menus());
232229 initialize_workspace(app_state.clone(), cx);
@@ -279,17 +276,18 @@ fn main() { · 46 unmodified lines
279276 .detach();
280277 }
281278 Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
282− todo!()
283− // triggered_authentication = true;
284− // let app_state = app_state.clone();
285− // let client = client.clone();
286− // cx.spawn(|mut cx| async move {
287− // // ignore errors here, we'll show a generic "not signed in"
288− // let _ = authenticate(client, &cx).await;
289− // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
290− // .await
291− // })
292− // .detach_and_log_err(cx)
279+ triggered_authentication = true;
280+ let app_state = app_state.clone();
281+ let client = client.clone();
282+ cx.spawn(|mut cx| async move {
283+ // ignore errors here, we'll show a generic "not signed in"
284+ let _ = authenticate(client, &cx).await;
285+ //todo!()
286+ // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
287+ // .await
288+ anyhow::Ok(())
289+ })
290+ .detach_and_log_err(cx)
293291 }
294292 Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
295293 todo!()
@@ -340,7 +338,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> { · 44 unmodified lines
340338 if client::IMPERSONATE_LOGIN.is_some() {
341339 client.authenticate_and_connect(false, &cx).await?;
342340 }
343− } else if client.has_keychain_credentials(&cx).await {
341+ } else if client.has_keychain_credentials(&cx) {
344342 client.authenticate_and_connect(true, &cx).await?;
345343 }
346344 Ok::<_, anyhow::Error>(())
@@ -368,10 +366,9 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp · 21 unmodified lines
368366 cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
369367 .await
370368 .log_err();
371− // todo!(welcome)
372− //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
373− //todo!()
374− // cx.update(|cx| show_welcome_experience(app_state, cx));
369+ } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
370+ cx.update(|cx| show_welcome_experience(app_state, cx))
371+ .log_err();
375372 } else {
376373 cx.update(|cx| {
377374 workspace::open_new(app_state, cx, |workspace, cx| {
@@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) { · 331 unmodified lines
709706 .unwrap();
710707 }
711708
712−// #[cfg(debug_assertions)]
713−// async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
714−// let mut events = fs
715−// .watch("styles/src".as_ref(), Duration::from_millis(100))
716−// .await;
717−// while (events.next().await).is_some() {
718−// let output = Command::new("npm")
719−// .current_dir("styles")
720−// .args(["run", "build"])
721−// .output()
722−// .await
723−// .log_err()?;
724−// if output.status.success() {
725−// cx.update(|cx| theme_selector::reload(cx))
726−// } else {
727−// eprintln!(
728−// "build script failed {}",
729−// String::from_utf8_lossy(&output.stderr)
730−// );
731−// }
732−// }
733−// Some(())
734−// }
735−
736−// #[cfg(debug_assertions)]
737−// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
738−// let mut events = fs
739−// .watch(
740−// "crates/zed/src/languages".as_ref(),
741−// Duration::from_millis(100),
742−// )
743−// .await;
744−// while (events.next().await).is_some() {
745−// languages.reload();
746−// }
747−// Some(())
748−// }
749−
750−// #[cfg(debug_assertions)]
751−// fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
752−// cx.spawn(|mut cx| async move {
753−// let mut events = fs
754−// .watch(
755−// "assets/icons/file_icons/file_types.json".as_ref(),
756−// Duration::from_millis(100),
757−// )
758−// .await;
759−// while (events.next().await).is_some() {
760−// cx.update(|cx| {
761−// cx.update_global(|file_types, _| {
762−// *file_types = project_panel::file_associations::FileAssociations::new(Assets);
763−// });
764−// })
765−// }
766−// })
767−// .detach()
768−// }
769−
770−// #[cfg(not(debug_assertions))]
771−// async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
772−// None
773−// }
774−
775−// #[cfg(not(debug_assertions))]
776−// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
777−// None
778−//
779−
780−// #[cfg(not(debug_assertions))]
781−// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
782−
783−pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
784− // &[
785− // ("Go to file", &file_finder::Toggle),
786− // ("Open command palette", &command_palette::Toggle),
787− // ("Open recent projects", &recent_projects::OpenRecent),
788− // ("Change your settings", &zed_actions::OpenSettings),
789− // ]
790− // todo!()
791− &[]
709+#[cfg(debug_assertions)]
710+async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
711+ use std::time::Duration;
712+
713+ let mut events = fs
714+ .watch(
715+ "crates/zed2/src/languages".as_ref(),
716+ Duration::from_millis(100),
717+ )
718+ .await;
719+ while (events.next().await).is_some() {
720+ languages.reload();
721+ }
722+ Some(())
723+}
724+
725+#[cfg(debug_assertions)]
726+fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
727+ use std::time::Duration;
728+
729+ cx.spawn(|mut cx| async move {
730+ let mut events = fs
731+ .watch(
732+ "assets/icons/file_icons/file_types.json".as_ref(),
733+ Duration::from_millis(100),
734+ )
735+ .await;
736+ while (events.next().await).is_some() {
737+ cx.update(|cx| {
738+ cx.update_global(|file_types, _| {
739+ *file_types = project_panel::file_associations::FileAssociations::new(Assets);
740+ });
741+ })
742+ .ok();
743+ }
744+ })
745+ .detach()
792746 }
747+
748+#[cfg(not(debug_assertions))]
749+async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
750+ None
751+}
752+
753+#[cfg(not(debug_assertions))]
754+fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
script/crate-dep-graph+1−1
@@ -11,7 +11,7 @@ graph_file=target/crate-graph.html · 10 unmodified lines
1111 cargo depgraph \
1212 --workspace-only \
1313 --offline \
14− --root=zed,cli,collab \
14+ --root=zed2,cli,collab2 \
1515 --dedup-transitive-deps \
1616 | dot -Tsvg > $graph_file
1717

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t