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
9471 "workspace",9471 "workspace",
9472]9472]
9473 9473
9474[[package]]
9475name = "theme_selector2"
9476version = "0.1.0"
9477dependencies = [
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
9474[[package]]9495[[package]]
9475name = "thiserror"9496name = "thiserror"
9476version = "1.0.48"9497version = "1.0.48"
@@ -11054,6 +11075,31 @@ dependencies = [ · 1577 unmodified lines
11054 "workspace",11075 "workspace",
11055]11076]
11056 11077
11078[[package]]
11079name = "welcome2"
11080version = "0.1.0"
11081dependencies = [
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
11057[[package]]11103[[package]]
11058name = "which"11104name = "which"
11059version = "4.4.2"11105version = "4.4.2"
@@ -11508,7 +11554,7 @@ dependencies = [ · 448 unmodified lines
11508 11554
11509[[package]]11555[[package]]
11510name = "zed"11556name = "zed"
11511version = "0.115.0"11557version = "0.116.0"
11512dependencies = [11558dependencies = [
11513 "activity_indicator",11559 "activity_indicator",
11514 "ai",11560 "ai",
@@ -11720,6 +11766,7 @@ dependencies = [ · 205 unmodified lines
11720 "terminal_view2",11766 "terminal_view2",
11721 "text2",11767 "text2",
11722 "theme2",11768 "theme2",
11769 "theme_selector2",
11723 "thiserror",11770 "thiserror",
11724 "tiny_http",11771 "tiny_http",
11725 "toml 0.5.11",11772 "toml 0.5.11",
@@ -11757,6 +11804,7 @@ dependencies = [ · 31 unmodified lines
11757 "urlencoding",11804 "urlencoding",
11758 "util",11805 "util",
11759 "uuid 1.4.1",11806 "uuid 1.4.1",
11807 "welcome2",
11760 "workspace2",11808 "workspace2",
11761 "zed_actions2",11809 "zed_actions2",
11762]11810]
Cargo.toml+2−0
@@ -107,6 +107,7 @@ members = [ · 106 unmodified lines
107 "crates/theme2",107 "crates/theme2",
108 "crates/theme_importer",108 "crates/theme_importer",
109 "crates/theme_selector",109 "crates/theme_selector",
110 "crates/theme_selector2",
110 "crates/ui2",111 "crates/ui2",
111 "crates/util",112 "crates/util",
112 "crates/semantic_index",113 "crates/semantic_index",
@@ -115,6 +116,7 @@ members = [ · 2 unmodified lines
115 "crates/vcs_menu",116 "crates/vcs_menu",
116 "crates/workspace2",117 "crates/workspace2",
117 "crates/welcome",118 "crates/welcome",
119 "crates/welcome2",
118 "crates/xtask",120 "crates/xtask",
119 "crates/zed",121 "crates/zed",
120 "crates/zed2",122 "crates/zed2",
crates/call2/src/call2.rs+47−4
@@ -14,8 +14,8 @@ use client::{ · 13 unmodified lines
14use collections::HashSet;14use collections::HashSet;
15use futures::{channel::oneshot, future::Shared, Future, FutureExt};15use futures::{channel::oneshot, future::Shared, Future, FutureExt};
16use gpui::{16use gpui::{
17 AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,17 AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
18 View, ViewContext, VisualContext, WeakModel, WeakView,18 Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
19};19};
20pub use participant::ParticipantLocation;20pub use participant::ParticipantLocation;
21use postage::watch;21use postage::watch;
@@ -334,12 +334,55 @@ impl ActiveCall { · 312 unmodified lines
334 pub fn join_channel(334 pub fn join_channel(
335 &mut self,335 &mut self,
336 channel_id: u64,336 channel_id: u64,
337 requesting_window: Option<WindowHandle<Workspace>>,
337 cx: &mut ModelContext<Self>,338 cx: &mut ModelContext<Self>,
338 ) -> Task<Result<Option<Model<Room>>>> {339 ) -> Task<Result<Option<Model<Room>>>> {
339 if let Some(room) = self.room().cloned() {340 if let Some(room) = self.room().cloned() {
340 if room.read(cx).channel_id() == Some(channel_id) {341 if room.read(cx).channel_id() == Some(channel_id) {
341 return Task::ready(Ok(Some(room)));342 return cx.spawn(|_, _| async move {
342 } else {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() {
343 room.update(cx, |room, cx| room.clear_state(cx));386 room.update(cx, |room, cx| room.clear_state(cx));
344 }387 }
345 }388 }
crates/client2/src/client2.rs+4−4
@@ -693,8 +693,8 @@ impl Client { · 692 unmodified lines
693 }693 }
694 }694 }
695 695
696 pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {696 pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
697 read_credentials_from_keychain(cx).await.is_some()697 read_credentials_from_keychain(cx).is_some()
698 }698 }
699 699
700 #[async_recursion(?Send)]700 #[async_recursion(?Send)]
@@ -725,7 +725,7 @@ impl Client { · 24 unmodified lines
725 let mut read_from_keychain = false;725 let mut read_from_keychain = false;
726 let mut credentials = self.state.read().credentials.clone();726 let mut credentials = self.state.read().credentials.clone();
727 if credentials.is_none() && try_keychain {727 if credentials.is_none() && try_keychain {
728 credentials = read_credentials_from_keychain(cx).await;728 credentials = read_credentials_from_keychain(cx);
729 read_from_keychain = credentials.is_some();729 read_from_keychain = credentials.is_some();
730 }730 }
731 if credentials.is_none() {731 if credentials.is_none() {
@@ -1324,7 +1324,7 @@ impl Client { · 592 unmodified lines
1324 }1324 }
1325}1325}
1326 1326
1327async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {1327fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
1328 if IMPERSONATE_LOGIN.is_some() {1328 if IMPERSONATE_LOGIN.is_some() {
1329 return None;1329 return None;
1330 }1330 }
crates/collab2/src/tests/channel_tests.rs+24−13
@@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member( · 363 unmodified lines
364 let active_call_b = cx_b.read(ActiveCall::global);364 let active_call_b = cx_b.read(ActiveCall::global);
365 365
366 assert!(active_call_b366 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))
368 .await369 .await
369 .is_ok());370 .is_ok());
370}371}
@@ -394,7 +395,9 @@ async fn test_channel_room( · 23 unmodified lines
394 let active_call_b = cx_b.read(ActiveCall::global);395 let active_call_b = cx_b.read(ActiveCall::global);
395 396
396 active_call_a397 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 })
398 .await401 .await
399 .unwrap();402 .unwrap();
400 403
@@ -442,7 +445,9 @@ async fn test_channel_room( · 41 unmodified lines
442 });445 });
443 446
444 active_call_b447 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 })
446 .await451 .await
447 .unwrap();452 .unwrap();
448 453
@@ -559,12 +564,16 @@ async fn test_channel_room( · 110 unmodified lines
559 });564 });
560 565
561 active_call_a566 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 })
563 .await570 .await
564 .unwrap();571 .unwrap();
565 572
566 active_call_b573 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 })
568 .await577 .await
569 .unwrap();578 .unwrap();
570 579
@@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo · 37 unmodified lines
608 let active_call_a = cx_a.read(ActiveCall::global);617 let active_call_a = cx_a.read(ActiveCall::global);
609 618
610 active_call_a619 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 })
612 .await623 .await
613 .unwrap();624 .unwrap();
614 625
@@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo · 12 unmodified lines
627 638
628 active_call_a639 active_call_a
629 .update(cx_a, |active_call, cx| {640 .update(cx_a, |active_call, cx| {
630 active_call.join_channel(rust_id, cx)641 active_call.join_channel(rust_id, None, cx)
631 })642 })
632 .await643 .await
633 .unwrap();644 .unwrap();
@@ -793,7 +804,7 @@ async fn test_call_from_channel( · 159 unmodified lines
793 let active_call_b = cx_b.read(ActiveCall::global);804 let active_call_b = cx_b.read(ActiveCall::global);
794 805
795 active_call_a806 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))
797 .await808 .await
798 .unwrap();809 .unwrap();
799 810
@@ -1286,7 +1297,7 @@ async fn test_guest_access( · 486 unmodified lines
1286 1297
1287 // Non-members should not be allowed to join1298 // Non-members should not be allowed to join
1288 assert!(active_call_b1299 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))
1290 .await1301 .await
1291 .is_err());1302 .is_err());
1292 1303
@@ -1308,7 +1319,7 @@ async fn test_guest_access( · 15 unmodified lines
1308 1319
1309 // Client B joins channel A as a guest1320 // Client B joins channel A as a guest
1310 active_call_b1321 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))
1312 .await1323 .await
1313 .unwrap();1324 .unwrap();
1314 1325
@@ -1341,7 +1352,7 @@ async fn test_guest_access( · 26 unmodified lines
1341 assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);1352 assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
1342 1353
1343 active_call_b1354 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))
1345 .await1356 .await
1346 .unwrap();1357 .unwrap();
1347 1358
@@ -1372,7 +1383,7 @@ async fn test_invite_access( · 24 unmodified lines
1372 1383
1373 // should not be allowed to join1384 // should not be allowed to join
1374 assert!(active_call_b1385 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))
1376 .await1387 .await
1377 .is_err());1388 .is_err());
1378 1389
@@ -1390,7 +1401,7 @@ async fn test_invite_access( · 11 unmodified lines
1390 .unwrap();1401 .unwrap();
1391 1402
1392 active_call_b1403 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))
1394 .await1405 .await
1395 .unwrap();1406 .unwrap();
1396 1407
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
510 510
511 // Simultaneously join channel 1 and then channel 2511 // Simultaneously join channel 1 and then channel 2
512 active_call_a512 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))
514 .detach();514 .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));
516 517
517 join_channel_2.await.unwrap();518 join_channel_2.await.unwrap();
518 519
@@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 19 unmodified lines
538 call.invite(client_c.user_id().unwrap(), None, cx)539 call.invite(client_c.user_id().unwrap(), None, cx)
539 });540 });
540 541
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));
542 544
543 b_invite.await.unwrap();545 b_invite.await.unwrap();
544 c_invite.await.unwrap();546 c_invite.await.unwrap();
@@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 22 unmodified lines
567 .unwrap();569 .unwrap();
568 570
569 // Simultaneously join channel 1 and call user B and user C from client A.571 // 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));
571 574
572 let b_invite = active_call_a.update(cx_a, |call, cx| {575 let b_invite = active_call_a.update(cx_a, |call, cx| {
573 call.invite(client_b.user_id().unwrap(), None, cx)576 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
17// Client, Contact, User, UserStore,17// Client, Contact, User, UserStore,
18// };18// };
19use contact_finder::ContactFinder;19use contact_finder::ContactFinder;
20use menu::Confirm;
20use rpc::proto;21use rpc::proto;
21// use context_menu::{ContextMenu, ContextMenuItem};22// use context_menu::{ContextMenu, ContextMenuItem};
22// use db::kvp::KEY_VALUE_STORE;23// use db::kvp::KEY_VALUE_STORE;
@@ -90,10 +91,10 @@ use rpc::proto; · 67 unmodified lines
90// channel_id: ChannelId,91// channel_id: ChannelId,
91// }92// }
92 93
93// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]94#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)]
94// pub struct OpenChannelNotes {95pub struct OpenChannelNotes {
95// pub channel_id: ChannelId,96 pub channel_id: ChannelId,
96// }97}
97 98
98// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]99// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99// pub struct JoinChannelCall {100// pub struct JoinChannelCall {
@@ -160,26 +161,26 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; · 60 unmodified lines
160use std::{iter::once, mem, sync::Arc};161use std::{iter::once, mem, sync::Arc};
161 162
162use call::ActiveCall;163use call::ActiveCall;
163use channel::{Channel, ChannelId, ChannelStore};164use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
164use client::{Client, Contact, User, UserStore};165use client::{Client, Contact, User, UserStore};
165use db::kvp::KEY_VALUE_STORE;166use db::kvp::KEY_VALUE_STORE;
166use editor::Editor;167use editor::Editor;
167use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};168use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
168use fuzzy::{match_strings, StringMatchCandidate};169use fuzzy::{match_strings, StringMatchCandidate};
169use gpui::{170use gpui::{
170 actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter,171 actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div,
171 FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement,172 EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model,
172 Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext,173 ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View,
173 WeakView,174 ViewContext, VisualContext, WeakView,
174};175};
175use project::Fs;176use project::Fs;
176use serde_derive::{Deserialize, Serialize};177use serde_derive::{Deserialize, Serialize};
177use settings::Settings;178use settings::{Settings, SettingsStore};
178use ui::{179use ui::{
179 h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, Label, List, ListHeader, ListItem,180 h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List,
180 Toggle, Tooltip,181 ListHeader, ListItem, Toggle, Tooltip,
181};182};
182use util::{maybe, ResultExt};183use util::{maybe, ResultExt, TryFutureExt};
183use workspace::{184use workspace::{
184 dock::{DockPosition, Panel, PanelEvent},185 dock::{DockPosition, Panel, PanelEvent},
185 notifications::NotifyResultExt,186 notifications::NotifyResultExt,
@@ -293,10 +294,10 @@ pub enum ChannelEditingState { · 107 unmodified lines
293}294}
294 295
295impl ChannelEditingState {296impl ChannelEditingState {
296 fn pending_name(&self) -> Option<&str> {297 fn pending_name(&self) -> Option<String> {
297 match self {298 match self {
298 ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),299 ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
299 ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),300 ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
300 }301 }
301 }302 }
302}303}
@@ -306,10 +307,10 @@ pub struct CollabPanel { · 3 unmodified lines
306 fs: Arc<dyn Fs>,307 fs: Arc<dyn Fs>,
307 focus_handle: FocusHandle,308 focus_handle: FocusHandle,
308 // channel_clipboard: Option<ChannelMoveClipboard>,309 // channel_clipboard: Option<ChannelMoveClipboard>,
309 // pending_serialization: Task<Option<()>>,310 pending_serialization: Task<Option<()>>,
310 // context_menu: ViewHandle<ContextMenu>,311 // context_menu: ViewHandle<ContextMenu>,
311 filter_editor: View<Editor>,312 filter_editor: View<Editor>,
312 // channel_name_editor: ViewHandle<Editor>,313 channel_name_editor: View<Editor>,
313 channel_editing_state: Option<ChannelEditingState>,314 channel_editing_state: Option<ChannelEditingState>,
314 entries: Vec<ListEntry>,315 entries: Vec<ListEntry>,
315 selection: Option<usize>,316 selection: Option<usize>,
@@ -322,17 +323,17 @@ pub struct CollabPanel { · 6 unmodified lines
322 subscriptions: Vec<Subscription>,323 subscriptions: Vec<Subscription>,
323 collapsed_sections: Vec<Section>,324 collapsed_sections: Vec<Section>,
324 collapsed_channels: Vec<ChannelId>,325 collapsed_channels: Vec<ChannelId>,
325 // drag_target_channel: ChannelDragTarget,326 drag_target_channel: ChannelDragTarget,
326 workspace: WeakView<Workspace>,327 workspace: WeakView<Workspace>,
327 // context_menu_on_selected: bool,328 // context_menu_on_selected: bool,
328}329}
329 330
330// #[derive(PartialEq, Eq)]331#[derive(PartialEq, Eq)]
331// enum ChannelDragTarget {332enum ChannelDragTarget {
332// None,333 None,
333// Root,334 Root,
334// Channel(ChannelId),335 Channel(ChannelId),
335// }336}
336 337
337#[derive(Serialize, Deserialize)]338#[derive(Serialize, Deserialize)]
338struct SerializedCollabPanel {339struct SerializedCollabPanel {
@@ -438,28 +439,21 @@ impl CollabPanel { · 99 unmodified lines
438 // })439 // })
439 // .detach();440 // .detach();
440 441
441 // let channel_name_editor = cx.add_view(|cx| {442 let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
442 // Editor::single_line(443
443 // Some(Arc::new(|theme| {444 cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
444 // theme.collab_panel.user_query_editor.clone()445 if let editor::EditorEvent::Blurred = event {
445 // })),446 if let Some(state) = &this.channel_editing_state {
446 // cx,447 if state.pending_name().is_some() {
447 // )448 return;
448 // });449 }
449 450 }
450 // cx.subscribe(&channel_name_editor, |this, _, event, cx| {451 this.take_editing_state(cx);
451 // if let editor::Event::Blurred = event {452 this.update_entries(false, cx);
452 // if let Some(state) = &this.channel_editing_state {453 cx.notify();
453 // if state.pending_name().is_some() {454 }
454 // return;455 })
455 // }456 .detach();
456 // }
457 // this.take_editing_state(cx);
458 // this.update_entries(false, cx);
459 // cx.notify();
460 // }
461 // })
462 // .detach();
463 457
464 // let list_state =458 // let list_state =
465 // ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {459 // ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
@@ -597,9 +591,9 @@ impl CollabPanel { · 131 unmodified lines
597 focus_handle: cx.focus_handle(),591 focus_handle: cx.focus_handle(),
598 // channel_clipboard: None,592 // channel_clipboard: None,
599 fs: workspace.app_state().fs.clone(),593 fs: workspace.app_state().fs.clone(),
600 // pending_serialization: Task::ready(None),594 pending_serialization: Task::ready(None),
601 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),595 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
602 // channel_name_editor,596 channel_name_editor,
603 filter_editor,597 filter_editor,
604 entries: Vec::default(),598 entries: Vec::default(),
605 channel_editing_state: None,599 channel_editing_state: None,
@@ -614,59 +608,58 @@ impl CollabPanel { · 8 unmodified lines
614 workspace: workspace.weak_handle(),608 workspace: workspace.weak_handle(),
615 client: workspace.app_state().client.clone(),609 client: workspace.app_state().client.clone(),
616 // context_menu_on_selected: true,610 // context_menu_on_selected: true,
617 // drag_target_channel: ChannelDragTarget::None,611 drag_target_channel: ChannelDragTarget::None,
618 // list_state,612 // list_state,
619 };613 };
620 614
621 this.update_entries(false, cx);615 this.update_entries(false, cx);
622 616
623 // // Update the dock position when the setting changes.617 // Update the dock position when the setting changes.
624 // let mut old_dock_position = this.position(cx);618 let mut old_dock_position = this.position(cx);
625 // this.subscriptions619 this.subscriptions.push(cx.observe_global::<SettingsStore>(
626 // .push(620 move |this: &mut Self, cx| {
627 // cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {621 let new_dock_position = this.position(cx);
628 // let new_dock_position = this.position(cx);622 if new_dock_position != old_dock_position {
629 // if new_dock_position != old_dock_position {623 old_dock_position = new_dock_position;
630 // old_dock_position = new_dock_position;624 cx.emit(PanelEvent::ChangePosition);
631 // cx.emit(Event::DockPositionChanged);625 }
632 // }626 cx.notify();
633 // cx.notify();627 },
634 // }),628 ));
635 // );
636 629
637 // let active_call = ActiveCall::global(cx);630 let active_call = ActiveCall::global(cx);
638 this.subscriptions631 this.subscriptions
639 .push(cx.observe(&this.user_store, |this, _, cx| {632 .push(cx.observe(&this.user_store, |this, _, cx| {
640 this.update_entries(true, cx)633 this.update_entries(true, cx)
641 }));634 }));
642 // this.subscriptions635 this.subscriptions
643 // .push(cx.observe(&this.channel_store, |this, _, cx| {636 .push(cx.observe(&this.channel_store, |this, _, cx| {
644 // this.update_entries(true, cx)637 this.update_entries(true, cx)
645 // }));638 }));
646 // this.subscriptions639 this.subscriptions
647 // .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));640 .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
648 // this.subscriptions641 this.subscriptions
649 // .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {642 .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
650 // this.update_entries(true, cx)643 this.update_entries(true, cx)
651 // }));644 }));
652 // this.subscriptions.push(cx.subscribe(645 this.subscriptions.push(cx.subscribe(
653 // &this.channel_store,646 &this.channel_store,
654 // |this, _channel_store, e, cx| match e {647 |this, _channel_store, e, cx| match e {
655 // ChannelEvent::ChannelCreated(channel_id)648 ChannelEvent::ChannelCreated(channel_id)
656 // | ChannelEvent::ChannelRenamed(channel_id) => {649 | ChannelEvent::ChannelRenamed(channel_id) => {
657 // if this.take_editing_state(cx) {650 if this.take_editing_state(cx) {
658 // this.update_entries(false, cx);651 this.update_entries(false, cx);
659 // this.selection = this.entries.iter().position(|entry| {652 this.selection = this.entries.iter().position(|entry| {
660 // if let ListEntry::Channel { channel, .. } = entry {653 if let ListEntry::Channel { channel, .. } = entry {
661 // channel.id == *channel_id654 channel.id == *channel_id
662 // } else {655 } else {
663 // false656 false
664 // }657 }
665 // });658 });
666 // }659 }
667 // }660 }
668 // },661 },
669 // ));662 ));
670 663
671 this664 this
672 })665 })
@@ -696,10 +689,9 @@ impl CollabPanel { · 23 unmodified lines
696 if let Some(serialized_panel) = serialized_panel {689 if let Some(serialized_panel) = serialized_panel {
697 panel.update(cx, |panel, cx| {690 panel.update(cx, |panel, cx| {
698 panel.width = serialized_panel.width;691 panel.width = serialized_panel.width;
699 //todo!(collapsed_channels)692 panel.collapsed_channels = serialized_panel
700 // panel.collapsed_channels = serialized_panel693 .collapsed_channels
701 // .collapsed_channels694 .unwrap_or_else(|| Vec::new());
702 // .unwrap_or_else(|| Vec::new());
703 cx.notify();695 cx.notify();
704 });696 });
705 }697 }
@@ -707,25 +699,25 @@ impl CollabPanel { · 1 unmodified line
707 })699 })
708 }700 }
709 701
710 // fn serialize(&mut self, cx: &mut ViewContext<Self>) {702 fn serialize(&mut self, cx: &mut ViewContext<Self>) {
711 // let width = self.width;703 let width = self.width;
712 // let collapsed_channels = self.collapsed_channels.clone();704 let collapsed_channels = self.collapsed_channels.clone();
713 // self.pending_serialization = cx.background().spawn(705 self.pending_serialization = cx.background_executor().spawn(
714 // async move {706 async move {
715 // KEY_VALUE_STORE707 KEY_VALUE_STORE
716 // .write_kvp(708 .write_kvp(
717 // COLLABORATION_PANEL_KEY.into(),709 COLLABORATION_PANEL_KEY.into(),
718 // serde_json::to_string(&SerializedCollabPanel {710 serde_json::to_string(&SerializedCollabPanel {
719 // width,711 width,
720 // collapsed_channels: Some(collapsed_channels),712 collapsed_channels: Some(collapsed_channels),
721 // })?,713 })?,
722 // )714 )
723 // .await?;715 .await?;
724 // anyhow::Ok(())716 anyhow::Ok(())
725 // }717 }
726 // .log_err(),718 .log_err(),
727 // );719 );
728 // }720 }
729 721
730 fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {722 fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
731 let channel_store = self.channel_store.read(cx);723 let channel_store = self.channel_store.read(cx);
@@ -1456,16 +1448,16 @@ impl CollabPanel { · 724 unmodified lines
1456 // .into_any()1448 // .into_any()
1457 // }1449 // }
1458 1450
1459 // fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {1451 fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1460 // if let Some(_) = self.channel_editing_state.take() {1452 if let Some(_) = self.channel_editing_state.take() {
1461 // self.channel_name_editor.update(cx, |editor, cx| {1453 self.channel_name_editor.update(cx, |editor, cx| {
1462 // editor.set_text("", cx);1454 editor.set_text("", cx);
1463 // });1455 });
1464 // true1456 true
1465 // } else {1457 } else {
1466 // false1458 false
1467 // }1459 }
1468 // }1460 }
1469 1461
1470 // fn render_contact_placeholder(1462 // fn render_contact_placeholder(
1471 // &self,1463 // &self,
@@ -1501,67 +1493,6 @@ impl CollabPanel { · 29 unmodified lines
1501 // .into_any()1493 // .into_any()
1502 // }1494 // }
1503 1495
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
1565 // fn render_channel_notes(1496 // fn render_channel_notes(
1566 // &self,1497 // &self,
1567 // channel_id: ChannelId,1498 // channel_id: ChannelId,
@@ -1754,109 +1685,6 @@ impl CollabPanel { · 186 unmodified lines
1754 // .into_any()1685 // .into_any()
1755 // }1686 // }
1756 1687
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
1860 // fn has_subchannels(&self, ix: usize) -> bool {1688 // fn has_subchannels(&self, ix: usize) -> bool {
1861 // self.entries.get(ix).map_or(false, |entry| {1689 // self.entries.get(ix).map_or(false, |entry| {
1862 // if let ListEntry::Channel { has_children, .. } = entry {1690 // if let ListEntry::Channel { has_children, .. } = entry {
@@ -2054,148 +1882,148 @@ impl CollabPanel { · 191 unmodified lines
2054 // cx.notify();1882 // cx.notify();
2055 // }1883 // }
2056 1884
2057 // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {1885 fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
2058 // if self.confirm_channel_edit(cx) {1886 if self.confirm_channel_edit(cx) {
2059 // return;1887 return;
2060 // }1888 }
2061 1889
2062 // if let Some(selection) = self.selection {1890 // if let Some(selection) = self.selection {
2063 // if let Some(entry) = self.entries.get(selection) {1891 // if let Some(entry) = self.entries.get(selection) {
2064 // match entry {1892 // match entry {
2065 // ListEntry::Header(section) => match section {1893 // ListEntry::Header(section) => match section {
2066 // Section::ActiveCall => Self::leave_call(cx),1894 // Section::ActiveCall => Self::leave_call(cx),
2067 // Section::Channels => self.new_root_channel(cx),1895 // Section::Channels => self.new_root_channel(cx),
2068 // Section::Contacts => self.toggle_contact_finder(cx),1896 // Section::Contacts => self.toggle_contact_finder(cx),
2069 // Section::ContactRequests1897 // Section::ContactRequests
2070 // | Section::Online1898 // | Section::Online
2071 // | Section::Offline1899 // | Section::Offline
2072 // | Section::ChannelInvites => {1900 // | Section::ChannelInvites => {
2073 // self.toggle_section_expanded(*section, cx);1901 // self.toggle_section_expanded(*section, cx);
2074 // }1902 // }
2075 // },1903 // },
2076 // ListEntry::Contact { contact, calling } => {1904 // ListEntry::Contact { contact, calling } => {
2077 // if contact.online && !contact.busy && !calling {1905 // if contact.online && !contact.busy && !calling {
2078 // self.call(contact.user.id, Some(self.project.clone()), cx);1906 // self.call(contact.user.id, Some(self.project.clone()), cx);
2079 // }1907 // }
2080 // }1908 // }
2081 // ListEntry::ParticipantProject {1909 // ListEntry::ParticipantProject {
2082 // project_id,1910 // project_id,
2083 // host_user_id,1911 // host_user_id,
2084 // ..1912 // ..
2085 // } => {1913 // } => {
2086 // if let Some(workspace) = self.workspace.upgrade(cx) {1914 // if let Some(workspace) = self.workspace.upgrade(cx) {
2087 // let app_state = workspace.read(cx).app_state().clone();1915 // let app_state = workspace.read(cx).app_state().clone();
2088 // workspace::join_remote_project(1916 // workspace::join_remote_project(
2089 // *project_id,1917 // *project_id,
2090 // *host_user_id,1918 // *host_user_id,
2091 // app_state,1919 // app_state,
2092 // cx,1920 // cx,
2093 // )1921 // )
2094 // .detach_and_log_err(cx);1922 // .detach_and_log_err(cx);
2095 // }1923 // }
2096 // }1924 // }
2097 // ListEntry::ParticipantScreen { peer_id, .. } => {1925 // ListEntry::ParticipantScreen { peer_id, .. } => {
2098 // let Some(peer_id) = peer_id else {1926 // let Some(peer_id) = peer_id else {
2099 // return;1927 // return;
2100 // };1928 // };
2101 // if let Some(workspace) = self.workspace.upgrade(cx) {1929 // if let Some(workspace) = self.workspace.upgrade(cx) {
2102 // workspace.update(cx, |workspace, cx| {1930 // workspace.update(cx, |workspace, cx| {
2103 // workspace.open_shared_screen(*peer_id, cx)1931 // workspace.open_shared_screen(*peer_id, cx)
2104 // });1932 // });
2105 // }1933 // }
2106 // }1934 // }
2107 // ListEntry::Channel { channel, .. } => {1935 // ListEntry::Channel { channel, .. } => {
2108 // let is_active = maybe!({1936 // let is_active = maybe!({
2109 // let call_channel = ActiveCall::global(cx)1937 // let call_channel = ActiveCall::global(cx)
2110 // .read(cx)1938 // .read(cx)
2111 // .room()?1939 // .room()?
2112 // .read(cx)1940 // .read(cx)
2113 // .channel_id()?;1941 // .channel_id()?;
2114 1942
2115 // Some(call_channel == channel.id)1943 // Some(call_channel == channel.id)
2116 // })1944 // })
2117 // .unwrap_or(false);1945 // .unwrap_or(false);
2118 // if is_active {1946 // if is_active {
2119 // self.open_channel_notes(1947 // self.open_channel_notes(
2120 // &OpenChannelNotes {1948 // &OpenChannelNotes {
2121 // channel_id: channel.id,1949 // channel_id: channel.id,
2122 // },1950 // },
2123 // cx,1951 // cx,
2124 // )1952 // )
2125 // } else {1953 // } else {
2126 // self.join_channel(channel.id, cx)1954 // self.join_channel(channel.id, cx)
2127 // }1955 // }
2128 // }1956 // }
2129 // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),1957 // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2130 // _ => {}1958 // _ => {}
2131 // }1959 // }
2132 // }1960 // }
2133 // }1961 // }
2134 // }1962 }
2135 1963
2136 // fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {1964 fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
2137 // if self.channel_editing_state.is_some() {1965 if self.channel_editing_state.is_some() {
2138 // self.channel_name_editor.update(cx, |editor, cx| {1966 self.channel_name_editor.update(cx, |editor, cx| {
2139 // editor.insert(" ", cx);1967 editor.insert(" ", cx);
2140 // });1968 });
2141 // }1969 }
2142 // }1970 }
2143 1971
2144 // fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {1972 fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2145 // if let Some(editing_state) = &mut self.channel_editing_state {1973 if let Some(editing_state) = &mut self.channel_editing_state {
2146 // match editing_state {1974 match editing_state {
2147 // ChannelEditingState::Create {1975 ChannelEditingState::Create {
2148 // location,1976 location,
2149 // pending_name,1977 pending_name,
2150 // ..1978 ..
2151 // } => {1979 } => {
2152 // if pending_name.is_some() {1980 if pending_name.is_some() {
2153 // return false;1981 return false;
2154 // }1982 }
2155 // let channel_name = self.channel_name_editor.read(cx).text(cx);1983 let channel_name = self.channel_name_editor.read(cx).text(cx);
2156 1984
2157 // *pending_name = Some(channel_name.clone());1985 *pending_name = Some(channel_name.clone());
2158 1986
2159 // self.channel_store1987 self.channel_store
2160 // .update(cx, |channel_store, cx| {1988 .update(cx, |channel_store, cx| {
2161 // channel_store.create_channel(&channel_name, *location, cx)1989 channel_store.create_channel(&channel_name, *location, cx)
2162 // })1990 })
2163 // .detach();1991 .detach();
2164 // cx.notify();1992 cx.notify();
2165 // }1993 }
2166 // ChannelEditingState::Rename {1994 ChannelEditingState::Rename {
2167 // location,1995 location,
2168 // pending_name,1996 pending_name,
2169 // } => {1997 } => {
2170 // if pending_name.is_some() {1998 if pending_name.is_some() {
2171 // return false;1999 return false;
2172 // }2000 }
2173 // let channel_name = self.channel_name_editor.read(cx).text(cx);2001 let channel_name = self.channel_name_editor.read(cx).text(cx);
2174 // *pending_name = Some(channel_name.clone());2002 *pending_name = Some(channel_name.clone());
2175 2003
2176 // self.channel_store2004 self.channel_store
2177 // .update(cx, |channel_store, cx| {2005 .update(cx, |channel_store, cx| {
2178 // channel_store.rename(*location, &channel_name, cx)2006 channel_store.rename(*location, &channel_name, cx)
2179 // })2007 })
2180 // .detach();2008 .detach();
2181 // cx.notify();2009 cx.notify();
2182 // }2010 }
2183 // }2011 }
2184 // cx.focus_self();2012 cx.focus_self();
2185 // true2013 true
2186 // } else {2014 } else {
2187 // false2015 false
2188 // }2016 }
2189 // }2017 }
2190 2018
2191 // fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {2019 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) {2020 if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2193 // self.collapsed_sections.remove(ix);2021 self.collapsed_sections.remove(ix);
2194 // } else {2022 } else {
2195 // self.collapsed_sections.push(section);2023 self.collapsed_sections.push(section);
2196 // }2024 }
2197 // self.update_entries(false, cx);2025 self.update_entries(false, cx);
2198 // }2026 }
2199 2027
2200 // fn collapse_selected_channel(2028 // fn collapse_selected_channel(
2201 // &mut self,2029 // &mut self,
@@ -2233,20 +2061,20 @@ impl CollabPanel { · 31 unmodified lines
2233 // self.toggle_channel_collapsed(action.location, cx);2061 // self.toggle_channel_collapsed(action.location, cx);
2234 // }2062 // }
2235 2063
2236 // fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {2064 fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2237 // match self.collapsed_channels.binary_search(&channel_id) {2065 match self.collapsed_channels.binary_search(&channel_id) {
2238 // Ok(ix) => {2066 Ok(ix) => {
2239 // self.collapsed_channels.remove(ix);2067 self.collapsed_channels.remove(ix);
2240 // }2068 }
2241 // Err(ix) => {2069 Err(ix) => {
2242 // self.collapsed_channels.insert(ix, channel_id);2070 self.collapsed_channels.insert(ix, channel_id);
2243 // }2071 }
2244 // };2072 };
2245 // self.serialize(cx);2073 self.serialize(cx);
2246 // self.update_entries(true, cx);2074 self.update_entries(true, cx);
2247 // cx.notify();2075 cx.notify();
2248 // cx.focus_self();2076 cx.focus_self();
2249 // }2077 }
2250 2078
2251 fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {2079 fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
2252 self.collapsed_channels.binary_search(&channel_id).is_ok()2080 self.collapsed_channels.binary_search(&channel_id).is_ok()
@@ -2270,23 +2098,23 @@ impl CollabPanel { · 17 unmodified lines
2270 }2098 }
2271 }2099 }
2272 2100
2273 // fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {2101 fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2274 // self.channel_editing_state = Some(ChannelEditingState::Create {2102 self.channel_editing_state = Some(ChannelEditingState::Create {
2275 // location: None,2103 location: None,
2276 // pending_name: None,2104 pending_name: None,
2277 // });2105 });
2278 // self.update_entries(false, cx);2106 self.update_entries(false, cx);
2279 // self.select_channel_editor();2107 self.select_channel_editor();
2280 // cx.focus(self.channel_name_editor.as_any());2108 cx.focus_view(&self.channel_name_editor);
2281 // cx.notify();2109 cx.notify();
2282 // }2110 }
2283 2111
2284 // fn select_channel_editor(&mut self) {2112 fn select_channel_editor(&mut self) {
2285 // self.selection = self.entries.iter().position(|entry| match entry {2113 self.selection = self.entries.iter().position(|entry| match entry {
2286 // ListEntry::ChannelEditor { .. } => true,2114 ListEntry::ChannelEditor { .. } => true,
2287 // _ => false,2115 _ => false,
2288 // });2116 });
2289 // }2117 }
2290 2118
2291 // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {2119 // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2292 // self.collapsed_channels2120 // self.collapsed_channels
@@ -2346,11 +2174,12 @@ impl CollabPanel { · 53 unmodified lines
2346 // }2174 // }
2347 // }2175 // }
2348 2176
2349 // fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {2177 fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
2350 // if let Some(workspace) = self.workspace.upgrade(cx) {2178 if let Some(workspace) = self.workspace.upgrade() {
2351 // ChannelView::open(action.channel_id, workspace, cx).detach();2179 todo!();
2352 // }2180 // ChannelView::open(action.channel_id, workspace, cx).detach();
2353 // }2181 }
2182 }
2354 2183
2355 // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {2184 // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
2356 // let Some(channel) = self.selected_channel() else {2185 // let Some(channel) = self.selected_channel() else {
@@ -2439,44 +2268,38 @@ impl CollabPanel { · 82 unmodified lines
2439 // // Should move to the filter editor if clicking on it2268 // // Should move to the filter editor if clicking on it
2440 // // Should move selection to the channel editor if activating it2269 // // Should move selection to the channel editor if activating it
2441 2270
2442 // fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {2271 fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2443 // let user_store = self.user_store.clone();2272 let user_store = self.user_store.clone();
2444 // let prompt_message = format!(2273 let prompt_message = format!(
2445 // "Are you sure you want to remove \"{}\" from your contacts?",2274 "Are you sure you want to remove \"{}\" from your contacts?",
2446 // github_login2275 github_login
2447 // );2276 );
2448 // let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);2277 let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2449 // let window = cx.window();2278 let window = cx.window();
2450 // cx.spawn(|_, mut cx| async move {2279 cx.spawn(|_, mut cx| async move {
2451 // if answer.next().await == Some(0) {2280 if answer.await? == 0 {
2452 // if let Err(e) = user_store2281 user_store
2453 // .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))2282 .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
2454 // .await2283 .await
2455 // {2284 .notify_async_err(&mut cx);
2456 // window.prompt(2285 }
2457 // PromptLevel::Info,2286 anyhow::Ok(())
2458 // &format!("Failed to remove contact: {}", e),2287 })
2459 // &["Ok"],2288 .detach_and_log_err(cx);
2460 // &mut cx,2289 }
2461 // );
2462 // }
2463 // }
2464 // })
2465 // .detach();
2466 // }
2467 2290
2468 // fn respond_to_contact_request(2291 fn respond_to_contact_request(
2469 // &mut self,2292 &mut self,
2470 // user_id: u64,2293 user_id: u64,
2471 // accept: bool,2294 accept: bool,
2472 // cx: &mut ViewContext<Self>,2295 cx: &mut ViewContext<Self>,
2473 // ) {2296 ) {
2474 // self.user_store2297 self.user_store
2475 // .update(cx, |store, cx| {2298 .update(cx, |store, cx| {
2476 // store.respond_to_contact_request(user_id, accept, cx)2299 store.respond_to_contact_request(user_id, accept, cx)
2477 // })2300 })
2478 // .detach();2301 .detach_and_log_err(cx);
2479 // }2302 }
2480 2303
2481 // fn respond_to_channel_invite(2304 // fn respond_to_channel_invite(
2482 // &mut self,2305 // &mut self,
@@ -2504,21 +2327,22 @@ impl CollabPanel { · 21 unmodified lines
2504 // .detach_and_log_err(cx);2327 // .detach_and_log_err(cx);
2505 // }2328 // }
2506 2329
2507 // fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {2330 fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
2508 // let Some(workspace) = self.workspace.upgrade(cx) else {2331 let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
2509 // return;2332 return;
2510 // };2333 };
2511 // let Some(handle) = cx.window().downcast::<Workspace>() else {2334 let active_call = ActiveCall::global(cx);
2512 // return;2335 cx.spawn(|_, mut cx| async move {
2513 // };2336 active_call
2514 // workspace::join_channel(2337 .update(&mut cx, |active_call, cx| {
2515 // channel_id,2338 active_call.join_channel(channel_id, Some(handle), cx)
2516 // workspace.read(cx).app_state().clone(),2339 })
2517 // Some(handle),2340 .log_err()?
2518 // cx,2341 .await
2519 // )2342 .notify_async_err(&mut cx)
2520 // .detach_and_log_err(cx)2343 })
2521 // }2344 .detach()
2345 }
2522 2346
2523 // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {2347 // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
2524 // let channel_id = action.channel_id;2348 // let channel_id = action.channel_id;
@@ -2590,7 +2414,9 @@ impl CollabPanel { · 65 unmodified lines
2590 } => self2414 } => self
2591 .render_channel(&*channel, depth, has_children, is_selected, cx)2415 .render_channel(&*channel, depth, has_children, is_selected, cx)
2592 .into_any_element(),2416 .into_any_element(),
2593 ListEntry::ChannelEditor { depth } => todo!(),2417 ListEntry::ChannelEditor { depth } => {
2418 self.render_channel_editor(depth, cx).into_any_element()
2419 }
2594 }2420 }
2595 }))2421 }))
2596 }2422 }
@@ -2689,10 +2515,7 @@ impl CollabPanel { · 92 unmodified lines
2689 2515
2690 Some(2516 Some(
2691 IconButton::new("add-channel", Icon::Plus)2517 IconButton::new("add-channel", Icon::Plus)
2692 .on_click(cx.listener(|this, _, cx| {2518 .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
2693 todo!()
2694 // this.new_root_channel(cx)
2695 }))
2696 .tooltip(|cx| Tooltip::text("Create a channel", cx)),2519 .tooltip(|cx| Tooltip::text("Create a channel", cx)),
2697 )2520 )
2698 }2521 }
@@ -2707,18 +2530,16 @@ impl CollabPanel { · 8 unmodified lines
2707 | Section::Offline => true,2530 | Section::Offline => true,
2708 };2531 };
2709 2532
2710 let mut header = ListHeader::new(text);2533 ListHeader::new(text)
2711 if let Some(button) = button {2534 .when_some(button, |el, button| el.right_button(button))
2712 header = header.right_button(button)2535 .selected(is_selected)
2713 }2536 .when(can_collapse, |el| {
2714 // todo!() is selected2537 el.toggle(ui::Toggle::Toggled(is_collapsed)).on_toggle(
2715 if can_collapse {2538 cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)),
2716 // todo!() on click to toggle2539 )
2717 header = header.toggle(ui::Toggle::Toggled(is_collapsed));2540 })
2718 }
2719
2720 header
2721 }2541 }
2542
2722 fn render_contact(2543 fn render_contact(
2723 &mut self,2544 &mut self,
2724 contact: &Contact,2545 contact: &Contact,
@@ -2742,10 +2563,32 @@ impl CollabPanel { · 17 unmodified lines
2742 })2563 })
2743 .log_err();2564 .log_err();
2744 }))2565 }))
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
2746 if let Some(avatar) = contact.user.avatar.clone() {2587 if let Some(avatar) = contact.user.avatar.clone() {
2747 //item = item.left_avatar(avatar);2588 item = item.left_avatar(avatar);
2748 }2589 }
2590
2591 div().group("").child(item)
2749 // let event_handler =2592 // let event_handler =
2750 // MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {2593 // MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
2751 // Flex::row()2594 // Flex::row()
@@ -2774,40 +2617,7 @@ impl CollabPanel { · 22 unmodified lines
2774 // )2617 // )
2775 // .with_children(status_badge)2618 // .with_children(status_badge)
2776 // }))2619 // }))
2777 // .with_child(2620
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 // })
2811 // .with_children(if calling {2621 // .with_children(if calling {
2812 // Some(2622 // Some(
2813 // Label::new("Calling", collab_theme.calling_indicator.text.clone())2623 // Label::new("Calling", collab_theme.calling_indicator.text.clone())
@@ -2865,8 +2675,6 @@ impl CollabPanel { · 51 unmodified lines
2865 // )2675 // )
2866 // .into_any()2676 // .into_any()
2867 // };2677 // };
2868
2869 item
2870 }2678 }
2871 2679
2872 fn render_contact_request(2680 fn render_contact_request(
@@ -2877,104 +2685,48 @@ impl CollabPanel { · 4 unmodified lines
2877 cx: &mut ViewContext<Self>,2685 cx: &mut ViewContext<Self>,
2878 ) -> impl IntoElement {2686 ) -> impl IntoElement {
2879 let github_login = SharedString::from(user.github_login.clone());2687 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 };
2880 2695
2881 let mut item = ListItem::new(github_login.clone())2696 let controls = if is_incoming {
2882 .child(Label::new(github_login.clone()))2697 vec![
2883 .on_click(cx.listener(|this, _, cx| {2698 IconButton::new("remove_contact", Icon::Close)
2884 todo!();2699 .on_click(cx.listener(move |this, _, cx| {
2885 }));2700 this.respond_to_contact_request(user_id, false, cx);
2886 if let Some(avatar) = user.avatar.clone() {2701 }))
2887 item = item.left_avatar(avatar);2702 .color(color)
2888 }2703 .tooltip(|cx| Tooltip::text("Decline invite", cx)),
2889 // .with_children(user.avatar.clone().map(|avatar| {2704 IconButton::new("remove_contact", Icon::Check)
2890 // Image::from_data(avatar)2705 .on_click(cx.listener(move |this, _, cx| {
2891 // .with_style(theme.contact_avatar)2706 this.respond_to_contact_request(user_id, true, cx);
2892 // .aligned()2707 }))
2893 // .left()2708 .color(color)
2894 // }))2709 .tooltip(|cx| Tooltip::text("Accept invite", cx)),
2895 // .with_child(2710 ]
2896 // Label::new(2711 } else {
2897 // user.github_login.clone(),2712 let github_login = github_login.clone();
2898 // theme.contact_username.text.clone(),2713 vec![IconButton::new("remove_contact", Icon::Close)
2899 // )2714 .on_click(cx.listener(move |this, _, cx| {
2900 // .contained()2715 this.remove_contact(user_id, &github_login, cx);
2901 // .with_style(theme.contact_username.container)2716 }))
2902 // .aligned()2717 .color(color)
2903 // .left()2718 .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
2904 // .flex(1., true),2719 };
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 // }
2966 2720
2967 // row.constrained()2721 ListItem::new(github_login.clone())
2968 // .with_height(theme.row_height)2722 .child(
2969 // .contained()2723 h_stack()
2970 // .with_style(2724 .w_full()
2971 // *theme2725 .justify_between()
2972 // .contact_row2726 .child(Label::new(github_login.clone()))
2973 // .in_state(is_selected)2727 .child(h_stack().children(controls)),
2974 // .style_for(&mut Default::default()),2728 )
2975 // )2729 .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar))
2976 // .into_any()
2977 item
2978 }2730 }
2979 2731
2980 fn render_contact_placeholder(2732 fn render_contact_placeholder(
@@ -2983,34 +2735,10 @@ impl CollabPanel { · 2 unmodified lines
2983 cx: &mut ViewContext<Self>,2735 cx: &mut ViewContext<Self>,
2984 ) -> impl IntoElement {2736 ) -> impl IntoElement {
2985 ListItem::new("contact-placeholder")2737 ListItem::new("contact-placeholder")
2738 .child(IconElement::new(Icon::Plus))
2986 .child(Label::new("Add a Contact"))2739 .child(Label::new("Add a Contact"))
2987 .on_click(cx.listener(|this, _, cx| todo!()))2740 .selected(is_selected)
2988 // enum AddContacts {}2741 .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
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()
3014 }2742 }
3015 2743
3016 fn render_channel(2744 fn render_channel(
@@ -3023,6 +2751,15 @@ impl CollabPanel { · 6 unmodified lines
3023 ) -> impl IntoElement {2751 ) -> impl IntoElement {
3024 let channel_id = channel.id;2752 let channel_id = channel.id;
3025 2753
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);
3026 let is_public = self2763 let is_public = self
3027 .channel_store2764 .channel_store
3028 .read(cx)2765 .read(cx)
@@ -3034,17 +2771,7 @@ impl CollabPanel { · 5 unmodified lines
3034 .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok())2771 .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok())
3035 .unwrap_or(false);2772 .unwrap_or(false);
3036 2773
3037 let is_active = maybe!({2774 let has_messages_notification = channel.unseen_message_id.is_some();
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;
3048 let has_notes_notification = channel.unseen_note_version.is_some();2775 let has_notes_notification = channel.unseen_note_version.is_some();
3049 2776
3050 const FACEPILE_LIMIT: usize = 3;2777 const FACEPILE_LIMIT: usize = 3;
@@ -3052,6 +2779,7 @@ impl CollabPanel { · 1 unmodified line
3052 2779
3053 let face_pile = if !participants.is_empty() {2780 let face_pile = if !participants.is_empty() {
3054 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);2781 let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
2782 let user = &participants[0];
3055 2783
3056 let result = FacePile {2784 let result = FacePile {
3057 faces: participants2785 faces: participants
@@ -3059,6 +2787,7 @@ impl CollabPanel { · 1 unmodified line
3059 .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))2787 .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element()))
3060 .take(FACEPILE_LIMIT)2788 .take(FACEPILE_LIMIT)
3061 .chain(if extra_count > 0 {2789 .chain(if extra_count > 0 {
2790 // todo!() @nate - this label looks wrong.
3062 Some(Label::new(format!("+{}", extra_count)).into_any_element())2791 Some(Label::new(format!("+{}", extra_count)).into_any_element())
3063 } else {2792 } else {
3064 None2793 None
@@ -3081,7 +2810,7 @@ impl CollabPanel { · 16 unmodified lines
3081 .w_full()2810 .w_full()
3082 .justify_between()2811 .justify_between()
3083 .child(2812 .child(
3084 div()2813 h_stack()
3085 .id(channel_id as usize)2814 .id(channel_id as usize)
3086 .child(Label::new(channel.name.clone()))2815 .child(Label::new(channel.name.clone()))
3087 .children(face_pile.map(|face_pile| face_pile.render(cx)))2816 .children(face_pile.map(|face_pile| face_pile.render(cx)))
@@ -3092,11 +2821,10 @@ impl CollabPanel { · 4 unmodified lines
3092 .child(2821 .child(
3093 div()2822 div()
3094 .id("channel_chat")2823 .id("channel_chat")
3095 .bg(gpui::blue())
3096 .when(!has_messages_notification, |el| el.invisible())2824 .when(!has_messages_notification, |el| el.invisible())
3097 .group_hover("", |style| style.visible())2825 .group_hover("", |style| style.visible())
3098 .child(2826 .child(
3099 IconButton::new("test_chat", Icon::MessageBubbles)2827 IconButton::new("channel_chat", Icon::MessageBubbles)
3100 .color(if has_messages_notification {2828 .color(if has_messages_notification {
3101 Color::Default2829 Color::Default
3102 } else {2830 } else {
@@ -3111,20 +2839,16 @@ impl CollabPanel { · 8 unmodified lines
3111 .when(!has_notes_notification, |el| el.invisible())2839 .when(!has_notes_notification, |el| el.invisible())
3112 .group_hover("", |style| style.visible())2840 .group_hover("", |style| style.visible())
3113 .child(2841 .child(
3114 div().child("Notes").id("test_notes").tooltip(|cx| {2842 IconButton::new("channel_notes", Icon::File)
3115 Tooltip::text("Open channel notes", cx)2843 .color(if has_notes_notification {
3116 }),2844 Color::Default
3117 ), // .child(2845 } else {
3118 // IconButton::new("channel_notes", Icon::File)2846 Color::Muted
3119 // .color(if has_notes_notification {2847 })
3120 // Color::Default2848 .tooltip(|cx| {
3121 // } else {2849 Tooltip::text("Open channel notes", cx)
3122 // Color::Muted2850 }),
3123 // })2851 ),
3124 // .tooltip(|cx| {
3125 // Tooltip::text("Open channel notes", cx)
3126 // }),
3127 // ),
3128 ),2852 ),
3129 ),2853 ),
3130 )2854 )
@@ -3133,7 +2857,18 @@ impl CollabPanel { · 2 unmodified lines
3133 } else {2857 } else {
3134 Toggle::NotToggleable2858 Toggle::NotToggleable
3135 })2859 })
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 }))
3137 .on_secondary_mouse_down(cx.listener(|this, _, cx| {2872 .on_secondary_mouse_down(cx.listener(|this, _, cx| {
3138 todo!() // open context menu2873 todo!() // open context menu
3139 })),2874 })),
@@ -3445,6 +3180,32 @@ impl CollabPanel { · 305 unmodified lines
3445 // .with_cursor_style(CursorStyle::PointingHand)3180 // .with_cursor_style(CursorStyle::PointingHand)
3446 // .into_any()3181 // .into_any()
3447 }3182 }
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 }
3448}3209}
3449 3210
3450// fn render_tree_branch(3211// fn render_tree_branch(
@@ -3499,6 +3260,8 @@ impl Render for CollabPanel { · 48 unmodified lines
3499 div()3260 div()
3500 .key_context("CollabPanel")3261 .key_context("CollabPanel")
3501 .track_focus(&self.focus_handle)3262 .track_focus(&self.focus_handle)
3263 .on_action(cx.listener(Self::confirm))
3264 .on_action(cx.listener(Self::insert_space))
3502 .map(|el| {3265 .map(|el| {
3503 if self.user_store.read(cx).current_user().is_none() {3266 if self.user_store.read(cx).current_user().is_none() {
3504 el.child(self.render_signed_out(cx))3267 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
181 ContactRequestStatus::RequestSent => Some("icons/x.svg"),181 ContactRequestStatus::RequestSent => Some("icons/x.svg"),
182 ContactRequestStatus::RequestAccepted => None,182 ContactRequestStatus::RequestAccepted => None,
183 };183 };
184 dbg!(icon_path);
185 Some(184 Some(
186 div()185 div()
187 .flex_1()186 .flex_1()
crates/collab_ui2/src/collab_titlebar_item.rs+25−1
@@ -37,7 +37,10 @@ use gpui::{ · 36 unmodified lines
37};37};
38use project::Project;38use project::Project;
39use theme::ActiveTheme;39use theme::ActiveTheme;
40use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};40use ui::{
41 h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton,
42 IconElement, IconSize, KeyBinding, Tooltip,
43};
41use util::ResultExt;44use util::ResultExt;
42use workspace::{notifications::NotifyResultExt, Workspace};45use workspace::{notifications::NotifyResultExt, Workspace};
43 46
@@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem { · 254 unmodified lines
298 })301 })
299 .detach();302 .detach();
300 }))303 }))
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 )
301 }325 }
302 })326 })
303 }327 }
crates/command_palette2/src/command_palette.rs+6−4
@@ -1,3 +1,8 @@
1use std::{
2 cmp::{self, Reverse},
3 sync::Arc,
4};
5
1use collections::{CommandPaletteFilter, HashMap};6use collections::{CommandPaletteFilter, HashMap};
2use fuzzy::{StringMatch, StringMatchCandidate};7use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{8use gpui::{
@@ -5,10 +10,7 @@ use gpui::{ · 1 unmodified line
5 Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,10 Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
6};11};
7use picker::{Picker, PickerDelegate};12use picker::{Picker, PickerDelegate};
8use std::{13
9 cmp::{self, Reverse},
10 sync::Arc,
11};
12use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};14use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
13use util::{15use util::{
14 channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},16 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
30 30
31impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>31impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
32where32where
33 V: 'static + Send + Sync,33 V: 'static,
34{34{
35 fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription35 fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
36 where36 where
37 F: Fn(bool, &mut V, &mut ViewContext<V>) + Send + Sync + 'static,37 F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
38 {38 {
39 self.observe_global::<FeatureFlags>(move |v, cx| {39 self.observe_global::<FeatureFlags>(move |v, cx| {
40 let feature_flags = cx.global::<FeatureFlags>();40 let feature_flags = cx.global::<FeatureFlags>();
crates/gpui2/src/action.rs+1−0
@@ -162,6 +162,7 @@ macro_rules! actions { · 161 unmodified lines
162 162
163 ( $name:ident ) => {163 ( $name:ident ) => {
164 #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]164 #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
165 #[serde(crate = "gpui::serde")]
165 pub struct $name;166 pub struct $name;
166 };167 };
167 168
crates/gpui2/src/element.rs+32−12
@@ -111,7 +111,7 @@ pub struct Component<C> { · 110 unmodified lines
111 111
112pub struct CompositeElementState<C: RenderOnce> {112pub struct CompositeElementState<C: RenderOnce> {
113 rendered_element: Option<<C::Rendered as IntoElement>::Element>,113 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>,
115}115}
116 116
117impl<C> Component<C> {117impl<C> Component<C> {
@@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> { · 13 unmodified lines
131 cx: &mut WindowContext,131 cx: &mut WindowContext,
132 ) -> (LayoutId, Self::State) {132 ) -> (LayoutId, Self::State) {
133 let mut element = self.component.take().unwrap().render(cx).into_element();133 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);134 if let Some(element_id) = element.element_id() {
135 let state = CompositeElementState {135 let layout_id =
136 rendered_element: Some(element),136 cx.with_element_state(element_id, |state, cx| element.layout(state, cx));
137 rendered_element_state: state,137 let state = CompositeElementState {
138 };138 rendered_element: Some(element),
139 (layout_id, state)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 }
140 }151 }
141 152
142 fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {153 fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
143 state154 let element = state.rendered_element.take().unwrap();
144 .rendered_element155 if let Some(element_id) = element.element_id() {
145 .take()156 cx.with_element_state(element_id, |element_state, cx| {
146 .unwrap()157 let mut element_state = element_state.unwrap();
147 .paint(bounds, &mut state.rendered_element_state, cx);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 }
148 }168 }
149}169}
150 170
crates/gpui2/src/elements/uniform_list.rs+2−4
@@ -173,7 +173,7 @@ impl Element for UniformList { · 172 unmodified lines
173 let item_size = element_state.item_size;173 let item_size = element_state.item_size;
174 let content_size = Size {174 let content_size = Size {
175 width: padded_bounds.size.width,175 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,
177 };177 };
178 178
179 let shared_scroll_offset = element_state179 let shared_scroll_offset = element_state
@@ -221,9 +221,7 @@ impl Element for UniformList { · 41 unmodified lines
221 221
222 let items = (self.render_items)(visible_range.clone(), cx);222 let items = (self.render_items)(visible_range.clone(), cx);
223 cx.with_z_index(1, |cx| {223 cx.with_z_index(1, |cx| {
224 let content_mask = ContentMask {224 let content_mask = ContentMask { bounds };
225 bounds: padded_bounds,
226 };
227 cx.with_content_mask(Some(content_mask), |cx| {225 cx.with_content_mask(Some(content_mask), |cx| {
228 for (item, ix) in items.into_iter().zip(visible_range) {226 for (item, ix) in items.into_iter().zip(visible_range) {
229 let item_origin = padded_bounds.origin227 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
1939 })1939 })
1940 }1940 }
1941 1941
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
1959 /// Obtain the current content mask.1942 /// Obtain the current content mask.
1960 fn content_mask(&self) -> ContentMask<Pixels> {1943 fn content_mask(&self) -> ContentMask<Pixels> {
1961 self.window()1944 self.window()
crates/picker2/src/picker2.rs+24−5
@@ -1,7 +1,8 @@
1use editor::Editor;1use editor::Editor;
2use gpui::{2use gpui::{
3 div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,3 div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
4 MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,4 MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
5 WindowContext,
5};6};
6use std::{cmp, sync::Arc};7use std::{cmp, sync::Arc};
7use ui::{prelude::*, v_stack, Color, Divider, Label};8use ui::{prelude::*, v_stack, Color, Divider, Label};
@@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> { · 8 unmodified lines
16 17
17pub trait PickerDelegate: Sized + 'static {18pub trait PickerDelegate: Sized + 'static {
18 type ListItem: IntoElement;19 type ListItem: IntoElement;
19
20 fn match_count(&self) -> usize;20 fn match_count(&self) -> usize;
21 fn selected_index(&self) -> usize;21 fn selected_index(&self) -> usize;
22 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);22 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
205 .when(self.delegate.match_count() > 0, |el| {205 .when(self.delegate.match_count() > 0, |el| {
206 el.child(206 el.child(
207 v_stack()207 v_stack()
208 .p_1()
209 .grow()208 .grow()
210 .child(209 .child(
211 uniform_list(210 uniform_list(
@@ -239,7 +238,8 @@ impl<D: PickerDelegate> Render for Picker<D> { · 27 unmodified lines
239 }238 }
240 },239 },
241 )240 )
242 .track_scroll(self.scroll_handle.clone()),241 .track_scroll(self.scroll_handle.clone())
242 .p_1()
243 )243 )
244 .max_h_72()244 .max_h_72()
245 .overflow_hidden(),245 .overflow_hidden(),
@@ -256,3 +256,22 @@ impl<D: PickerDelegate> Render for Picker<D> { · 10 unmodified lines
256 })256 })
257 }257 }
258}258}
259
260pub 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
13use serde::{Deserialize, Serialize};13use serde::{Deserialize, Serialize};
14use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};14use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
15 15
16#[derive(Clone)]
16pub enum Prettier {17pub enum Prettier {
17 Real(RealPrettier),18 Real(RealPrettier),
18 #[cfg(any(test, feature = "test-support"))]19 #[cfg(any(test, feature = "test-support"))]
19 Test(TestPrettier),20 Test(TestPrettier),
20}21}
21 22
23#[derive(Clone)]
22pub struct RealPrettier {24pub struct RealPrettier {
23 default: bool,25 default: bool,
24 prettier_dir: PathBuf,26 prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier { · 1 unmodified line
26}28}
27 29
28#[cfg(any(test, feature = "test-support"))]30#[cfg(any(test, feature = "test-support"))]
31#[derive(Clone)]
29pub struct TestPrettier {32pub struct TestPrettier {
30 prettier_dir: PathBuf,33 prettier_dir: PathBuf,
31 default: bool,34 default: bool,
32}35}
33 36
37pub const FAIL_THRESHOLD: usize = 4;
34pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";38pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
35pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");39pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
36const PRETTIER_PACKAGE_NAME: &str = "prettier";40const 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
153 const { method, id, params } = message;153 const { method, id, params } = message;
154 if (method === undefined) {154 if (method === undefined) {
155 throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);155 throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
156 } else if (method == "initialized") {
157 return;
156 }158 }
159
157 if (id === undefined) {160 if (id === undefined) {
158 throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);161 throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
159 }162 }
crates/prettier2/src/prettier2.rs+4−0
@@ -13,12 +13,14 @@ use std::{ · 12 unmodified lines
13};13};
14use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};14use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
15 15
16#[derive(Clone)]
16pub enum Prettier {17pub enum Prettier {
17 Real(RealPrettier),18 Real(RealPrettier),
18 #[cfg(any(test, feature = "test-support"))]19 #[cfg(any(test, feature = "test-support"))]
19 Test(TestPrettier),20 Test(TestPrettier),
20}21}
21 22
23#[derive(Clone)]
22pub struct RealPrettier {24pub struct RealPrettier {
23 default: bool,25 default: bool,
24 prettier_dir: PathBuf,26 prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier { · 1 unmodified line
26}28}
27 29
28#[cfg(any(test, feature = "test-support"))]30#[cfg(any(test, feature = "test-support"))]
31#[derive(Clone)]
29pub struct TestPrettier {32pub struct TestPrettier {
30 prettier_dir: PathBuf,33 prettier_dir: PathBuf,
31 default: bool,34 default: bool,
32}35}
33 36
37pub const FAIL_THRESHOLD: usize = 4;
34pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";38pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
35pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");39pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
36const PRETTIER_PACKAGE_NAME: &str = "prettier";40const 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
153 const { method, id, params } = message;153 const { method, id, params } = message;
154 if (method === undefined) {154 if (method === undefined) {
155 throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);155 throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
156 } else if (method == "initialized") {
157 return;
156 }158 }
159
157 if (id === undefined) {160 if (id === undefined) {
158 throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);161 throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
159 }162 }
crates/project/src/prettier_support.rs+758−0
@@ -0,0 +1,758 @@
1use std::{
2 ops::ControlFlow,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use anyhow::Context;
8use collections::HashSet;
9use fs::Fs;
10use futures::{
11 future::{self, Shared},
12 FutureExt,
13};
14use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task};
15use language::{
16 language_settings::{Formatter, LanguageSettings},
17 Buffer, Language, LanguageServerName, LocalFile,
18};
19use lsp::LanguageServerId;
20use node_runtime::NodeRuntime;
21use prettier::Prettier;
22use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
23
24use crate::{
25 Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
26};
27
28pub 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
51pub(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
104pub struct DefaultPrettier {
105 prettier: PrettierInstallation,
106 installed_plugins: HashSet<&'static str>,
107}
108
109pub 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
118pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
119
120#[derive(Clone)]
121pub struct PrettierInstance {
122 attempt: usize,
123 prettier: Option<PrettierTask>,
124}
125
126impl 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
139impl 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
165impl 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
212fn 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
284fn 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
307fn 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
357async 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
388async 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
405impl 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 @@
1mod ignore;1mod ignore;
2mod lsp_command;2mod lsp_command;
3mod prettier_support;
3pub mod project_settings;4pub mod project_settings;
4pub mod search;5pub mod search;
5pub mod terminals;6pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{ · 14 unmodified lines
20 mpsc::{self, UnboundedReceiver},21 mpsc::{self, UnboundedReceiver},
21 oneshot,22 oneshot,
22 },23 },
23 future::{self, try_join_all, Shared},24 future::{try_join_all, Shared},
24 stream::FuturesUnordered,25 stream::FuturesUnordered,
25 AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,26 AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
26};27};
@@ -31,9 +32,7 @@ use gpui::{ · 4 unmodified lines
31};32};
32use itertools::Itertools;33use itertools::Itertools;
33use language::{34use language::{
34 language_settings::{35 language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
35 language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
36 },
37 point_to_lsp,36 point_to_lsp,
38 proto::{37 proto::{
39 deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,38 deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*; · 14 unmodified lines
54use node_runtime::NodeRuntime;53use node_runtime::NodeRuntime;
55use parking_lot::Mutex;54use parking_lot::Mutex;
56use postage::watch;55use postage::watch;
57use prettier::Prettier;56use prettier_support::{DefaultPrettier, PrettierInstance};
58use project_settings::{LspSettings, ProjectSettings};57use project_settings::{LspSettings, ProjectSettings};
59use rand::prelude::*;58use rand::prelude::*;
60use search::SearchQuery;59use search::SearchQuery;
@@ -72,7 +71,7 @@ use std::{ · 11 unmodified lines
72 hash::Hash,71 hash::Hash,
73 mem,72 mem,
74 num::NonZeroU32,73 num::NonZeroU32,
75 ops::{ControlFlow, Range},74 ops::Range,
76 path::{self, Component, Path, PathBuf},75 path::{self, Component, Path, PathBuf},
77 process::Stdio,76 process::Stdio,
78 str,77 str,
@@ -85,11 +84,8 @@ use std::{ · 6 unmodified lines
85use terminals::Terminals;84use terminals::Terminals;
86use text::Anchor;85use text::Anchor;
87use util::{86use util::{
88 debug_panic, defer,87 debug_panic, defer, http::HttpClient, merge_json_value_into,
89 http::HttpClient,88 paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
90 merge_json_value_into,
91 paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
92 post_inc, ResultExt, TryFutureExt as _,
93};89};
94 90
95pub use fs::*;91pub use fs::*;
@@ -168,16 +164,9 @@ pub struct Project { · 72 unmodified lines
168 copilot_log_subscription: Option<lsp::Subscription>,164 copilot_log_subscription: Option<lsp::Subscription>,
169 current_lsp_settings: HashMap<Arc<str>, LspSettings>,165 current_lsp_settings: HashMap<Arc<str>, LspSettings>,
170 node: Option<Arc<dyn NodeRuntime>>,166 node: Option<Arc<dyn NodeRuntime>>,
171 default_prettier: Option<DefaultPrettier>,167 default_prettier: DefaultPrettier,
172 prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,168 prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
173 prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,169 prettier_instances: HashMap<PathBuf, PrettierInstance>,
174}
175
176struct 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>,
181}170}
182 171
183struct DelayedDebounced {172struct DelayedDebounced {
@@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate { · 358 unmodified lines
542 http_client: Arc<dyn HttpClient>,531 http_client: Arc<dyn HttpClient>,
543}532}
544 533
534// Currently, formatting operations are represented differently depending on
535// whether they come from a language server or an external command.
536enum FormatOperation {
537 Lsp(Vec<(Range<Anchor>, String)>),
538 External(Diff),
539 Prettier(Diff),
540}
541
545impl FormatTrigger {542impl FormatTrigger {
546 fn from_proto(value: i32) -> FormatTrigger {543 fn from_proto(value: i32) -> FormatTrigger {
547 match value {544 match value {
@@ -690,7 +687,7 @@ impl Project { · 142 unmodified lines
690 copilot_log_subscription: None,687 copilot_log_subscription: None,
691 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),688 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
692 node: Some(node),689 node: Some(node),
693 default_prettier: None,690 default_prettier: DefaultPrettier::default(),
694 prettiers_per_worktree: HashMap::default(),691 prettiers_per_worktree: HashMap::default(),
695 prettier_instances: HashMap::default(),692 prettier_instances: HashMap::default(),
696 }693 }
@@ -791,7 +788,7 @@ impl Project { · 94 unmodified lines
791 copilot_log_subscription: None,788 copilot_log_subscription: None,
792 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),789 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
793 node: None,790 node: None,
794 default_prettier: None,791 default_prettier: DefaultPrettier::default(),
795 prettiers_per_worktree: HashMap::default(),792 prettiers_per_worktree: HashMap::default(),
796 prettier_instances: HashMap::default(),793 prettier_instances: HashMap::default(),
797 };794 };
@@ -928,8 +925,19 @@ impl Project { · 130 unmodified lines
928 .detach();925 .detach();
929 }926 }
930 927
928 let mut prettier_plugins_by_worktree = HashMap::default();
931 for (worktree, language, settings) in language_formatters_to_check {929 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);
933 }941 }
934 942
935 // Start all the newly-enabled language servers.943 // Start all the newly-enabled language servers.
@@ -2685,8 +2693,11 @@ impl Project { · 1749 unmodified lines
2685 let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();2693 let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
2686 let buffer_file = File::from_dyn(buffer_file.as_ref());2694 let buffer_file = File::from_dyn(buffer_file.as_ref());
2687 let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));2695 let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
2688 2696 if let Some(prettier_plugins) =
2689 self.install_default_formatters(worktree, &new_language, &settings, cx);2697 prettier_support::prettier_plugins_for_language(&new_language, &settings)
2698 {
2699 self.install_default_prettier(worktree, prettier_plugins, cx);
2700 };
2690 if let Some(file) = buffer_file {2701 if let Some(file) = buffer_file {
2691 let worktree = file.worktree.clone();2702 let worktree = file.worktree.clone();
2692 if let Some(tree) = worktree.read(cx).as_local() {2703 if let Some(tree) = worktree.read(cx).as_local() {
@@ -4073,8 +4084,6 @@ impl Project { · 1380 unmodified lines
4073 4084
4074 let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;4085 let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
4075 let ensure_final_newline = settings.ensure_final_newline_on_save;4086 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();
4078 let tab_size = settings.tab_size;4087 let tab_size = settings.tab_size;
4079 4088
4080 // First, format buffer's whitespace according to the settings.4089 // First, format buffer's whitespace according to the settings.
@@ -4099,18 +4108,10 @@ impl Project { · 18 unmodified lines
4099 buffer.end_transaction(cx)4108 buffer.end_transaction(cx)
4100 });4109 });
4101 4110
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
4110 // Apply language-specific formatting using either a language server4111 // Apply language-specific formatting using either a language server
4111 // or external command.4112 // or external command.
4112 let mut format_operation = None;4113 let mut format_operation = None;
4113 match (formatter, format_on_save) {4114 match (&settings.formatter, &settings.format_on_save) {
4114 (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}4115 (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
4115 4116
4116 (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)4117 (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4155,46 +4156,11 @@ impl Project { · 38 unmodified lines
4155 }4156 }
4156 }4157 }
4157 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {4158 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
4158 if let Some((prettier_path, prettier_task)) = project4159 if let Some(new_operation) =
4159 .update(&mut cx, |project, cx| {4160 prettier_support::format_with_prettier(&project, buffer, &mut cx)
4160 project.prettier_instance_for_buffer(buffer, cx)4161 .await
4161 }).await {4162 {
4162 match prettier_task.await4163 format_operation = Some(new_operation);
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 }
4198 } else if let Some((language_server, buffer_abs_path)) =4164 } else if let Some((language_server, buffer_abs_path)) =
4199 language_server.as_ref().zip(buffer_abs_path.as_ref())4165 language_server.as_ref().zip(buffer_abs_path.as_ref())
4200 {4166 {
@@ -4212,48 +4178,13 @@ impl Project { · 11 unmodified lines
4212 ));4178 ));
4213 }4179 }
4214 }4180 }
4215 (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {4181 (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
4216 if let Some((prettier_path, prettier_task)) = project4182 if let Some(new_operation) =
4217 .update(&mut cx, |project, cx| {4183 prettier_support::format_with_prettier(&project, buffer, &mut cx)
4218 project.prettier_instance_for_buffer(buffer, cx)4184 .await
4219 }).await {4185 {
4220 match prettier_task.await4186 format_operation = Some(new_operation);
4221 {4187 }
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 }
4257 }4188 }
4258 };4189 };
4259 4190
@@ -6566,85 +6497,6 @@ impl Project { · 2306 unmodified lines
6566 .detach();6497 .detach();
6567 }6498 }
6568 6499
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
6648 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {6500 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
6649 let new_active_entry = entry.and_then(|project_path| {6501 let new_active_entry = entry.and_then(|project_path| {
6650 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;6502 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -8536,446 +8388,6 @@ impl Project { · 1885 unmodified lines
8536 Vec::new()8388 Vec::new()
8537 }8389 }
8538 }8390 }
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
8813fn 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
8862fn 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
8883fn 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")))]
8934async 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(())
8979}8391}
8980 8392
8981fn subscribe_for_copilot_events(8393fn subscribe_for_copilot_events(
crates/project2/src/prettier_support.rs+772−0
@@ -0,0 +1,772 @@
1use std::{
2 ops::ControlFlow,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use anyhow::Context;
8use collections::HashSet;
9use fs::Fs;
10use futures::{
11 future::{self, Shared},
12 FutureExt,
13};
14use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
15use language::{
16 language_settings::{Formatter, LanguageSettings},
17 Buffer, Language, LanguageServerName, LocalFile,
18};
19use lsp::LanguageServerId;
20use node_runtime::NodeRuntime;
21use prettier::Prettier;
22use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
23
24use crate::{
25 Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
26};
27
28pub 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
51pub(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
109pub struct DefaultPrettier {
110 prettier: PrettierInstallation,
111 installed_plugins: HashSet<&'static str>,
112}
113
114pub 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
123pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
124
125#[derive(Clone)]
126pub struct PrettierInstance {
127 attempt: usize,
128 prettier: Option<PrettierTask>,
129}
130
131impl 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
144impl 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
170impl 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
217fn 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
289fn 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
312fn 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
364async 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
395async 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
412impl 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 @@
1mod ignore;1mod ignore;
2mod lsp_command;2mod lsp_command;
3mod prettier_support;
3pub mod project_settings;4pub mod project_settings;
4pub mod search;5pub mod search;
5pub mod terminals;6pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{ · 14 unmodified lines
20 mpsc::{self, UnboundedReceiver},21 mpsc::{self, UnboundedReceiver},
21 oneshot,22 oneshot,
22 },23 },
23 future::{self, try_join_all, Shared},24 future::{try_join_all, Shared},
24 stream::FuturesUnordered,25 stream::FuturesUnordered,
25 AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,26 AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
26};27};
@@ -31,9 +32,7 @@ use gpui::{ · 4 unmodified lines
31};32};
32use itertools::Itertools;33use itertools::Itertools;
33use language::{34use language::{
34 language_settings::{35 language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
35 language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
36 },
37 point_to_lsp,36 point_to_lsp,
38 proto::{37 proto::{
39 deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,38 deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*; · 14 unmodified lines
54use node_runtime::NodeRuntime;53use node_runtime::NodeRuntime;
55use parking_lot::Mutex;54use parking_lot::Mutex;
56use postage::watch;55use postage::watch;
57use prettier::Prettier;56use prettier_support::{DefaultPrettier, PrettierInstance};
58use project_settings::{LspSettings, ProjectSettings};57use project_settings::{LspSettings, ProjectSettings};
59use rand::prelude::*;58use rand::prelude::*;
60use search::SearchQuery;59use search::SearchQuery;
@@ -70,7 +69,7 @@ use std::{ · 9 unmodified lines
70 hash::Hash,69 hash::Hash,
71 mem,70 mem,
72 num::NonZeroU32,71 num::NonZeroU32,
73 ops::{ControlFlow, Range},72 ops::Range,
74 path::{self, Component, Path, PathBuf},73 path::{self, Component, Path, PathBuf},
75 process::Stdio,74 process::Stdio,
76 str,75 str,
@@ -83,11 +82,8 @@ use std::{ · 6 unmodified lines
83use terminals::Terminals;82use terminals::Terminals;
84use text::Anchor;83use text::Anchor;
85use util::{84use util::{
86 debug_panic, defer,85 debug_panic, defer, http::HttpClient, merge_json_value_into,
87 http::HttpClient,86 paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
88 merge_json_value_into,
89 paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
90 post_inc, ResultExt, TryFutureExt as _,
91};87};
92 88
93pub use fs::*;89pub use fs::*;
@@ -166,16 +162,9 @@ pub struct Project { · 72 unmodified lines
166 copilot_log_subscription: Option<lsp::Subscription>,162 copilot_log_subscription: Option<lsp::Subscription>,
167 current_lsp_settings: HashMap<Arc<str>, LspSettings>,163 current_lsp_settings: HashMap<Arc<str>, LspSettings>,
168 node: Option<Arc<dyn NodeRuntime>>,164 node: Option<Arc<dyn NodeRuntime>>,
169 default_prettier: Option<DefaultPrettier>,165 default_prettier: DefaultPrettier,
170 prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,166 prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
171 prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,167 prettier_instances: HashMap<PathBuf, PrettierInstance>,
172}
173
174struct 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>,
179}168}
180 169
181struct DelayedDebounced {170struct DelayedDebounced {
@@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate { · 358 unmodified lines
540 http_client: Arc<dyn HttpClient>,529 http_client: Arc<dyn HttpClient>,
541}530}
542 531
532// Currently, formatting operations are represented differently depending on
533// whether they come from a language server or an external command.
534enum FormatOperation {
535 Lsp(Vec<(Range<Anchor>, String)>),
536 External(Diff),
537 Prettier(Diff),
538}
539
543impl FormatTrigger {540impl FormatTrigger {
544 fn from_proto(value: i32) -> FormatTrigger {541 fn from_proto(value: i32) -> FormatTrigger {
545 match value {542 match value {
@@ -689,7 +686,7 @@ impl Project { · 143 unmodified lines
689 copilot_log_subscription: None,686 copilot_log_subscription: None,
690 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),687 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
691 node: Some(node),688 node: Some(node),
692 default_prettier: None,689 default_prettier: DefaultPrettier::default(),
693 prettiers_per_worktree: HashMap::default(),690 prettiers_per_worktree: HashMap::default(),
694 prettier_instances: HashMap::default(),691 prettier_instances: HashMap::default(),
695 }692 }
@@ -792,7 +789,7 @@ impl Project { · 96 unmodified lines
792 copilot_log_subscription: None,789 copilot_log_subscription: None,
793 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),790 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
794 node: None,791 node: None,
795 default_prettier: None,792 default_prettier: DefaultPrettier::default(),
796 prettiers_per_worktree: HashMap::default(),793 prettiers_per_worktree: HashMap::default(),
797 prettier_instances: HashMap::default(),794 prettier_instances: HashMap::default(),
798 };795 };
@@ -965,8 +962,19 @@ impl Project { · 166 unmodified lines
965 .detach();962 .detach();
966 }963 }
967 964
965 let mut prettier_plugins_by_worktree = HashMap::default();
968 for (worktree, language, settings) in language_formatters_to_check {966 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);
970 }978 }
971 979
972 // Start all the newly-enabled language servers.980 // Start all the newly-enabled language servers.
@@ -2722,8 +2730,11 @@ impl Project { · 1749 unmodified lines
2722 let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();2730 let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
2723 let buffer_file = File::from_dyn(buffer_file.as_ref());2731 let buffer_file = File::from_dyn(buffer_file.as_ref());
2724 let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));2732 let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
2725 2733 if let Some(prettier_plugins) =
2726 self.install_default_formatters(worktree, &new_language, &settings, cx);2734 prettier_support::prettier_plugins_for_language(&new_language, &settings)
2735 {
2736 self.install_default_prettier(worktree, prettier_plugins, cx);
2737 };
2727 if let Some(file) = buffer_file {2738 if let Some(file) = buffer_file {
2728 let worktree = file.worktree.clone();2739 let worktree = file.worktree.clone();
2729 if let Some(tree) = worktree.read(cx).as_local() {2740 if let Some(tree) = worktree.read(cx).as_local() {
@@ -4126,7 +4137,8 @@ impl Project { · 1396 unmodified lines
4126 this.buffers_being_formatted4137 this.buffers_being_formatted
4127 .remove(&buffer.read(cx).remote_id());4138 .remove(&buffer.read(cx).remote_id());
4128 }4139 }
4129 }).ok();4140 })
4141 .ok();
4130 }4142 }
4131 });4143 });
4132 4144
@@ -4138,8 +4150,6 @@ impl Project { · 5 unmodified lines
4138 4150
4139 let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;4151 let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
4140 let ensure_final_newline = settings.ensure_final_newline_on_save;4152 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();
4143 let tab_size = settings.tab_size;4153 let tab_size = settings.tab_size;
4144 4154
4145 // First, format buffer's whitespace according to the settings.4155 // First, format buffer's whitespace according to the settings.
@@ -4164,18 +4174,10 @@ impl Project { · 18 unmodified lines
4164 buffer.end_transaction(cx)4174 buffer.end_transaction(cx)
4165 })?;4175 })?;
4166 4176
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
4175 // Apply language-specific formatting using either a language server4177 // Apply language-specific formatting using either a language server
4176 // or external command.4178 // or external command.
4177 let mut format_operation = None;4179 let mut format_operation = None;
4178 match (formatter, format_on_save) {4180 match (&settings.formatter, &settings.format_on_save) {
4179 (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}4181 (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
4180 4182
4181 (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)4183 (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4220,46 +4222,11 @@ impl Project { · 38 unmodified lines
4220 }4222 }
4221 }4223 }
4222 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {4224 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
4223 if let Some((prettier_path, prettier_task)) = project4225 if let Some(new_operation) =
4224 .update(&mut cx, |project, cx| {4226 prettier_support::format_with_prettier(&project, buffer, &mut cx)
4225 project.prettier_instance_for_buffer(buffer, cx)4227 .await
4226 })?.await {4228 {
4227 match prettier_task.await4229 format_operation = Some(new_operation);
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 }
4263 } else if let Some((language_server, buffer_abs_path)) =4230 } else if let Some((language_server, buffer_abs_path)) =
4264 language_server.as_ref().zip(buffer_abs_path.as_ref())4231 language_server.as_ref().zip(buffer_abs_path.as_ref())
4265 {4232 {
@@ -4277,48 +4244,13 @@ impl Project { · 11 unmodified lines
4277 ));4244 ));
4278 }4245 }
4279 }4246 }
4280 (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {4247 (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
4281 if let Some((prettier_path, prettier_task)) = project4248 if let Some(new_operation) =
4282 .update(&mut cx, |project, cx| {4249 prettier_support::format_with_prettier(&project, buffer, &mut cx)
4283 project.prettier_instance_for_buffer(buffer, cx)4250 .await
4284 })?.await {4251 {
4285 match prettier_task.await4252 format_operation = Some(new_operation);
4286 {4253 }
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 }
4322 }4254 }
4323 };4255 };
4324 4256
@@ -6638,84 +6570,6 @@ impl Project { · 2313 unmodified lines
6638 .detach();6570 .detach();
6639 }6571 }
6640 6572
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
6719 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {6573 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
6720 let new_active_entry = entry.and_then(|project_path| {6574 let new_active_entry = entry.and_then(|project_path| {
6721 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;6575 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -8579,486 +8433,6 @@ impl Project { · 1857 unmodified lines
8579 Vec::new()8433 Vec::new()
8580 }8434 }
8581 }8435 }
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
8880fn 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
8943fn 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
8964fn 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")))]
9017async 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(())
9062}8436}
9063 8437
9064fn subscribe_for_copilot_events(8438fn subscribe_for_copilot_events(
crates/project_panel2/src/file_associations.rs+31−40
@@ -41,56 +41,47 @@ impl FileAssociations { · 40 unmodified lines
41 })41 })
42 }42 }
43 43
44 pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {44 pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
45 maybe!({45 let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
46 let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
47 46
48 // FIXME: Associate a type with the languages and have the file's langauge47 // FIXME: Associate a type with the languages and have the file's langauge
49 // override these associations48 // override these associations
50 maybe!({49 maybe!({
51 let suffix = path.icon_suffix()?;50 let suffix = path.icon_suffix()?;
52 51
53 this.suffixes52 this.suffixes
54 .get(suffix)53 .get(suffix)
55 .and_then(|type_str| this.types.get(type_str))54 .and_then(|type_str| this.types.get(type_str))
56 .map(|type_config| type_config.icon.clone())55 .map(|type_config| type_config.icon.clone())
57 })
58 .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
59 })56 })
60 .unwrap_or_else(|| Arc::from("".to_string()))57 .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
61 }58 }
62 59
63 pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {60 pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
64 maybe!({61 let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
65 let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
66 62
67 let key = if expanded {63 let key = if expanded {
68 EXPANDED_DIRECTORY_TYPE64 EXPANDED_DIRECTORY_TYPE
69 } else {65 } else {
70 COLLAPSED_DIRECTORY_TYPE66 COLLAPSED_DIRECTORY_TYPE
71 };67 };
72 68
73 this.types69 this.types
74 .get(key)70 .get(key)
75 .map(|type_config| type_config.icon.clone())71 .map(|type_config| type_config.icon.clone())
76 })
77 .unwrap_or_else(|| Arc::from("".to_string()))
78 }72 }
79 73
80 pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {74 pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
81 maybe!({75 let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
82 let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
83 76
84 let key = if expanded {77 let key = if expanded {
85 EXPANDED_CHEVRON_TYPE78 EXPANDED_CHEVRON_TYPE
86 } else {79 } else {
87 COLLAPSED_CHEVRON_TYPE80 COLLAPSED_CHEVRON_TYPE
88 };81 };
89 82
90 this.types83 this.types
91 .get(key)84 .get(key)
92 .map(|type_config| type_config.icon.clone())85 .map(|type_config| type_config.icon.clone())
93 })
94 .unwrap_or_else(|| Arc::from("".to_string()))
95 }86 }
96}87}
crates/project_panel2/src/project_panel.rs+3−3
@@ -1283,16 +1283,16 @@ impl ProjectPanel { · 1282 unmodified lines
1283 let icon = match entry.kind {1283 let icon = match entry.kind {
1284 EntryKind::File(_) => {1284 EntryKind::File(_) => {
1285 if show_file_icons {1285 if show_file_icons {
1286 Some(FileAssociations::get_icon(&entry.path, cx))1286 FileAssociations::get_icon(&entry.path, cx)
1287 } else {1287 } else {
1288 None1288 None
1289 }1289 }
1290 }1290 }
1291 _ => {1291 _ => {
1292 if show_folder_icons {1292 if show_folder_icons {
1293 Some(FileAssociations::get_folder_icon(is_expanded, cx))1293 FileAssociations::get_folder_icon(is_expanded, cx)
1294 } else {1294 } else {
1295 Some(FileAssociations::get_chevron_icon(is_expanded, cx))1295 FileAssociations::get_chevron_icon(is_expanded, cx)
1296 }1296 }
1297 }1297 }
1298 };1298 };
crates/search2/src/search_bar.rs+3−3
@@ -1,4 +1,4 @@
1use gpui::{IntoElement, MouseDownEvent, WindowContext};1use gpui::{ClickEvent, IntoElement, WindowContext};
2use ui::{Button, ButtonVariant, IconButton};2use ui::{Button, ButtonVariant, IconButton};
3 3
4use crate::mode::SearchMode;4use crate::mode::SearchMode;
@@ -6,7 +6,7 @@ use crate::mode::SearchMode; · 1 unmodified line
6pub(super) fn render_nav_button(6pub(super) fn render_nav_button(
7 icon: ui::Icon,7 icon: ui::Icon,
8 _active: bool,8 _active: bool,
9 on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,9 on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
10) -> impl IntoElement {10) -> impl IntoElement {
11 // let tooltip_style = cx.theme().tooltip.clone();11 // let tooltip_style = cx.theme().tooltip.clone();
12 // let cursor_style = if active {12 // let cursor_style = if active {
@@ -21,7 +21,7 @@ pub(super) fn render_nav_button( · 8 unmodified lines
21pub(crate) fn render_search_mode_button(21pub(crate) fn render_search_mode_button(
22 mode: SearchMode,22 mode: SearchMode,
23 is_active: bool,23 is_active: bool,
24 on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,24 on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
25) -> Button {25) -> Button {
26 let button_variant = if is_active {26 let button_variant = if is_active {
27 ButtonVariant::Filled27 ButtonVariant::Filled
crates/storybook2/src/stories/focus.rs+1−1
@@ -2,7 +2,7 @@ use gpui::{ · 1 unmodified line
2 actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,2 actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
3 WindowContext,3 WindowContext,
4};4};
5use theme2::ActiveTheme;5use ui::prelude::*;
6 6
7actions!(ActionA, ActionB, ActionC);7actions!(ActionA, ActionB, ActionC);
8 8
crates/storybook2/src/stories/picker.rs+1−1
@@ -4,7 +4,7 @@ use gpui::{ · 3 unmodified lines
4};4};
5use picker::{Picker, PickerDelegate};5use picker::{Picker, PickerDelegate};
6use std::sync::Arc;6use std::sync::Arc;
7use theme2::ActiveTheme;7use ui::prelude::*;
8use ui::{Label, ListItem};8use ui::{Label, ListItem};
9 9
10pub struct PickerStory {10pub struct PickerStory {
crates/storybook2/src/stories/scroll.rs+1−1
@@ -1,5 +1,5 @@
1use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};1use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
2use theme2::ActiveTheme;2use ui::prelude::*;
3use ui::Tooltip;3use ui::Tooltip;
4 4
5pub struct ScrollStory;5pub struct ScrollStory;
crates/storybook2/src/story_selector.rs+0−2
@@ -19,7 +19,6 @@ pub enum ComponentStory { · 18 unmodified lines
19 Focus,19 Focus,
20 Icon,20 Icon,
21 IconButton,21 IconButton,
22 Input,
23 Keybinding,22 Keybinding,
24 Label,23 Label,
25 ListItem,24 ListItem,
@@ -39,7 +38,6 @@ impl ComponentStory { · 13 unmodified lines
39 Self::Focus => FocusStory::view(cx).into(),38 Self::Focus => FocusStory::view(cx).into(),
40 Self::Icon => cx.build_view(|_| ui::IconStory).into(),39 Self::Icon => cx.build_view(|_| ui::IconStory).into(),
41 Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),40 Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
42 Self::Input => cx.build_view(|_| ui::InputStory).into(),
43 Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),41 Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
44 Self::Label => cx.build_view(|_| ui::LabelStory).into(),42 Self::Label => cx.build_view(|_| ui::LabelStory).into(),
45 Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),43 Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
crates/theme2/src/registry.rs+4−0
@@ -86,6 +86,10 @@ impl ThemeRegistry { · 85 unmodified lines
86 }));86 }));
87 }87 }
88 88
89 pub fn clear(&mut self) {
90 self.themes.clear();
91 }
92
89 pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {93 pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
90 self.themes.keys().cloned()94 self.themes.keys().cloned()
91 }95 }
crates/theme_selector2/Cargo.toml+29−0
@@ -0,0 +1,29 @@
1[package]
2name = "theme_selector2"
3version = "0.1.0"
4edition = "2021"
5publish = false
6
7[lib]
8path = "src/theme_selector.rs"
9doctest = false
10
11[dependencies]
12editor = { package = "editor2", path = "../editor2" }
13fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
14fs = { package = "fs2", path = "../fs2" }
15gpui = { package = "gpui2", path = "../gpui2" }
16ui = { package = "ui2", path = "../ui2" }
17picker = { package = "picker2", path = "../picker2" }
18theme = { package = "theme2", path = "../theme2" }
19settings = { package = "settings2", path = "../settings2" }
20feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
21workspace = { package = "workspace2", path = "../workspace2" }
22util = { path = "../util" }
23log.workspace = true
24parking_lot.workspace = true
25postage.workspace = true
26smol.workspace = true
27
28[dev-dependencies]
29editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
crates/theme_selector2/src/theme_selector.rs+276−0
@@ -0,0 +1,276 @@
1use feature_flags::FeatureFlagAppExt;
2use fs::Fs;
3use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
4use gpui::{
5 actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render,
6 SharedString, View, ViewContext, VisualContext, WeakView,
7};
8use picker::{Picker, PickerDelegate};
9use settings::{update_settings_file, SettingsStore};
10use std::sync::Arc;
11use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
12use ui::ListItem;
13use util::ResultExt;
14use workspace::{ui::HighlightedLabel, Workspace};
15
16actions!(Toggle, Reload);
17
18pub 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
27pub 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)]
38pub 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
55pub struct ThemeSelector {
56 picker: View<Picker<ThemeSelectorDelegate>>,
57}
58
59impl EventEmitter<DismissEvent> for ThemeSelector {}
60
61impl FocusableView for ThemeSelector {
62 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
63 self.picker.focus_handle(cx)
64 }
65}
66
67impl 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
75impl 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
82pub 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
92impl 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
159impl 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 @@
1use gpui::{ClickEvent, WindowContext};
2
3pub 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 @@
1mod avatar;1mod avatar;
2mod button;2mod button;
3mod button2;
3mod checkbox;4mod checkbox;
4mod context_menu;5mod context_menu;
5mod disclosure;6mod disclosure;
6mod divider;7mod divider;
7mod icon;8mod icon;
8mod icon_button;9mod icon_button;
9mod input;
10mod keybinding;10mod keybinding;
11mod label;11mod label;
12mod list;12mod list;
@@ -21,13 +21,13 @@ mod stories; · 8 unmodified lines
21 21
22pub use avatar::*;22pub use avatar::*;
23pub use button::*;23pub use button::*;
24pub use button2::*;
24pub use checkbox::*;25pub use checkbox::*;
25pub use context_menu::*;26pub use context_menu::*;
26pub use disclosure::*;27pub use disclosure::*;
27pub use divider::*;28pub use divider::*;
28pub use icon::*;29pub use icon::*;
29pub use icon_button::*;30pub use icon_button::*;
30pub use input::*;
31pub use keybinding::*;31pub use keybinding::*;
32pub use label::*;32pub use label::*;
33pub use list::*;33pub use list::*;
crates/ui2/src/components/button.rs+5−10
@@ -1,9 +1,7 @@
1use std::rc::Rc;
2
3use gpui::{1use gpui::{
4 DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,2 ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
5 StatefulInteractiveElement, WindowContext,
6};3};
4use std::rc::Rc;
7 5
8use crate::prelude::*;6use crate::prelude::*;
9use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};7use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
@@ -67,7 +65,7 @@ impl ButtonVariant { · 57 unmodified lines
67#[derive(IntoElement)]65#[derive(IntoElement)]
68pub struct Button {66pub struct Button {
69 disabled: bool,67 disabled: bool,
70 click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,68 click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
71 icon: Option<Icon>,69 icon: Option<Icon>,
72 icon_position: Option<IconPosition>,70 icon_position: Option<IconPosition>,
73 label: SharedString,71 label: SharedString,
@@ -118,7 +116,7 @@ impl RenderOnce for Button { · 44 unmodified lines
118 }116 }
119 117
120 if let Some(click_handler) = self.click_handler.clone() {118 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| {
122 click_handler(event, cx);120 click_handler(event, cx);
123 });121 });
124 }122 }
@@ -168,10 +166,7 @@ impl Button { · 43 unmodified lines
168 self166 self
169 }167 }
170 168
171 pub fn on_click(169 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
172 mut self,
173 handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
174 ) -> Self {
175 self.click_handler = Some(Rc::new(handler));170 self.click_handler = Some(Rc::new(handler));
176 self171 self
177 }172 }
crates/ui2/src/components/button2.rs+413−0
@@ -0,0 +1,413 @@
1use gpui::{
2 rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
3 StatefulInteractiveElement, WindowContext,
4};
5use smallvec::SmallVec;
6
7use 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)]
22pub enum IconPosition2 {
23 #[default]
24 Before,
25 After,
26}
27
28#[derive(Default, PartialEq, Clone, Copy)]
29pub enum ButtonStyle2 {
30 #[default]
31 Filled,
32 // Tinted,
33 Subtle,
34 Transparent,
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct ButtonStyle {
39 pub background: Hsla,
40 pub border_color: Hsla,
41 pub label_color: Hsla,
42 pub icon_color: Hsla,
43}
44
45impl 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)]
167pub enum ButtonSize2 {
168 #[default]
169 Default,
170 Compact,
171 None,
172}
173
174impl 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
204pub 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)]
258pub 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
268impl 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
290impl 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
310impl 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
336impl 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
361impl 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 @@
1use gpui::{div, Element, ParentElement};1use std::rc::Rc;
2 2
3use crate::{Color, Icon, IconElement, IconSize, Toggle};3use gpui::{div, ClickEvent, Element, IntoElement, ParentElement, WindowContext};
4 4
5pub fn disclosure_control(toggle: Toggle) -> impl Element {5use crate::{Color, Icon, IconButton, IconSize, Toggle};
6
7pub fn disclosure_control(
8 toggle: Toggle,
9 on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
10) -> impl Element {
6 match (toggle.is_toggleable(), toggle.is_toggled()) {11 match (toggle.is_toggleable(), toggle.is_toggled()) {
7 (false, _) => div(),12 (false, _) => div(),
8 (_, true) => div().child(13 (_, true) => div().child(
9 IconElement::new(Icon::ChevronDown)14 IconButton::new("toggle", Icon::ChevronDown)
10 .color(Color::Muted)15 .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 }),
12 ),20 ),
13 (_, false) => div().child(21 (_, false) => div().child(
14 IconElement::new(Icon::ChevronRight)22 IconButton::new("toggle", Icon::ChevronRight)
15 .color(Color::Muted)23 .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 }),
17 ),28 ),
18 }29 }
19}30}
crates/ui2/src/components/icon_button.rs+31−24
@@ -1,25 +1,26 @@
1use crate::{h_stack, prelude::*, Icon, IconElement};1use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
2use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};2use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
3 3
4#[derive(IntoElement)]4#[derive(IntoElement)]
5pub struct IconButton {5pub struct IconButton {
6 id: ElementId,6 id: ElementId,
7 icon: Icon,7 icon: Icon,
8 color: Color,8 color: Color,
9 size: IconSize,
9 variant: ButtonVariant,10 variant: ButtonVariant,
10 state: InteractionState,11 disabled: bool,
11 selected: bool,12 selected: bool,
12 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,13 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>>,
14}15}
15 16
16impl RenderOnce for IconButton {17impl RenderOnce for IconButton {
17 type Rendered = Stateful<Div>;18 type Rendered = Stateful<Div>;
18 19
19 fn render(self, cx: &mut WindowContext) -> Self::Rendered {20 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
20 let icon_color = match (self.state, self.color) {21 let icon_color = match (self.disabled, self.selected, self.color) {
21 (InteractionState::Disabled, _) => Color::Disabled,22 (true, _, _) => Color::Disabled,
22 (InteractionState::Active, _) => Color::Selected,23 (false, true, _) => Color::Selected,
23 _ => self.color,24 _ => self.color,
24 };25 };
25 26
@@ -50,10 +51,14 @@ impl RenderOnce for IconButton { · 24 unmodified lines
50 // place we use an icon button.51 // place we use an icon button.
51 // .hover(|style| style.bg(bg_hover_color))52 // .hover(|style| style.bg(bg_hover_color))
52 .active(|style| style.bg(bg_active_color))53 .active(|style| style.bg(bg_active_color))
53 .child(IconElement::new(self.icon).color(icon_color));54 .child(
54 55 IconElement::new(self.icon)
55 if let Some(click_handler) = self.on_mouse_down {56 .size(self.size)
56 button = button.on_mouse_down(MouseButton::Left, move |event, cx| {57 .color(icon_color),
58 );
59
60 if let Some(click_handler) = self.on_click {
61 button = button.on_click(move |event, cx| {
57 cx.stop_propagation();62 cx.stop_propagation();
58 click_handler(event, cx);63 click_handler(event, cx);
59 })64 })
@@ -65,8 +70,7 @@ impl RenderOnce for IconButton { · 5 unmodified lines
65 }70 }
66 }71 }
67 72
68 // HACK: Add an additional identified element wrapper to fix tooltips not showing up.73 button
69 div().id(self.id.clone()).child(button)
70 }74 }
71}75}
72 76
@@ -76,11 +80,12 @@ impl IconButton { · 3 unmodified lines
76 id: id.into(),80 id: id.into(),
77 icon,81 icon,
78 color: Color::default(),82 color: Color::default(),
83 size: Default::default(),
79 variant: ButtonVariant::default(),84 variant: ButtonVariant::default(),
80 state: InteractionState::default(),
81 selected: false,85 selected: false,
86 disabled: false,
82 tooltip: None,87 tooltip: None,
83 on_mouse_down: None,88 on_click: None,
84 }89 }
85 }90 }
86 91
@@ -94,13 +99,13 @@ impl IconButton { · 7 unmodified lines
94 self99 self
95 }100 }
96 101
97 pub fn variant(mut self, variant: ButtonVariant) -> Self {102 pub fn size(mut self, size: IconSize) -> Self {
98 self.variant = variant;103 self.size = size;
99 self104 self
100 }105 }
101 106
102 pub fn state(mut self, state: InteractionState) -> Self {107 pub fn variant(mut self, variant: ButtonVariant) -> Self {
103 self.state = state;108 self.variant = variant;
104 self109 self
105 }110 }
106 111
@@ -109,16 +114,18 @@ impl IconButton { · 2 unmodified lines
109 self114 self
110 }115 }
111 116
117 pub fn disabled(mut self, disabled: bool) -> Self {
118 self.disabled = disabled;
119 self
120 }
121
112 pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {122 pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
113 self.tooltip = Some(Box::new(tooltip));123 self.tooltip = Some(Box::new(tooltip));
114 self124 self
115 }125 }
116 126
117 pub fn on_click(127 pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self {
118 mut self,128 self.on_click = Some(Box::new(handler));
119 handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
120 ) -> Self {
121 self.on_mouse_down = Some(Box::new(handler));
122 self129 self
123 }130 }
124 131
crates/ui2/src/components/input.rs+0−108
@@ -1,108 +0,0 @@
1use crate::{prelude::*, Label};
2use gpui::{prelude::*, Div, IntoElement, Stateful};
3
4#[derive(Default, PartialEq)]
5pub enum InputVariant {
6 #[default]
7 Ghost,
8 Filled,
9}
10
11#[derive(IntoElement)]
12pub struct Input {
13 placeholder: SharedString,
14 value: String,
15 state: InteractionState,
16 variant: InputVariant,
17 disabled: bool,
18 is_active: bool,
19}
20
21impl 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
72impl 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
25 left_icon: Option<Icon>,25 left_icon: Option<Icon>,
26 meta: Option<ListHeaderMeta>,26 meta: Option<ListHeaderMeta>,
27 toggle: Toggle,27 toggle: Toggle,
28 on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
28 inset: bool,29 inset: bool,
30 selected: bool,
29}31}
30 32
31impl ListHeader {33impl ListHeader {
@@ -36,6 +38,8 @@ impl ListHeader { · 4 unmodified lines
36 meta: None,38 meta: None,
37 inset: false,39 inset: false,
38 toggle: Toggle::NotToggleable,40 toggle: Toggle::NotToggleable,
41 on_toggle: None,
42 selected: false,
39 }43 }
40 }44 }
41 45
@@ -44,6 +48,14 @@ impl ListHeader { · 2 unmodified lines
44 self48 self
45 }49 }
46 50
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
47 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {59 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
48 self.left_icon = left_icon;60 self.left_icon = left_icon;
49 self61 self
@@ -57,13 +69,18 @@ impl ListHeader { · 7 unmodified lines
57 self.meta = meta;69 self.meta = meta;
58 self70 self
59 }71 }
72
73 pub fn selected(mut self, selected: bool) -> Self {
74 self.selected = selected;
75 self
76 }
60}77}
61 78
62impl RenderOnce for ListHeader {79impl RenderOnce for ListHeader {
63 type Rendered = Div;80 type Rendered = Div;
64 81
65 fn render(self, cx: &mut WindowContext) -> Self::Rendered {82 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);
67 84
68 let meta = match self.meta {85 let meta = match self.meta {
69 Some(ListHeaderMeta::Tools(icons)) => div().child(86 Some(ListHeaderMeta::Tools(icons)) => div().child(
@@ -85,6 +102,9 @@ impl RenderOnce for ListHeader { · 15 unmodified lines
85 div()102 div()
86 .h_5()103 .h_5()
87 .when(self.inset, |this| this.px_2())104 .when(self.inset, |this| this.px_2())
105 .when(self.selected, |this| {
106 this.bg(cx.theme().colors().ghost_element_selected)
107 })
88 .flex()108 .flex()
89 .flex_1()109 .flex_1()
90 .items_center()110 .items_center()
@@ -177,6 +197,7 @@ pub struct ListItem { · 86 unmodified lines
177 toggle: Toggle,197 toggle: Toggle,
178 inset: bool,198 inset: bool,
179 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,199 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
200 on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
180 on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,201 on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
181 children: SmallVec<[AnyElement; 2]>,202 children: SmallVec<[AnyElement; 2]>,
182}203}
@@ -193,6 +214,7 @@ impl ListItem { · 10 unmodified lines
193 inset: false,214 inset: false,
194 on_click: None,215 on_click: None,
195 on_secondary_mouse_down: None,216 on_secondary_mouse_down: None,
217 on_toggle: None,
196 children: SmallVec::new(),218 children: SmallVec::new(),
197 }219 }
198 }220 }
@@ -230,6 +252,14 @@ impl ListItem { · 31 unmodified lines
230 self252 self
231 }253 }
232 254
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
233 pub fn selected(mut self, selected: bool) -> Self {263 pub fn selected(mut self, selected: bool) -> Self {
234 self.selected = selected;264 self.selected = selected;
235 self265 self
@@ -255,19 +285,6 @@ impl RenderOnce for ListItem { · 19 unmodified lines
255 type Rendered = Stateful<Div>;285 type Rendered = Stateful<Div>;
256 286
257 fn render(self, cx: &mut WindowContext) -> Self::Rendered {287 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
271 div()288 div()
272 .id(self.id)289 .id(self.id)
273 .relative()290 .relative()
@@ -282,8 +299,8 @@ impl RenderOnce for ListItem { · 8 unmodified lines
282 .when(self.selected, |this| {299 .when(self.selected, |this| {
283 this.bg(cx.theme().colors().ghost_element_selected)300 this.bg(cx.theme().colors().ghost_element_selected)
284 })301 })
285 .when_some(self.on_click.clone(), |this, on_click| {302 .when_some(self.on_click, |this, on_click| {
286 this.on_click(move |event, cx| {303 this.cursor_pointer().on_click(move |event, cx| {
287 // HACK: GPUI currently fires `on_click` with any mouse button,304 // HACK: GPUI currently fires `on_click` with any mouse button,
288 // but we only care about the left button.305 // but we only care about the left button.
289 if event.down.button == MouseButton::Left {306 if event.down.button == MouseButton::Left {
@@ -304,23 +321,18 @@ impl RenderOnce for ListItem { · 14 unmodified lines
304 .gap_1()321 .gap_1()
305 .items_center()322 .items_center()
306 .relative()323 .relative()
307 .child(disclosure_control(self.toggle))324 .child(disclosure_control(self.toggle, self.on_toggle))
308 .children(left_content)325 .map(|this| match self.left_slot {
309 .children(self.children)326 Some(GraphicSlot::Icon(i)) => this.child(
310 // HACK: We need to attach the `on_click` handler to the child element in order to have the click327 IconElement::new(i)
311 // event actually fire.328 .size(IconSize::Small)
312 // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the329 .color(Color::Muted),
313 // outer `div`.330 ),
314 .id("on_click_hack")331 Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)),
315 .when_some(self.on_click, |this, on_click| {332 Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)),
316 this.on_click(move |event, cx| {333 None => this,
317 // HACK: GPUI currently fires `on_click` with any mouse button,334 })
318 // but we only care about the left button.335 .children(self.children),
319 if event.down.button == MouseButton::Left {
320 (on_click)(event, cx)
321 }
322 })
323 }),
324 )336 )
325 }337 }
326}338}
crates/ui2/src/components/stories.rs+0−3
@@ -4,18 +4,15 @@ mod checkbox; · 3 unmodified lines
4mod context_menu;4mod context_menu;
5mod icon;5mod icon;
6mod icon_button;6mod icon_button;
7mod input;
8mod keybinding;7mod keybinding;
9mod label;8mod label;
10mod list_item;9mod list_item;
11
12pub use avatar::*;10pub use avatar::*;
13pub use button::*;11pub use button::*;
14pub use checkbox::*;12pub use checkbox::*;
15pub use context_menu::*;13pub use context_menu::*;
16pub use icon::*;14pub use icon::*;
17pub use icon_button::*;15pub use icon_button::*;
18pub use input::*;
19pub use keybinding::*;16pub use keybinding::*;
20pub use label::*;17pub use label::*;
21pub use list_item::*;18pub use list_item::*;
crates/ui2/src/components/stories/button.rs+48−116
@@ -1,9 +1,8 @@
1use gpui::{rems, Div, Render};1use gpui::{Div, Render};
2use story::Story;2use story::Story;
3use strum::IntoEnumIterator;
4 3
5use crate::prelude::*;4use crate::prelude::*;
6use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};5use crate::{h_stack, Button, Icon, IconPosition};
7 6
8pub struct ButtonStory;7pub struct ButtonStory;
9 8
@@ -11,8 +10,6 @@ impl Render for ButtonStory { · 1 unmodified line
11 type Element = Div;10 type Element = Div;
12 11
13 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {12 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
14 let states = InteractionState::iter();
15
16 Story::container()13 Story::container()
17 .child(Story::title_for::<Button>())14 .child(Story::title_for::<Button>())
18 .child(15 .child(
@@ -20,121 +17,56 @@ impl Render for ButtonStory { · 1 unmodified line
20 .flex()17 .flex()
21 .gap_8()18 .gap_8()
22 .child(19 .child(
23 div()20 div().child(Story::label("Ghost (Default)")).child(
24 .child(Story::label("Ghost (Default)"))21 h_stack()
25 .child(h_stack().gap_2().children(states.clone().map(|state| {22 .gap_2()
26 v_stack()23 .child(Button::new("Label").variant(ButtonVariant::Ghost)),
27 .gap_1()24 ),
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 }))),
93 )25 )
26 .child(Story::label("Ghost – Left Icon"))
94 .child(27 .child(
95 div()28 h_stack().gap_2().child(
96 .child(Story::label("Fixed With"))29 Button::new("Label")
97 .child(h_stack().gap_2().children(states.clone().map(|state| {30 .variant(ButtonVariant::Ghost)
98 v_stack()31 .icon(Icon::Plus)
99 .gap_1()32 .icon_position(IconPosition::Left),
100 .child(Label::new(state.to_string()).color(Color::Muted))33 ),
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 }))),
136 ),34 ),
137 )35 )
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 )
138 .child(Story::label("Button with `on_click`"))70 .child(Story::label("Button with `on_click`"))
139 .child(71 .child(
140 Button::new("Label")72 Button::new("Label")
crates/ui2/src/components/stories/input.rs+0−18
@@ -1,18 +0,0 @@
1use gpui::{Div, Render};
2use story::Story;
3
4use crate::prelude::*;
5use crate::Input;
6
7pub struct InputStory;
8
9impl 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
2use story::Story;2use story::Story;
3 3
4use crate::prelude::*;4use crate::prelude::*;
5use crate::ListItem;5use crate::{Icon, ListItem};
6 6
7pub struct ListItemStory;7pub struct ListItemStory;
8 8
@@ -14,6 +14,20 @@ impl Render for ListItemStory { · 5 unmodified lines
14 .child(Story::title_for::<ListItem>())14 .child(Story::title_for::<ListItem>())
15 .child(Story::label("Default"))15 .child(Story::label("Default"))
16 .child(ListItem::new("hello_world").child("Hello, world!"))16 .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 )
17 .child(Story::label("With `on_click`"))31 .child(Story::label("With `on_click`"))
18 .child(32 .child(
19 ListItem::new("with_on_click")33 ListItem::new("with_on_click")
@@ -24,11 +38,11 @@ impl Render for ListItemStory { · 4 unmodified lines
24 )38 )
25 .child(Story::label("With `on_secondary_mouse_down`"))39 .child(Story::label("With `on_secondary_mouse_down`"))
26 .child(40 .child(
27 ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(41 ListItem::new("with_on_secondary_mouse_down")
28 |_event, _cx| {42 .child("Right click me")
43 .on_secondary_mouse_down(|_event, _cx| {
29 println!("Right mouse down!");44 println!("Right mouse down!");
30 },45 }),
31 ),
32 )46 )
33 }47 }
34}48}
crates/ui2/src/fixed.rs+6−0
@@ -0,0 +1,6 @@
1use gpui::DefiniteLength;
2
3pub 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
3 ViewContext, WindowContext,3 ViewContext, WindowContext,
4};4};
5 5
6pub use crate::clickable::*;
7pub use crate::fixed::*;
8pub use crate::selectable::*;
6pub use crate::StyledExt;9pub use crate::StyledExt;
7pub use crate::{ButtonVariant, Color};10pub use crate::{ButtonVariant, Color};
8pub use theme::ActiveTheme;11pub use theme::ActiveTheme;
9
10use strum::EnumIter;
11
12#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
13pub enum IconSide {
14 #[default]
15 Left,
16 Right,
17}
18
19#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
20pub 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
39impl 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)]
50pub enum Selection {
51 #[default]
52 Unselected,
53 Indeterminate,
54 Selected,
55}
56
57impl 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 @@
1use gpui::{AnyView, WindowContext};
2
3pub 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)]
12pub enum Selection {
13 #[default]
14 Unselected,
15 Indeterminate,
16 Selected,
17}
18
19impl 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 @@
1use gpui::{Hsla, WindowContext};1use gpui::{Hsla, WindowContext};
2use theme::ActiveTheme;2use theme::ActiveTheme;
3 3
4#[derive(Default, PartialEq, Copy, Clone)]4#[derive(Debug, Default, PartialEq, Copy, Clone)]
5pub enum Color {5pub enum Color {
6 #[default]6 #[default]
7 Default,7 Default,
crates/ui2/src/ui2.rs+6−0
@@ -12,13 +12,19 @@ · 11 unmodified lines
12#![doc = include_str!("../docs/building-ui.md")]12#![doc = include_str!("../docs/building-ui.md")]
13#![doc = include_str!("../docs/todo.md")]13#![doc = include_str!("../docs/todo.md")]
14 14
15mod clickable;
15mod components;16mod components;
17mod fixed;
16pub mod prelude;18pub mod prelude;
19mod selectable;
17mod styled_ext;20mod styled_ext;
18mod styles;21mod styles;
19pub mod utils;22pub mod utils;
20 23
24pub use clickable::*;
21pub use components::*;25pub use components::*;
26pub use fixed::*;
22pub use prelude::*;27pub use prelude::*;
28pub use selectable::*;
23pub use styled_ext::*;29pub use styled_ext::*;
24pub use styles::*;30pub use styles::*;
crates/welcome2/Cargo.toml+37−0
@@ -0,0 +1,37 @@
1[package]
2name = "welcome2"
3version = "0.1.0"
4edition = "2021"
5publish = false
6
7[lib]
8path = "src/welcome.rs"
9
10[features]
11test-support = []
12
13[dependencies]
14client = { package = "client2", path = "../client2" }
15editor = { package = "editor2", path = "../editor2" }
16fs = { package = "fs2", path = "../fs2" }
17fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
18gpui = { package = "gpui2", path = "../gpui2" }
19ui = { package = "ui2", path = "../ui2" }
20db = { package = "db2", path = "../db2" }
21install_cli = { package = "install_cli2", path = "../install_cli2" }
22project = { package = "project2", path = "../project2" }
23settings = { package = "settings2", path = "../settings2" }
24theme = { package = "theme2", path = "../theme2" }
25theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
26util = { path = "../util" }
27picker = { package = "picker2", path = "../picker2" }
28workspace = { package = "workspace2", path = "../workspace2" }
29# vim = { package = "vim2", path = "../vim2" }
30
31anyhow.workspace = true
32log.workspace = true
33schemars.workspace = true
34serde.workspace = true
35
36[dev-dependencies]
37editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
crates/welcome2/src/base_keymap_picker.rs+208−0
@@ -0,0 +1,208 @@
1use super::base_keymap_setting::BaseKeymap;
2use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
3use gpui::{
4 actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
5 View, ViewContext, VisualContext, WeakView,
6};
7use picker::{Picker, PickerDelegate};
8use project::Fs;
9use settings::{update_settings_file, Settings};
10use std::sync::Arc;
11use ui::ListItem;
12use util::ResultExt;
13use workspace::{ui::HighlightedLabel, Workspace};
14
15actions!(ToggleBaseKeymapSelector);
16
17pub fn init(cx: &mut AppContext) {
18 cx.observe_new_views(|workspace: &mut Workspace, _cx| {
19 workspace.register_action(toggle);
20 })
21 .detach();
22}
23
24pub 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
38pub struct BaseKeymapSelector {
39 focus_handle: gpui::FocusHandle,
40 picker: View<Picker<BaseKeymapSelectorDelegate>>,
41}
42
43impl FocusableView for BaseKeymapSelector {
44 fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
45 self.focus_handle.clone()
46 }
47}
48
49impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
50
51impl 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
65impl 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
73pub struct BaseKeymapSelectorDelegate {
74 view: WeakView<BaseKeymapSelector>,
75 matches: Vec<StringMatch>,
76 selected_index: usize,
77 fs: Arc<dyn Fs>,
78}
79
80impl 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
100impl 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 @@
1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use settings::Settings;
4
5#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
6pub enum BaseKeymap {
7 #[default]
8 VSCode,
9 JetBrains,
10 SublimeText,
11 Atom,
12 TextMate,
13}
14
15impl 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
47impl 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 @@
1mod base_keymap_picker;
2mod base_keymap_setting;
3
4use db::kvp::KEY_VALUE_STORE;
5use 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};
10use settings::{Settings, SettingsStore};
11use std::sync::Arc;
12use workspace::{
13 dock::DockPosition,
14 item::{Item, ItemEvent},
15 open_new, AppState, Welcome, Workspace, WorkspaceId,
16};
17
18pub use base_keymap_setting::BaseKeymap;
19
20pub const FIRST_OPEN: &str = "first_open";
21
22pub 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
36pub 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
51pub struct WelcomePage {
52 workspace: WeakView<Workspace>,
53 focus_handle: FocusHandle,
54 _settings_subscription: Subscription,
55}
56
57impl 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
243impl 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
253impl EventEmitter<ItemEvent> for WelcomePage {}
254
255impl FocusableView for WelcomePage {
256 fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
257 self.focus_handle.clone()
258 }
259}
260
261impl 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
7use schemars::JsonSchema;7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};8use serde::{Deserialize, Serialize};
9use std::sync::Arc;9use std::sync::Arc;
10use theme2::ActiveTheme;10use ui::prelude::*;
11use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};11use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
12 12
13pub enum PanelEvent {13pub enum PanelEvent {
14 ChangePosition,14 ChangePosition,
@@ -686,22 +686,26 @@ impl Render for PanelButtons { · 671 unmodified lines
686 let name = entry.panel.persistent_name();686 let name = entry.panel.persistent_name();
687 let panel = entry.panel.clone();687 let panel = entry.panel.clone();
688 688
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 {
690 let action = dock.toggle_action();692 let action = dock.toggle_action();
693
691 let tooltip: SharedString =694 let tooltip: SharedString =
692 format!("Close {} dock", dock.position.to_label()).into();695 format!("Close {} dock", dock.position.to_label()).into();
693 IconButton::new(name, icon)696
694 .state(InteractionState::Active)697 (action, tooltip)
695 .action(action.boxed_clone())
696 .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
697 } else {698 } else {
698 let action = entry.panel.toggle_action(cx);699 let action = entry.panel.toggle_action(cx);
699 700
700 IconButton::new(name, icon)701 (action, name.into())
701 .action(action.boxed_clone())
702 .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
703 };702 };
704 703
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
705 Some(709 Some(
706 menu_handle(name)710 menu_handle(name)
707 .menu(move |cx| {711 .menu(move |cx| {
crates/workspace2/src/pane.rs+4−8
@@ -1482,18 +1482,14 @@ impl Pane { · 1481 unmodified lines
1482 .gap_px()1482 .gap_px()
1483 .child(1483 .child(
1484 div().border().border_color(gpui::red()).child(1484 div().border().border_color(gpui::red()).child(
1485 IconButton::new("navigate_backward", Icon::ArrowLeft).state(1485 IconButton::new("navigate_backward", Icon::ArrowLeft)
1486 InteractionState::Enabled1486 .disabled(!self.can_navigate_backward()),
1487 .if_enabled(self.can_navigate_backward()),
1488 ),
1489 ),1487 ),
1490 )1488 )
1491 .child(1489 .child(
1492 div().border().border_color(gpui::red()).child(1490 div().border().border_color(gpui::red()).child(
1493 IconButton::new("navigate_forward", Icon::ArrowRight).state(1491 IconButton::new("navigate_forward", Icon::ArrowRight)
1494 InteractionState::Enabled1492 .disabled(!self.can_navigate_forward()),
1495 .if_enabled(self.can_navigate_forward()),
1496 ),
1497 ),1493 ),
1498 ),1494 ),
1499 ),1495 ),
crates/workspace2/src/status_bar.rs+1−1
@@ -5,7 +5,7 @@ use gpui::{ · 4 unmodified lines
5 div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,5 div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
6 WindowContext,6 WindowContext,
7};7};
8use theme2::ActiveTheme;8use ui::prelude::*;
9use ui::{h_stack, Button, Icon, IconButton};9use ui::{h_stack, Button, Icon, IconButton};
10use util::ResultExt;10use util::ResultExt;
11 11
crates/workspace2/src/toolbar.rs+1−1
@@ -3,7 +3,7 @@ use gpui::{ · 2 unmodified lines
3 div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,3 div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
4 ViewContext, WindowContext,4 ViewContext, WindowContext,
5};5};
6use theme2::ActiveTheme;6use ui::prelude::*;
7use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};7use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
8 8
9pub enum ToolbarItemEvent {9pub enum ToolbarItemEvent {
crates/workspace2/src/workspace2.rs+16−16
@@ -1808,22 +1808,22 @@ impl Workspace { · 1807 unmodified lines
1808 pane1808 pane
1809 }1809 }
1810 1810
1811 // pub fn add_item_to_center(1811 pub fn add_item_to_center(
1812 // &mut self,1812 &mut self,
1813 // item: Box<dyn ItemHandle>,1813 item: Box<dyn ItemHandle>,
1814 // cx: &mut ViewContext<Self>,1814 cx: &mut ViewContext<Self>,
1815 // ) -> bool {1815 ) -> bool {
1816 // if let Some(center_pane) = self.last_active_center_pane.clone() {1816 if let Some(center_pane) = self.last_active_center_pane.clone() {
1817 // if let Some(center_pane) = center_pane.upgrade(cx) {1817 if let Some(center_pane) = center_pane.upgrade() {
1818 // center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));1818 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
1819 // true1819 true
1820 // } else {1820 } else {
1821 // false1821 false
1822 // }1822 }
1823 // } else {1823 } else {
1824 // false1824 false
1825 // }1825 }
1826 // }1826 }
1827 1827
1828 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {1828 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1829 self.active_pane1829 self.active_pane
crates/zed/Cargo.toml+1−1
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"] · 2 unmodified lines
3description = "The fast, collaborative code editor."3description = "The fast, collaborative code editor."
4edition = "2021"4edition = "2021"
5name = "zed"5name = "zed"
6version = "0.115.0"6version = "0.116.0"
7publish = false7publish = false
8 8
9[lib]9[lib]
crates/zed2/Cargo.toml+2−2
@@ -66,12 +66,12 @@ shellexpand = "2.1.0" · 65 unmodified lines
66text = { package = "text2", path = "../text2" }66text = { package = "text2", path = "../text2" }
67terminal_view = { package = "terminal_view2", path = "../terminal_view2" }67terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
68theme = { package = "theme2", path = "../theme2" }68theme = { package = "theme2", path = "../theme2" }
69# theme_selector = { path = "../theme_selector" }69theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
70util = { path = "../util" }70util = { path = "../util" }
71# semantic_index = { path = "../semantic_index" }71# semantic_index = { path = "../semantic_index" }
72# vim = { path = "../vim" }72# vim = { path = "../vim" }
73workspace = { package = "workspace2", path = "../workspace2" }73workspace = { package = "workspace2", path = "../workspace2" }
74# welcome = { path = "../welcome" }74welcome = { package = "welcome2", path = "../welcome2" }
75zed_actions = {package = "zed_actions2", path = "../zed_actions2"}75zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
76anyhow.workspace = true76anyhow.workspace = true
77async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }77async-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
13use editor::Editor;13use editor::Editor;
14use fs::RealFs;14use fs::RealFs;
15use futures::StreamExt;15use futures::StreamExt;
16use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};16use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
17use isahc::{prelude::Configurable, Request};17use isahc::{prelude::Configurable, Request};
18use language::LanguageRegistry;18use language::LanguageRegistry;
19use log::LevelFilter;19use log::LevelFilter;
@@ -36,7 +36,7 @@ use std::{ · 16 unmodified lines
36 path::{Path, PathBuf},36 path::{Path, PathBuf},
37 sync::{37 sync::{
38 atomic::{AtomicU32, Ordering},38 atomic::{AtomicU32, Ordering},
39 Arc,39 Arc, Weak,
40 },40 },
41 thread,41 thread,
42};42};
@@ -48,6 +48,7 @@ use util::{ · 5 unmodified lines
48 paths, ResultExt,48 paths, ResultExt,
49};49};
50use uuid::Uuid;50use uuid::Uuid;
51use welcome::{show_welcome_experience, FIRST_OPEN};
51use workspace::{AppState, WorkspaceStore};52use workspace::{AppState, WorkspaceStore};
52use zed2::{53use zed2::{
53 build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,54 build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
@@ -103,16 +104,15 @@ fn main() { · 49 unmodified lines
103 let listener = Arc::new(listener);104 let listener = Arc::new(listener);
104 let open_listener = listener.clone();105 let open_listener = listener.clone();
105 app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));106 app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
106 app.on_reopen(move |_cx| {107 app.on_reopen(move |cx| {
107 // todo!("workspace")108 if cx.has_global::<Weak<AppState>>() {
108 // if cx.has_global::<Weak<AppState>>() {109 if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
109 // if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {110 workspace::open_new(&app_state, cx, |workspace, cx| {
110 // workspace::open_new(&app_state, cx, |workspace, cx| {111 Editor::new_file(workspace, &Default::default(), cx)
111 // Editor::new_file(workspace, &Default::default(), cx)112 })
112 // })113 .detach();
113 // .detach();114 }
114 // }115 }
115 // }
116 });116 });
117 117
118 app.run(move |cx| {118 app.run(move |cx| {
@@ -164,17 +164,16 @@ fn main() { · 45 unmodified lines
164 // assistant::init(cx);164 // assistant::init(cx);
165 // component_test::init(cx);165 // component_test::init(cx);
166 166
167 // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
168 // cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))167 // cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
169 // .detach();168 // .detach();
170 // watch_file_types(fs.clone(), cx);169 watch_file_types(fs.clone(), cx);
171 170
172 languages.set_theme(cx.theme().clone());171 languages.set_theme(cx.theme().clone());
173 // cx.observe_global::<SettingsStore, _>({172 cx.observe_global::<SettingsStore>({
174 // let languages = languages.clone();173 let languages = languages.clone();
175 // move |cx| languages.set_theme(theme::current(cx).clone())174 move |cx| languages.set_theme(cx.theme().clone())
176 // })175 })
177 // .detach();176 .detach();
178 177
179 client.telemetry().start(installation_id, session_id, cx);178 client.telemetry().start(installation_id, session_id, cx);
180 let telemetry_settings = *client::TelemetrySettings::get_global(cx);179 let telemetry_settings = *client::TelemetrySettings::get_global(cx);
@@ -193,7 +192,6 @@ fn main() { · 12 unmodified lines
193 fs,192 fs,
194 build_window_options,193 build_window_options,
195 call_factory: call::Call::new,194 call_factory: call::Call::new,
196 // background_actions: todo!("ask Mikayla"),
197 workspace_store,195 workspace_store,
198 node_runtime,196 node_runtime,
199 });197 });
@@ -219,14 +217,13 @@ fn main() { · 19 unmodified lines
219 217
220 // journal2::init(app_state.clone(), cx);218 // journal2::init(app_state.clone(), cx);
221 // language_selector::init(cx);219 // language_selector::init(cx);
222 // theme_selector::init(cx);220 theme_selector::init(cx);
223 // activity_indicator::init(cx);221 // activity_indicator::init(cx);
224 // language_tools::init(cx);222 // language_tools::init(cx);
225 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);223 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
226 collab_ui::init(&app_state, cx);224 collab_ui::init(&app_state, cx);
227 // feedback::init(cx);225 // feedback::init(cx);
228 // welcome::init(cx);226 welcome::init(cx);
229 // zed::init(&app_state, cx);
230 227
231 // cx.set_menus(menus::menus());228 // cx.set_menus(menus::menus());
232 initialize_workspace(app_state.clone(), cx);229 initialize_workspace(app_state.clone(), cx);
@@ -279,17 +276,18 @@ fn main() { · 46 unmodified lines
279 .detach();276 .detach();
280 }277 }
281 Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {278 Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
282 todo!()279 triggered_authentication = true;
283 // triggered_authentication = true;280 let app_state = app_state.clone();
284 // let app_state = app_state.clone();281 let client = client.clone();
285 // let client = client.clone();282 cx.spawn(|mut cx| async move {
286 // cx.spawn(|mut cx| async move {283 // ignore errors here, we'll show a generic "not signed in"
287 // // ignore errors here, we'll show a generic "not signed in"284 let _ = authenticate(client, &cx).await;
288 // let _ = authenticate(client, &cx).await;285 //todo!()
289 // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))286 // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
290 // .await287 // .await
291 // })288 anyhow::Ok(())
292 // .detach_and_log_err(cx)289 })
290 .detach_and_log_err(cx)
293 }291 }
294 Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {292 Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
295 todo!()293 todo!()
@@ -340,7 +338,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> { · 44 unmodified lines
340 if client::IMPERSONATE_LOGIN.is_some() {338 if client::IMPERSONATE_LOGIN.is_some() {
341 client.authenticate_and_connect(false, &cx).await?;339 client.authenticate_and_connect(false, &cx).await?;
342 }340 }
343 } else if client.has_keychain_credentials(&cx).await {341 } else if client.has_keychain_credentials(&cx) {
344 client.authenticate_and_connect(true, &cx).await?;342 client.authenticate_and_connect(true, &cx).await?;
345 }343 }
346 Ok::<_, anyhow::Error>(())344 Ok::<_, anyhow::Error>(())
@@ -368,10 +366,9 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp · 21 unmodified lines
368 cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?366 cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
369 .await367 .await
370 .log_err();368 .log_err();
371 // todo!(welcome)369 } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
372 //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {370 cx.update(|cx| show_welcome_experience(app_state, cx))
373 //todo!()371 .log_err();
374 // cx.update(|cx| show_welcome_experience(app_state, cx));
375 } else {372 } else {
376 cx.update(|cx| {373 cx.update(|cx| {
377 workspace::open_new(app_state, cx, |workspace, cx| {374 workspace::open_new(app_state, cx, |workspace, cx| {
@@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) { · 331 unmodified lines
709 .unwrap();706 .unwrap();
710}707}
711 708
712// #[cfg(debug_assertions)]709#[cfg(debug_assertions)]
713// async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {710async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
714// let mut events = fs711 use std::time::Duration;
715// .watch("styles/src".as_ref(), Duration::from_millis(100))712
716// .await;713 let mut events = fs
717// while (events.next().await).is_some() {714 .watch(
718// let output = Command::new("npm")715 "crates/zed2/src/languages".as_ref(),
719// .current_dir("styles")716 Duration::from_millis(100),
720// .args(["run", "build"])717 )
721// .output()718 .await;
722// .await719 while (events.next().await).is_some() {
723// .log_err()?;720 languages.reload();
724// if output.status.success() {721 }
725// cx.update(|cx| theme_selector::reload(cx))722 Some(())
726// } else {723}
727// eprintln!(724
728// "build script failed {}",725#[cfg(debug_assertions)]
729// String::from_utf8_lossy(&output.stderr)726fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
730// );727 use std::time::Duration;
731// }728
732// }729 cx.spawn(|mut cx| async move {
733// Some(())730 let mut events = fs
734// }731 .watch(
735 732 "assets/icons/file_icons/file_types.json".as_ref(),
736// #[cfg(debug_assertions)]733 Duration::from_millis(100),
737// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {734 )
738// let mut events = fs735 .await;
739// .watch(736 while (events.next().await).is_some() {
740// "crates/zed/src/languages".as_ref(),737 cx.update(|cx| {
741// Duration::from_millis(100),738 cx.update_global(|file_types, _| {
742// )739 *file_types = project_panel::file_associations::FileAssociations::new(Assets);
743// .await;740 });
744// while (events.next().await).is_some() {741 })
745// languages.reload();742 .ok();
746// }743 }
747// Some(())744 })
748// }745 .detach()
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
783pub 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 &[]
792}746}
747
748#[cfg(not(debug_assertions))]
749async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
750 None
751}
752
753#[cfg(not(debug_assertions))]
754fn 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
11cargo depgraph \11cargo depgraph \
12 --workspace-only \12 --workspace-only \
13 --offline \13 --offline \
14 --root=zed,cli,collab \14 --root=zed2,cli,collab2 \
15 --dedup-transitive-deps \15 --dedup-transitive-deps \
16 | dot -Tsvg > $graph_file16 | dot -Tsvg > $graph_file
17 17

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