Unified · Split
| @@ -9471,6 +9471,27 @@ dependencies = [ · 9470 unmodified lines | |||
| 9471 | "workspace", | 9471 | "workspace", |
| 9472 | ] | 9472 | ] |
| 9473 | 9473 | ||
| 9474 | [[package]] | ||
| 9475 | name = "theme_selector2" | ||
| 9476 | version = "0.1.0" | ||
| 9477 | dependencies = [ | ||
| 9478 | "editor2", | ||
| 9479 | "feature_flags2", | ||
| 9480 | "fs2", | ||
| 9481 | "fuzzy2", | ||
| 9482 | "gpui2", | ||
| 9483 | "log", | ||
| 9484 | "parking_lot 0.11.2", | ||
| 9485 | "picker2", | ||
| 9486 | "postage", | ||
| 9487 | "settings2", | ||
| 9488 | "smol", | ||
| 9489 | "theme2", | ||
| 9490 | "ui2", | ||
| 9491 | "util", | ||
| 9492 | "workspace2", | ||
| 9493 | ] | ||
| 9494 | |||
| 9474 | [[package]] | 9495 | [[package]] |
| 9475 | name = "thiserror" | 9496 | name = "thiserror" |
| 9476 | version = "1.0.48" | 9497 | version = "1.0.48" |
| @@ -11054,6 +11075,31 @@ dependencies = [ · 1577 unmodified lines | |||
| 11054 | "workspace", | 11075 | "workspace", |
| 11055 | ] | 11076 | ] |
| 11056 | 11077 | ||
| 11078 | [[package]] | ||
| 11079 | name = "welcome2" | ||
| 11080 | version = "0.1.0" | ||
| 11081 | dependencies = [ | ||
| 11082 | "anyhow", | ||
| 11083 | "client2", | ||
| 11084 | "db2", | ||
| 11085 | "editor2", | ||
| 11086 | "fs2", | ||
| 11087 | "fuzzy2", | ||
| 11088 | "gpui2", | ||
| 11089 | "install_cli2", | ||
| 11090 | "log", | ||
| 11091 | "picker2", | ||
| 11092 | "project2", | ||
| 11093 | "schemars", | ||
| 11094 | "serde", | ||
| 11095 | "settings2", | ||
| 11096 | "theme2", | ||
| 11097 | "theme_selector2", | ||
| 11098 | "ui2", | ||
| 11099 | "util", | ||
| 11100 | "workspace2", | ||
| 11101 | ] | ||
| 11102 | |||
| 11057 | [[package]] | 11103 | [[package]] |
| 11058 | name = "which" | 11104 | name = "which" |
| 11059 | version = "4.4.2" | 11105 | version = "4.4.2" |
| @@ -11508,7 +11554,7 @@ dependencies = [ · 448 unmodified lines | |||
| 11508 | 11554 | ||
| 11509 | [[package]] | 11555 | [[package]] |
| 11510 | name = "zed" | 11556 | name = "zed" |
| 11511 | version = "0.115.0" | 11557 | version = "0.116.0" |
| 11512 | dependencies = [ | 11558 | dependencies = [ |
| 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 | ] |
| @@ -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", |
| @@ -14,8 +14,8 @@ use client::{ · 13 unmodified lines | |||
| 14 | use collections::HashSet; | 14 | use collections::HashSet; |
| 15 | use futures::{channel::oneshot, future::Shared, Future, FutureExt}; | 15 | use futures::{channel::oneshot, future::Shared, Future, FutureExt}; |
| 16 | use gpui::{ | 16 | use 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 | }; |
| 20 | pub use participant::ParticipantLocation; | 20 | pub use participant::ParticipantLocation; |
| 21 | use postage::watch; | 21 | use 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 | } |
| @@ -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 | ||
| 1327 | async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> { | 1327 | fn 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 | } |
| @@ -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_b | 366 | 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 | .await | 369 | .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_a | 397 | 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 | .await | 401 | .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_b | 447 | 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 | .await | 451 | .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_a | 566 | 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 | .await | 570 | .await |
| 564 | .unwrap(); | 571 | .unwrap(); |
| 565 | 572 | ||
| 566 | active_call_b | 573 | 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 | .await | 577 | .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_a | 619 | 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 | .await | 623 | .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_a | 639 | 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 | .await | 643 | .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_a | 806 | 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 | .await | 808 | .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 join | 1298 | // Non-members should not be allowed to join |
| 1288 | assert!(active_call_b | 1299 | 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 | .await | 1301 | .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 guest | 1320 | // Client B joins channel A as a guest |
| 1310 | active_call_b | 1321 | 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 | .await | 1323 | .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_b | 1354 | 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 | .await | 1356 | .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 join | 1384 | // should not be allowed to join |
| 1374 | assert!(active_call_b | 1385 | 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 | .await | 1387 | .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_b | 1403 | 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 | .await | 1405 | .await |
| 1395 | .unwrap(); | 1406 | .unwrap(); |
| 1396 | 1407 | ||
| @@ -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 2 | 511 | // Simultaneously join channel 1 and then channel 2 |
| 512 | active_call_a | 512 | 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) |
| @@ -17,6 +17,7 @@ mod contact_finder; · 16 unmodified lines | |||
| 17 | // Client, Contact, User, UserStore, | 17 | // Client, Contact, User, UserStore, |
| 18 | // }; | 18 | // }; |
| 19 | use contact_finder::ContactFinder; | 19 | use contact_finder::ContactFinder; |
| 20 | use menu::Confirm; | ||
| 20 | use rpc::proto; | 21 | use 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 { | 95 | pub 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 | |||
| 160 | use std::{iter::once, mem, sync::Arc}; | 161 | use std::{iter::once, mem, sync::Arc}; |
| 161 | 162 | ||
| 162 | use call::ActiveCall; | 163 | use call::ActiveCall; |
| 163 | use channel::{Channel, ChannelId, ChannelStore}; | 164 | use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; |
| 164 | use client::{Client, Contact, User, UserStore}; | 165 | use client::{Client, Contact, User, UserStore}; |
| 165 | use db::kvp::KEY_VALUE_STORE; | 166 | use db::kvp::KEY_VALUE_STORE; |
| 166 | use editor::Editor; | 167 | use editor::Editor; |
| 167 | use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; | 168 | use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; |
| 168 | use fuzzy::{match_strings, StringMatchCandidate}; | 169 | use fuzzy::{match_strings, StringMatchCandidate}; |
| 169 | use gpui::{ | 170 | use 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 | }; |
| 175 | use project::Fs; | 176 | use project::Fs; |
| 176 | use serde_derive::{Deserialize, Serialize}; | 177 | use serde_derive::{Deserialize, Serialize}; |
| 177 | use settings::Settings; | 178 | use settings::{Settings, SettingsStore}; |
| 178 | use ui::{ | 179 | use 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 | }; |
| 182 | use util::{maybe, ResultExt}; | 183 | use util::{maybe, ResultExt, TryFutureExt}; |
| 183 | use workspace::{ | 184 | use 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 | ||
| 295 | impl ChannelEditingState { | 296 | impl 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 { | 332 | enum 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)] |
| 338 | struct SerializedCollabPanel { | 339 | struct 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.subscriptions | 619 | 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.subscriptions | 631 | 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.subscriptions | 635 | 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.subscriptions | 639 | 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.subscriptions | 641 | 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_id | 654 | channel.id == *channel_id |
| 662 | // } else { | 655 | } else { |
| 663 | // false | 656 | false |
| 664 | // } | 657 | } |
| 665 | // }); | 658 | }); |
| 666 | // } | 659 | } |
| 667 | // } | 660 | } |
| 668 | // }, | 661 | }, |
| 669 | // )); | 662 | )); |
| 670 | 663 | ||
| 671 | this | 664 | 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_panel | 693 | .collapsed_channels |
| 701 | // .collapsed_channels | 694 | .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_STORE | 707 | 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 | // true | 1456 | true |
| 1465 | // } else { | 1457 | } else { |
| 1466 | // false | 1458 | 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::ContactRequests | 1897 | // Section::ContactRequests |
| 2070 | // | Section::Online | 1898 | // | Section::Online |
| 2071 | // | Section::Offline | 1899 | // | 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_store | 1987 | 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_store | 2004 | 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 | // true | 2013 | true |
| 2186 | // } else { | 2014 | } else { |
| 2187 | // false | 2015 | 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_channels | 2120 | // 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 it | 2268 | // // Should move to the filter editor if clicking on it |
| 2440 | // // Should move selection to the channel editor if activating it | 2269 | // // 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_login | 2275 | 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_store | 2281 | 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 | // .await | 2283 | .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_store | 2297 | 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 | } => self | 2414 | } => 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 selected | 2537 | 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 toggle | 2539 | ) |
| 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 | // *theme | 2725 | .justify_between() |
| 2972 | // .contact_row | 2726 | .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 = self | 2763 | let is_public = self |
| 3027 | .channel_store | 2764 | .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: participants | 2785 | 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 | None | 2793 | 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::Default | 2829 | 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::Default | 2848 | .tooltip(|cx| { |
| 3121 | // } else { | 2849 | Tooltip::text("Open channel notes", cx) |
| 3122 | // Color::Muted | 2850 | }), |
| 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::NotToggleable | 2858 | 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 menu | 2873 | 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)) |
| @@ -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() |
| @@ -37,7 +37,10 @@ use gpui::{ · 36 unmodified lines | |||
| 37 | }; | 37 | }; |
| 38 | use project::Project; | 38 | use project::Project; |
| 39 | use theme::ActiveTheme; | 39 | use theme::ActiveTheme; |
| 40 | use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; | 40 | use ui::{ |
| 41 | h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton, | ||
| 42 | IconElement, IconSize, KeyBinding, Tooltip, | ||
| 43 | }; | ||
| 41 | use util::ResultExt; | 44 | use util::ResultExt; |
| 42 | use workspace::{notifications::NotifyResultExt, Workspace}; | 45 | use 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 | } |
| @@ -1,3 +1,8 @@ | |||
| 1 | use std::{ | ||
| 2 | cmp::{self, Reverse}, | ||
| 3 | sync::Arc, | ||
| 4 | }; | ||
| 5 | |||
| 1 | use collections::{CommandPaletteFilter, HashMap}; | 6 | use collections::{CommandPaletteFilter, HashMap}; |
| 2 | use fuzzy::{StringMatch, StringMatchCandidate}; | 7 | use fuzzy::{StringMatch, StringMatchCandidate}; |
| 3 | use gpui::{ | 8 | use 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 | }; |
| 7 | use picker::{Picker, PickerDelegate}; | 12 | use picker::{Picker, PickerDelegate}; |
| 8 | use std::{ | 13 | |
| 9 | cmp::{self, Reverse}, | ||
| 10 | sync::Arc, | ||
| 11 | }; | ||
| 12 | use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; | 14 | use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; |
| 13 | use util::{ | 15 | use util::{ |
| 14 | channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, | 16 | channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, |
| @@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> { · 29 unmodified lines | |||
| 30 | 30 | ||
| 31 | impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V> | 31 | impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V> |
| 32 | where | 32 | where |
| 33 | V: 'static + Send + Sync, | 33 | V: 'static, |
| 34 | { | 34 | { |
| 35 | fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription | 35 | fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription |
| 36 | where | 36 | 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>(); |
| @@ -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 | ||
| @@ -111,7 +111,7 @@ pub struct Component<C> { · 110 unmodified lines | |||
| 111 | 111 | ||
| 112 | pub struct CompositeElementState<C: RenderOnce> { | 112 | pub 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 | ||
| 117 | impl<C> Component<C> { | 117 | impl<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 | state | 154 | let element = state.rendered_element.take().unwrap(); |
| 144 | .rendered_element | 155 | 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 | ||
| @@ -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_state | 179 | 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.origin | 227 | let item_origin = padded_bounds.origin |
| @@ -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() |
| @@ -1,7 +1,8 @@ | |||
| 1 | use editor::Editor; | 1 | use editor::Editor; |
| 2 | use gpui::{ | 2 | use 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 | }; |
| 6 | use std::{cmp, sync::Arc}; | 7 | use std::{cmp, sync::Arc}; |
| 7 | use ui::{prelude::*, v_stack, Color, Divider, Label}; | 8 | use ui::{prelude::*, v_stack, Color, Divider, Label}; |
| @@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> { · 8 unmodified lines | |||
| 16 | 17 | ||
| 17 | pub trait PickerDelegate: Sized + 'static { | 18 | pub 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 | |||
| 260 | pub fn simple_picker_match( | ||
| 261 | selected: bool, | ||
| 262 | cx: &mut WindowContext, | ||
| 263 | children: impl FnOnce(&mut WindowContext) -> AnyElement, | ||
| 264 | ) -> AnyElement { | ||
| 265 | let colors = cx.theme().colors(); | ||
| 266 | |||
| 267 | div() | ||
| 268 | .px_1() | ||
| 269 | .text_color(colors.text) | ||
| 270 | .text_ui() | ||
| 271 | .bg(colors.ghost_element_background) | ||
| 272 | .rounded_md() | ||
| 273 | .when(selected, |this| this.bg(colors.ghost_element_selected)) | ||
| 274 | .hover(|this| this.bg(colors.ghost_element_hover)) | ||
| 275 | .child((children)(cx)) | ||
| 276 | .into_any() | ||
| 277 | } | ||
| @@ -13,12 +13,14 @@ use node_runtime::NodeRuntime; · 12 unmodified lines | |||
| 13 | use serde::{Deserialize, Serialize}; | 13 | use serde::{Deserialize, Serialize}; |
| 14 | use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; | 14 | use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; |
| 15 | 15 | ||
| 16 | #[derive(Clone)] | ||
| 16 | pub enum Prettier { | 17 | pub 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)] | ||
| 22 | pub struct RealPrettier { | 24 | pub 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)] | ||
| 29 | pub struct TestPrettier { | 32 | pub struct TestPrettier { |
| 30 | prettier_dir: PathBuf, | 33 | prettier_dir: PathBuf, |
| 31 | default: bool, | 34 | default: bool, |
| 32 | } | 35 | } |
| 33 | 36 | ||
| 37 | pub const FAIL_THRESHOLD: usize = 4; | ||
| 34 | pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; | 38 | pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; |
| 35 | pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); | 39 | pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); |
| 36 | const PRETTIER_PACKAGE_NAME: &str = "prettier"; | 40 | const PRETTIER_PACKAGE_NAME: &str = "prettier"; |
| @@ -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 | } |
| @@ -13,12 +13,14 @@ use std::{ · 12 unmodified lines | |||
| 13 | }; | 13 | }; |
| 14 | use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; | 14 | use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; |
| 15 | 15 | ||
| 16 | #[derive(Clone)] | ||
| 16 | pub enum Prettier { | 17 | pub 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)] | ||
| 22 | pub struct RealPrettier { | 24 | pub 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)] | ||
| 29 | pub struct TestPrettier { | 32 | pub struct TestPrettier { |
| 30 | prettier_dir: PathBuf, | 33 | prettier_dir: PathBuf, |
| 31 | default: bool, | 34 | default: bool, |
| 32 | } | 35 | } |
| 33 | 36 | ||
| 37 | pub const FAIL_THRESHOLD: usize = 4; | ||
| 34 | pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; | 38 | pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; |
| 35 | pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); | 39 | pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); |
| 36 | const PRETTIER_PACKAGE_NAME: &str = "prettier"; | 40 | const PRETTIER_PACKAGE_NAME: &str = "prettier"; |
| @@ -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 | } |
| @@ -0,0 +1,758 @@ | |||
| 1 | use std::{ | ||
| 2 | ops::ControlFlow, | ||
| 3 | path::{Path, PathBuf}, | ||
| 4 | sync::Arc, | ||
| 5 | }; | ||
| 6 | |||
| 7 | use anyhow::Context; | ||
| 8 | use collections::HashSet; | ||
| 9 | use fs::Fs; | ||
| 10 | use futures::{ | ||
| 11 | future::{self, Shared}, | ||
| 12 | FutureExt, | ||
| 13 | }; | ||
| 14 | use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task}; | ||
| 15 | use language::{ | ||
| 16 | language_settings::{Formatter, LanguageSettings}, | ||
| 17 | Buffer, Language, LanguageServerName, LocalFile, | ||
| 18 | }; | ||
| 19 | use lsp::LanguageServerId; | ||
| 20 | use node_runtime::NodeRuntime; | ||
| 21 | use prettier::Prettier; | ||
| 22 | use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt}; | ||
| 23 | |||
| 24 | use crate::{ | ||
| 25 | Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId, | ||
| 26 | }; | ||
| 27 | |||
| 28 | pub fn prettier_plugins_for_language( | ||
| 29 | language: &Language, | ||
| 30 | language_settings: &LanguageSettings, | ||
| 31 | ) -> Option<HashSet<&'static str>> { | ||
| 32 | match &language_settings.formatter { | ||
| 33 | Formatter::Prettier { .. } | Formatter::Auto => {} | ||
| 34 | Formatter::LanguageServer | Formatter::External { .. } => return None, | ||
| 35 | }; | ||
| 36 | let mut prettier_plugins = None; | ||
| 37 | if language.prettier_parser_name().is_some() { | ||
| 38 | prettier_plugins | ||
| 39 | .get_or_insert_with(|| HashSet::default()) | ||
| 40 | .extend( | ||
| 41 | language | ||
| 42 | .lsp_adapters() | ||
| 43 | .iter() | ||
| 44 | .flat_map(|adapter| adapter.prettier_plugins()), | ||
| 45 | ) | ||
| 46 | } | ||
| 47 | |||
| 48 | prettier_plugins | ||
| 49 | } | ||
| 50 | |||
| 51 | pub(super) async fn format_with_prettier( | ||
| 52 | project: &ModelHandle<Project>, | ||
| 53 | buffer: &ModelHandle<Buffer>, | ||
| 54 | cx: &mut AsyncAppContext, | ||
| 55 | ) -> Option<FormatOperation> { | ||
| 56 | if let Some((prettier_path, prettier_task)) = project | ||
| 57 | .update(cx, |project, cx| { | ||
| 58 | project.prettier_instance_for_buffer(buffer, cx) | ||
| 59 | }) | ||
| 60 | .await | ||
| 61 | { | ||
| 62 | match prettier_task.await { | ||
| 63 | Ok(prettier) => { | ||
| 64 | let buffer_path = buffer.update(cx, |buffer, cx| { | ||
| 65 | File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) | ||
| 66 | }); | ||
| 67 | match prettier.format(buffer, buffer_path, cx).await { | ||
| 68 | Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)), | ||
| 69 | Err(e) => { | ||
| 70 | log::error!( | ||
| 71 | "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}" | ||
| 72 | ); | ||
| 73 | } | ||
| 74 | } | ||
| 75 | } | ||
| 76 | Err(e) => project.update(cx, |project, _| { | ||
| 77 | let instance_to_update = match prettier_path { | ||
| 78 | Some(prettier_path) => { | ||
| 79 | log::error!( | ||
| 80 | "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}" | ||
| 81 | ); | ||
| 82 | project.prettier_instances.get_mut(&prettier_path) | ||
| 83 | } | ||
| 84 | None => { | ||
| 85 | log::error!("Default prettier instance failed to spawn: {e:#}"); | ||
| 86 | match &mut project.default_prettier.prettier { | ||
| 87 | PrettierInstallation::NotInstalled { .. } => None, | ||
| 88 | PrettierInstallation::Installed(instance) => Some(instance), | ||
| 89 | } | ||
| 90 | } | ||
| 91 | }; | ||
| 92 | |||
| 93 | if let Some(instance) = instance_to_update { | ||
| 94 | instance.attempt += 1; | ||
| 95 | instance.prettier = None; | ||
| 96 | } | ||
| 97 | }), | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | None | ||
| 102 | } | ||
| 103 | |||
| 104 | pub struct DefaultPrettier { | ||
| 105 | prettier: PrettierInstallation, | ||
| 106 | installed_plugins: HashSet<&'static str>, | ||
| 107 | } | ||
| 108 | |||
| 109 | pub enum PrettierInstallation { | ||
| 110 | NotInstalled { | ||
| 111 | attempts: usize, | ||
| 112 | installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>, | ||
| 113 | not_installed_plugins: HashSet<&'static str>, | ||
| 114 | }, | ||
| 115 | Installed(PrettierInstance), | ||
| 116 | } | ||
| 117 | |||
| 118 | pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>; | ||
| 119 | |||
| 120 | #[derive(Clone)] | ||
| 121 | pub struct PrettierInstance { | ||
| 122 | attempt: usize, | ||
| 123 | prettier: Option<PrettierTask>, | ||
| 124 | } | ||
| 125 | |||
| 126 | impl Default for DefaultPrettier { | ||
| 127 | fn default() -> Self { | ||
| 128 | Self { | ||
| 129 | prettier: PrettierInstallation::NotInstalled { | ||
| 130 | attempts: 0, | ||
| 131 | installation_task: None, | ||
| 132 | not_installed_plugins: HashSet::default(), | ||
| 133 | }, | ||
| 134 | installed_plugins: HashSet::default(), | ||
| 135 | } | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | impl DefaultPrettier { | ||
| 140 | pub fn instance(&self) -> Option<&PrettierInstance> { | ||
| 141 | if let PrettierInstallation::Installed(instance) = &self.prettier { | ||
| 142 | Some(instance) | ||
| 143 | } else { | ||
| 144 | None | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | pub fn prettier_task( | ||
| 149 | &mut self, | ||
| 150 | node: &Arc<dyn NodeRuntime>, | ||
| 151 | worktree_id: Option<WorktreeId>, | ||
| 152 | cx: &mut ModelContext<'_, Project>, | ||
| 153 | ) -> Option<Task<anyhow::Result<PrettierTask>>> { | ||
| 154 | match &mut self.prettier { | ||
| 155 | PrettierInstallation::NotInstalled { .. } => { | ||
| 156 | Some(start_default_prettier(Arc::clone(node), worktree_id, cx)) | ||
| 157 | } | ||
| 158 | PrettierInstallation::Installed(existing_instance) => { | ||
| 159 | existing_instance.prettier_task(node, None, worktree_id, cx) | ||
| 160 | } | ||
| 161 | } | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | impl PrettierInstance { | ||
| 166 | pub fn prettier_task( | ||
| 167 | &mut self, | ||
| 168 | node: &Arc<dyn NodeRuntime>, | ||
| 169 | prettier_dir: Option<&Path>, | ||
| 170 | worktree_id: Option<WorktreeId>, | ||
| 171 | cx: &mut ModelContext<'_, Project>, | ||
| 172 | ) -> Option<Task<anyhow::Result<PrettierTask>>> { | ||
| 173 | if self.attempt > prettier::FAIL_THRESHOLD { | ||
| 174 | match prettier_dir { | ||
| 175 | Some(prettier_dir) => log::warn!( | ||
| 176 | "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting" | ||
| 177 | ), | ||
| 178 | None => log::warn!("Default prettier exceeded launch threshold, not starting"), | ||
| 179 | } | ||
| 180 | return None; | ||
| 181 | } | ||
| 182 | Some(match &self.prettier { | ||
| 183 | Some(prettier_task) => Task::ready(Ok(prettier_task.clone())), | ||
| 184 | None => match prettier_dir { | ||
| 185 | Some(prettier_dir) => { | ||
| 186 | let new_task = start_prettier( | ||
| 187 | Arc::clone(node), | ||
| 188 | prettier_dir.to_path_buf(), | ||
| 189 | worktree_id, | ||
| 190 | cx, | ||
| 191 | ); | ||
| 192 | self.attempt += 1; | ||
| 193 | self.prettier = Some(new_task.clone()); | ||
| 194 | Task::ready(Ok(new_task)) | ||
| 195 | } | ||
| 196 | None => { | ||
| 197 | self.attempt += 1; | ||
| 198 | let node = Arc::clone(node); | ||
| 199 | cx.spawn(|project, mut cx| async move { | ||
| 200 | project | ||
| 201 | .update(&mut cx, |_, cx| { | ||
| 202 | start_default_prettier(node, worktree_id, cx) | ||
| 203 | }) | ||
| 204 | .await | ||
| 205 | }) | ||
| 206 | } | ||
| 207 | }, | ||
| 208 | }) | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | fn start_default_prettier( | ||
| 213 | node: Arc<dyn NodeRuntime>, | ||
| 214 | worktree_id: Option<WorktreeId>, | ||
| 215 | cx: &mut ModelContext<'_, Project>, | ||
| 216 | ) -> Task<anyhow::Result<PrettierTask>> { | ||
| 217 | cx.spawn(|project, mut cx| async move { | ||
| 218 | loop { | ||
| 219 | let installation_task = project.update(&mut cx, |project, _| { | ||
| 220 | match &project.default_prettier.prettier { | ||
| 221 | PrettierInstallation::NotInstalled { | ||
| 222 | installation_task, .. | ||
| 223 | } => ControlFlow::Continue(installation_task.clone()), | ||
| 224 | PrettierInstallation::Installed(default_prettier) => { | ||
| 225 | ControlFlow::Break(default_prettier.clone()) | ||
| 226 | } | ||
| 227 | } | ||
| 228 | }); | ||
| 229 | match installation_task { | ||
| 230 | ControlFlow::Continue(None) => { | ||
| 231 | anyhow::bail!("Default prettier is not installed and cannot be started") | ||
| 232 | } | ||
| 233 | ControlFlow::Continue(Some(installation_task)) => { | ||
| 234 | log::info!("Waiting for default prettier to install"); | ||
| 235 | if let Err(e) = installation_task.await { | ||
| 236 | project.update(&mut cx, |project, _| { | ||
| 237 | if let PrettierInstallation::NotInstalled { | ||
| 238 | installation_task, | ||
| 239 | attempts, | ||
| 240 | .. | ||
| 241 | } = &mut project.default_prettier.prettier | ||
| 242 | { | ||
| 243 | *installation_task = None; | ||
| 244 | *attempts += 1; | ||
| 245 | } | ||
| 246 | }); | ||
| 247 | anyhow::bail!( | ||
| 248 | "Cannot start default prettier due to its installation failure: {e:#}" | ||
| 249 | ); | ||
| 250 | } | ||
| 251 | let new_default_prettier = project.update(&mut cx, |project, cx| { | ||
| 252 | let new_default_prettier = | ||
| 253 | start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); | ||
| 254 | project.default_prettier.prettier = | ||
| 255 | PrettierInstallation::Installed(PrettierInstance { | ||
| 256 | attempt: 0, | ||
| 257 | prettier: Some(new_default_prettier.clone()), | ||
| 258 | }); | ||
| 259 | new_default_prettier | ||
| 260 | }); | ||
| 261 | return Ok(new_default_prettier); | ||
| 262 | } | ||
| 263 | ControlFlow::Break(instance) => match instance.prettier { | ||
| 264 | Some(instance) => return Ok(instance), | ||
| 265 | None => { | ||
| 266 | let new_default_prettier = project.update(&mut cx, |project, cx| { | ||
| 267 | let new_default_prettier = | ||
| 268 | start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); | ||
| 269 | project.default_prettier.prettier = | ||
| 270 | PrettierInstallation::Installed(PrettierInstance { | ||
| 271 | attempt: instance.attempt + 1, | ||
| 272 | prettier: Some(new_default_prettier.clone()), | ||
| 273 | }); | ||
| 274 | new_default_prettier | ||
| 275 | }); | ||
| 276 | return Ok(new_default_prettier); | ||
| 277 | } | ||
| 278 | }, | ||
| 279 | } | ||
| 280 | } | ||
| 281 | }) | ||
| 282 | } | ||
| 283 | |||
| 284 | fn start_prettier( | ||
| 285 | node: Arc<dyn NodeRuntime>, | ||
| 286 | prettier_dir: PathBuf, | ||
| 287 | worktree_id: Option<WorktreeId>, | ||
| 288 | cx: &mut ModelContext<'_, Project>, | ||
| 289 | ) -> PrettierTask { | ||
| 290 | cx.spawn(|project, mut cx| async move { | ||
| 291 | log::info!("Starting prettier at path {prettier_dir:?}"); | ||
| 292 | let new_server_id = project.update(&mut cx, |project, _| { | ||
| 293 | project.languages.next_language_server_id() | ||
| 294 | }); | ||
| 295 | |||
| 296 | let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) | ||
| 297 | .await | ||
| 298 | .context("default prettier spawn") | ||
| 299 | .map(Arc::new) | ||
| 300 | .map_err(Arc::new)?; | ||
| 301 | register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); | ||
| 302 | Ok(new_prettier) | ||
| 303 | }) | ||
| 304 | .shared() | ||
| 305 | } | ||
| 306 | |||
| 307 | fn register_new_prettier( | ||
| 308 | project: &ModelHandle<Project>, | ||
| 309 | prettier: &Prettier, | ||
| 310 | worktree_id: Option<WorktreeId>, | ||
| 311 | new_server_id: LanguageServerId, | ||
| 312 | cx: &mut AsyncAppContext, | ||
| 313 | ) { | ||
| 314 | let prettier_dir = prettier.prettier_dir(); | ||
| 315 | let is_default = prettier.is_default(); | ||
| 316 | if is_default { | ||
| 317 | log::info!("Started default prettier in {prettier_dir:?}"); | ||
| 318 | } else { | ||
| 319 | log::info!("Started prettier in {prettier_dir:?}"); | ||
| 320 | } | ||
| 321 | if let Some(prettier_server) = prettier.server() { | ||
| 322 | project.update(cx, |project, cx| { | ||
| 323 | let name = if is_default { | ||
| 324 | LanguageServerName(Arc::from("prettier (default)")) | ||
| 325 | } else { | ||
| 326 | let worktree_path = worktree_id | ||
| 327 | .and_then(|id| project.worktree_for_id(id, cx)) | ||
| 328 | .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); | ||
| 329 | let name = match worktree_path { | ||
| 330 | Some(worktree_path) => { | ||
| 331 | if prettier_dir == worktree_path.as_ref() { | ||
| 332 | let name = prettier_dir | ||
| 333 | .file_name() | ||
| 334 | .and_then(|name| name.to_str()) | ||
| 335 | .unwrap_or_default(); | ||
| 336 | format!("prettier ({name})") | ||
| 337 | } else { | ||
| 338 | let dir_to_display = prettier_dir | ||
| 339 | .strip_prefix(worktree_path.as_ref()) | ||
| 340 | .ok() | ||
| 341 | .unwrap_or(prettier_dir); | ||
| 342 | format!("prettier ({})", dir_to_display.display()) | ||
| 343 | } | ||
| 344 | } | ||
| 345 | None => format!("prettier ({})", prettier_dir.display()), | ||
| 346 | }; | ||
| 347 | LanguageServerName(Arc::from(name)) | ||
| 348 | }; | ||
| 349 | project | ||
| 350 | .supplementary_language_servers | ||
| 351 | .insert(new_server_id, (name, Arc::clone(prettier_server))); | ||
| 352 | cx.emit(Event::LanguageServerAdded(new_server_id)); | ||
| 353 | }); | ||
| 354 | } | ||
| 355 | } | ||
| 356 | |||
| 357 | async fn install_prettier_packages( | ||
| 358 | plugins_to_install: HashSet<&'static str>, | ||
| 359 | node: Arc<dyn NodeRuntime>, | ||
| 360 | ) -> anyhow::Result<()> { | ||
| 361 | let packages_to_versions = | ||
| 362 | future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( | ||
| 363 | |package_name| async { | ||
| 364 | let returned_package_name = package_name.to_string(); | ||
| 365 | let latest_version = node | ||
| 366 | .npm_package_latest_version(package_name) | ||
| 367 | .await | ||
| 368 | .with_context(|| { | ||
| 369 | format!("fetching latest npm version for package {returned_package_name}") | ||
| 370 | })?; | ||
| 371 | anyhow::Ok((returned_package_name, latest_version)) | ||
| 372 | }, | ||
| 373 | )) | ||
| 374 | .await | ||
| 375 | .context("fetching latest npm versions")?; | ||
| 376 | |||
| 377 | log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); | ||
| 378 | let borrowed_packages = packages_to_versions | ||
| 379 | .iter() | ||
| 380 | .map(|(package, version)| (package.as_str(), version.as_str())) | ||
| 381 | .collect::<Vec<_>>(); | ||
| 382 | node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) | ||
| 383 | .await | ||
| 384 | .context("fetching formatter packages")?; | ||
| 385 | anyhow::Ok(()) | ||
| 386 | } | ||
| 387 | |||
| 388 | async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> { | ||
| 389 | let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); | ||
| 390 | fs.save( | ||
| 391 | &prettier_wrapper_path, | ||
| 392 | &text::Rope::from(prettier::PRETTIER_SERVER_JS), | ||
| 393 | text::LineEnding::Unix, | ||
| 394 | ) | ||
| 395 | .await | ||
| 396 | .with_context(|| { | ||
| 397 | format!( | ||
| 398 | "writing {} file at {prettier_wrapper_path:?}", | ||
| 399 | prettier::PRETTIER_SERVER_FILE | ||
| 400 | ) | ||
| 401 | })?; | ||
| 402 | Ok(()) | ||
| 403 | } | ||
| 404 | |||
| 405 | impl Project { | ||
| 406 | pub fn update_prettier_settings( | ||
| 407 | &self, | ||
| 408 | worktree: &ModelHandle<Worktree>, | ||
| 409 | changes: &[(Arc<Path>, ProjectEntryId, PathChange)], | ||
| 410 | cx: &mut ModelContext<'_, Project>, | ||
| 411 | ) { | ||
| 412 | let prettier_config_files = Prettier::CONFIG_FILE_NAMES | ||
| 413 | .iter() | ||
| 414 | .map(Path::new) | ||
| 415 | .collect::<HashSet<_>>(); | ||
| 416 | |||
| 417 | let prettier_config_file_changed = changes | ||
| 418 | .iter() | ||
| 419 | .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) | ||
| 420 | .filter(|(path, _, _)| { | ||
| 421 | !path | ||
| 422 | .components() | ||
| 423 | .any(|component| component.as_os_str().to_string_lossy() == "node_modules") | ||
| 424 | }) | ||
| 425 | .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); | ||
| 426 | let current_worktree_id = worktree.read(cx).id(); | ||
| 427 | if let Some((config_path, _, _)) = prettier_config_file_changed { | ||
| 428 | log::info!( | ||
| 429 | "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" | ||
| 430 | ); | ||
| 431 | let prettiers_to_reload = | ||
| 432 | self.prettiers_per_worktree | ||
| 433 | .get(¤t_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 | } | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | mod ignore; | 1 | mod ignore; |
| 2 | mod lsp_command; | 2 | mod lsp_command; |
| 3 | mod prettier_support; | ||
| 3 | pub mod project_settings; | 4 | pub mod project_settings; |
| 4 | pub mod search; | 5 | pub mod search; |
| 5 | pub mod terminals; | 6 | pub 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 | }; |
| 32 | use itertools::Itertools; | 33 | use itertools::Itertools; |
| 33 | use language::{ | 34 | use 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 | |||
| 54 | use node_runtime::NodeRuntime; | 53 | use node_runtime::NodeRuntime; |
| 55 | use parking_lot::Mutex; | 54 | use parking_lot::Mutex; |
| 56 | use postage::watch; | 55 | use postage::watch; |
| 57 | use prettier::Prettier; | 56 | use prettier_support::{DefaultPrettier, PrettierInstance}; |
| 58 | use project_settings::{LspSettings, ProjectSettings}; | 57 | use project_settings::{LspSettings, ProjectSettings}; |
| 59 | use rand::prelude::*; | 58 | use rand::prelude::*; |
| 60 | use search::SearchQuery; | 59 | use 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 | |||
| 85 | use terminals::Terminals; | 84 | use terminals::Terminals; |
| 86 | use text::Anchor; | 85 | use text::Anchor; |
| 87 | use util::{ | 86 | use 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 | ||
| 95 | pub use fs::*; | 91 | pub 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 | |||
| 176 | struct DefaultPrettier { | ||
| 177 | instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>, | ||
| 178 | installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>, | ||
| 179 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 180 | installed_plugins: HashSet<&'static str>, | ||
| 181 | } | 170 | } |
| 182 | 171 | ||
| 183 | struct DelayedDebounced { | 172 | struct 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. | ||
| 536 | enum FormatOperation { | ||
| 537 | Lsp(Vec<(Range<Anchor>, String)>), | ||
| 538 | External(Diff), | ||
| 539 | Prettier(Diff), | ||
| 540 | } | ||
| 541 | |||
| 545 | impl FormatTrigger { | 542 | impl 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 server | 4111 | // 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)) = project | 4159 | 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.await | 4163 | 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)) = project | 4182 | 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.await | 4186 | 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(¤t_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 | |||
| 8813 | fn start_default_prettier( | ||
| 8814 | node: Arc<dyn NodeRuntime>, | ||
| 8815 | worktree_id: Option<WorktreeId>, | ||
| 8816 | cx: &mut ModelContext<'_, Project>, | ||
| 8817 | ) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> { | ||
| 8818 | cx.spawn(|project, mut cx| async move { | ||
| 8819 | loop { | ||
| 8820 | let default_prettier_installing = project.update(&mut cx, |project, _| { | ||
| 8821 | project | ||
| 8822 | .default_prettier | ||
| 8823 | .as_ref() | ||
| 8824 | .and_then(|default_prettier| default_prettier.installation_process.clone()) | ||
| 8825 | }); | ||
| 8826 | match default_prettier_installing { | ||
| 8827 | Some(installation_task) => { | ||
| 8828 | if installation_task.await.is_ok() { | ||
| 8829 | break; | ||
| 8830 | } | ||
| 8831 | } | ||
| 8832 | None => break, | ||
| 8833 | } | ||
| 8834 | } | ||
| 8835 | |||
| 8836 | project.update(&mut cx, |project, cx| { | ||
| 8837 | match project | ||
| 8838 | .default_prettier | ||
| 8839 | .as_mut() | ||
| 8840 | .and_then(|default_prettier| default_prettier.instance.as_mut()) | ||
| 8841 | { | ||
| 8842 | Some(default_prettier) => default_prettier.clone(), | ||
| 8843 | None => { | ||
| 8844 | let new_default_prettier = | ||
| 8845 | start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); | ||
| 8846 | project | ||
| 8847 | .default_prettier | ||
| 8848 | .get_or_insert_with(|| DefaultPrettier { | ||
| 8849 | instance: None, | ||
| 8850 | installation_process: None, | ||
| 8851 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 8852 | installed_plugins: HashSet::default(), | ||
| 8853 | }) | ||
| 8854 | .instance = Some(new_default_prettier.clone()); | ||
| 8855 | new_default_prettier | ||
| 8856 | } | ||
| 8857 | } | ||
| 8858 | }) | ||
| 8859 | }) | ||
| 8860 | } | ||
| 8861 | |||
| 8862 | fn start_prettier( | ||
| 8863 | node: Arc<dyn NodeRuntime>, | ||
| 8864 | prettier_dir: PathBuf, | ||
| 8865 | worktree_id: Option<WorktreeId>, | ||
| 8866 | cx: &mut ModelContext<'_, Project>, | ||
| 8867 | ) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> { | ||
| 8868 | cx.spawn(|project, mut cx| async move { | ||
| 8869 | let new_server_id = project.update(&mut cx, |project, _| { | ||
| 8870 | project.languages.next_language_server_id() | ||
| 8871 | }); | ||
| 8872 | let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) | ||
| 8873 | .await | ||
| 8874 | .context("default prettier spawn") | ||
| 8875 | .map(Arc::new) | ||
| 8876 | .map_err(Arc::new)?; | ||
| 8877 | register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); | ||
| 8878 | Ok(new_prettier) | ||
| 8879 | }) | ||
| 8880 | .shared() | ||
| 8881 | } | ||
| 8882 | |||
| 8883 | fn register_new_prettier( | ||
| 8884 | project: &ModelHandle<Project>, | ||
| 8885 | prettier: &Prettier, | ||
| 8886 | worktree_id: Option<WorktreeId>, | ||
| 8887 | new_server_id: LanguageServerId, | ||
| 8888 | cx: &mut AsyncAppContext, | ||
| 8889 | ) { | ||
| 8890 | let prettier_dir = prettier.prettier_dir(); | ||
| 8891 | let is_default = prettier.is_default(); | ||
| 8892 | if is_default { | ||
| 8893 | log::info!("Started default prettier in {prettier_dir:?}"); | ||
| 8894 | } else { | ||
| 8895 | log::info!("Started prettier in {prettier_dir:?}"); | ||
| 8896 | } | ||
| 8897 | if let Some(prettier_server) = prettier.server() { | ||
| 8898 | project.update(cx, |project, cx| { | ||
| 8899 | let name = if is_default { | ||
| 8900 | LanguageServerName(Arc::from("prettier (default)")) | ||
| 8901 | } else { | ||
| 8902 | let worktree_path = worktree_id | ||
| 8903 | .and_then(|id| project.worktree_for_id(id, cx)) | ||
| 8904 | .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); | ||
| 8905 | let name = match worktree_path { | ||
| 8906 | Some(worktree_path) => { | ||
| 8907 | if prettier_dir == worktree_path.as_ref() { | ||
| 8908 | let name = prettier_dir | ||
| 8909 | .file_name() | ||
| 8910 | .and_then(|name| name.to_str()) | ||
| 8911 | .unwrap_or_default(); | ||
| 8912 | format!("prettier ({name})") | ||
| 8913 | } else { | ||
| 8914 | let dir_to_display = prettier_dir | ||
| 8915 | .strip_prefix(worktree_path.as_ref()) | ||
| 8916 | .ok() | ||
| 8917 | .unwrap_or(prettier_dir); | ||
| 8918 | format!("prettier ({})", dir_to_display.display()) | ||
| 8919 | } | ||
| 8920 | } | ||
| 8921 | None => format!("prettier ({})", prettier_dir.display()), | ||
| 8922 | }; | ||
| 8923 | LanguageServerName(Arc::from(name)) | ||
| 8924 | }; | ||
| 8925 | project | ||
| 8926 | .supplementary_language_servers | ||
| 8927 | .insert(new_server_id, (name, Arc::clone(prettier_server))); | ||
| 8928 | cx.emit(Event::LanguageServerAdded(new_server_id)); | ||
| 8929 | }); | ||
| 8930 | } | ||
| 8931 | } | ||
| 8932 | |||
| 8933 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 8934 | async fn install_default_prettier( | ||
| 8935 | plugins_to_install: HashSet<&'static str>, | ||
| 8936 | node: Arc<dyn NodeRuntime>, | ||
| 8937 | fs: Arc<dyn Fs>, | ||
| 8938 | ) -> anyhow::Result<()> { | ||
| 8939 | let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); | ||
| 8940 | // method creates parent directory if it doesn't exist | ||
| 8941 | fs.save( | ||
| 8942 | &prettier_wrapper_path, | ||
| 8943 | &text::Rope::from(prettier::PRETTIER_SERVER_JS), | ||
| 8944 | text::LineEnding::Unix, | ||
| 8945 | ) | ||
| 8946 | .await | ||
| 8947 | .with_context(|| { | ||
| 8948 | format!( | ||
| 8949 | "writing {} file at {prettier_wrapper_path:?}", | ||
| 8950 | prettier::PRETTIER_SERVER_FILE | ||
| 8951 | ) | ||
| 8952 | })?; | ||
| 8953 | |||
| 8954 | let packages_to_versions = | ||
| 8955 | future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( | ||
| 8956 | |package_name| async { | ||
| 8957 | let returned_package_name = package_name.to_string(); | ||
| 8958 | let latest_version = node | ||
| 8959 | .npm_package_latest_version(package_name) | ||
| 8960 | .await | ||
| 8961 | .with_context(|| { | ||
| 8962 | format!("fetching latest npm version for package {returned_package_name}") | ||
| 8963 | })?; | ||
| 8964 | anyhow::Ok((returned_package_name, latest_version)) | ||
| 8965 | }, | ||
| 8966 | )) | ||
| 8967 | .await | ||
| 8968 | .context("fetching latest npm versions")?; | ||
| 8969 | |||
| 8970 | log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); | ||
| 8971 | let borrowed_packages = packages_to_versions | ||
| 8972 | .iter() | ||
| 8973 | .map(|(package, version)| (package.as_str(), version.as_str())) | ||
| 8974 | .collect::<Vec<_>>(); | ||
| 8975 | node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) | ||
| 8976 | .await | ||
| 8977 | .context("fetching formatter packages")?; | ||
| 8978 | anyhow::Ok(()) | ||
| 8979 | } | 8391 | } |
| 8980 | 8392 | ||
| 8981 | fn subscribe_for_copilot_events( | 8393 | fn subscribe_for_copilot_events( |
| @@ -0,0 +1,772 @@ | |||
| 1 | use std::{ | ||
| 2 | ops::ControlFlow, | ||
| 3 | path::{Path, PathBuf}, | ||
| 4 | sync::Arc, | ||
| 5 | }; | ||
| 6 | |||
| 7 | use anyhow::Context; | ||
| 8 | use collections::HashSet; | ||
| 9 | use fs::Fs; | ||
| 10 | use futures::{ | ||
| 11 | future::{self, Shared}, | ||
| 12 | FutureExt, | ||
| 13 | }; | ||
| 14 | use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel}; | ||
| 15 | use language::{ | ||
| 16 | language_settings::{Formatter, LanguageSettings}, | ||
| 17 | Buffer, Language, LanguageServerName, LocalFile, | ||
| 18 | }; | ||
| 19 | use lsp::LanguageServerId; | ||
| 20 | use node_runtime::NodeRuntime; | ||
| 21 | use prettier::Prettier; | ||
| 22 | use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt}; | ||
| 23 | |||
| 24 | use crate::{ | ||
| 25 | Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId, | ||
| 26 | }; | ||
| 27 | |||
| 28 | pub fn prettier_plugins_for_language( | ||
| 29 | language: &Language, | ||
| 30 | language_settings: &LanguageSettings, | ||
| 31 | ) -> Option<HashSet<&'static str>> { | ||
| 32 | match &language_settings.formatter { | ||
| 33 | Formatter::Prettier { .. } | Formatter::Auto => {} | ||
| 34 | Formatter::LanguageServer | Formatter::External { .. } => return None, | ||
| 35 | }; | ||
| 36 | let mut prettier_plugins = None; | ||
| 37 | if language.prettier_parser_name().is_some() { | ||
| 38 | prettier_plugins | ||
| 39 | .get_or_insert_with(|| HashSet::default()) | ||
| 40 | .extend( | ||
| 41 | language | ||
| 42 | .lsp_adapters() | ||
| 43 | .iter() | ||
| 44 | .flat_map(|adapter| adapter.prettier_plugins()), | ||
| 45 | ) | ||
| 46 | } | ||
| 47 | |||
| 48 | prettier_plugins | ||
| 49 | } | ||
| 50 | |||
| 51 | pub(super) async fn format_with_prettier( | ||
| 52 | project: &WeakModel<Project>, | ||
| 53 | buffer: &Model<Buffer>, | ||
| 54 | cx: &mut AsyncAppContext, | ||
| 55 | ) -> Option<FormatOperation> { | ||
| 56 | if let Some((prettier_path, prettier_task)) = project | ||
| 57 | .update(cx, |project, cx| { | ||
| 58 | project.prettier_instance_for_buffer(buffer, cx) | ||
| 59 | }) | ||
| 60 | .ok()? | ||
| 61 | .await | ||
| 62 | { | ||
| 63 | match prettier_task.await { | ||
| 64 | Ok(prettier) => { | ||
| 65 | let buffer_path = buffer | ||
| 66 | .update(cx, |buffer, cx| { | ||
| 67 | File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) | ||
| 68 | }) | ||
| 69 | .ok()?; | ||
| 70 | match prettier.format(buffer, buffer_path, cx).await { | ||
| 71 | Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)), | ||
| 72 | Err(e) => { | ||
| 73 | log::error!( | ||
| 74 | "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}" | ||
| 75 | ); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | Err(e) => project | ||
| 80 | .update(cx, |project, _| { | ||
| 81 | let instance_to_update = match prettier_path { | ||
| 82 | Some(prettier_path) => { | ||
| 83 | log::error!( | ||
| 84 | "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}" | ||
| 85 | ); | ||
| 86 | project.prettier_instances.get_mut(&prettier_path) | ||
| 87 | } | ||
| 88 | None => { | ||
| 89 | log::error!("Default prettier instance failed to spawn: {e:#}"); | ||
| 90 | match &mut project.default_prettier.prettier { | ||
| 91 | PrettierInstallation::NotInstalled { .. } => None, | ||
| 92 | PrettierInstallation::Installed(instance) => Some(instance), | ||
| 93 | } | ||
| 94 | } | ||
| 95 | }; | ||
| 96 | |||
| 97 | if let Some(instance) = instance_to_update { | ||
| 98 | instance.attempt += 1; | ||
| 99 | instance.prettier = None; | ||
| 100 | } | ||
| 101 | }) | ||
| 102 | .ok()?, | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | None | ||
| 107 | } | ||
| 108 | |||
| 109 | pub struct DefaultPrettier { | ||
| 110 | prettier: PrettierInstallation, | ||
| 111 | installed_plugins: HashSet<&'static str>, | ||
| 112 | } | ||
| 113 | |||
| 114 | pub enum PrettierInstallation { | ||
| 115 | NotInstalled { | ||
| 116 | attempts: usize, | ||
| 117 | installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>, | ||
| 118 | not_installed_plugins: HashSet<&'static str>, | ||
| 119 | }, | ||
| 120 | Installed(PrettierInstance), | ||
| 121 | } | ||
| 122 | |||
| 123 | pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>; | ||
| 124 | |||
| 125 | #[derive(Clone)] | ||
| 126 | pub struct PrettierInstance { | ||
| 127 | attempt: usize, | ||
| 128 | prettier: Option<PrettierTask>, | ||
| 129 | } | ||
| 130 | |||
| 131 | impl Default for DefaultPrettier { | ||
| 132 | fn default() -> Self { | ||
| 133 | Self { | ||
| 134 | prettier: PrettierInstallation::NotInstalled { | ||
| 135 | attempts: 0, | ||
| 136 | installation_task: None, | ||
| 137 | not_installed_plugins: HashSet::default(), | ||
| 138 | }, | ||
| 139 | installed_plugins: HashSet::default(), | ||
| 140 | } | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | impl DefaultPrettier { | ||
| 145 | pub fn instance(&self) -> Option<&PrettierInstance> { | ||
| 146 | if let PrettierInstallation::Installed(instance) = &self.prettier { | ||
| 147 | Some(instance) | ||
| 148 | } else { | ||
| 149 | None | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | pub fn prettier_task( | ||
| 154 | &mut self, | ||
| 155 | node: &Arc<dyn NodeRuntime>, | ||
| 156 | worktree_id: Option<WorktreeId>, | ||
| 157 | cx: &mut ModelContext<'_, Project>, | ||
| 158 | ) -> Option<Task<anyhow::Result<PrettierTask>>> { | ||
| 159 | match &mut self.prettier { | ||
| 160 | PrettierInstallation::NotInstalled { .. } => { | ||
| 161 | Some(start_default_prettier(Arc::clone(node), worktree_id, cx)) | ||
| 162 | } | ||
| 163 | PrettierInstallation::Installed(existing_instance) => { | ||
| 164 | existing_instance.prettier_task(node, None, worktree_id, cx) | ||
| 165 | } | ||
| 166 | } | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | impl PrettierInstance { | ||
| 171 | pub fn prettier_task( | ||
| 172 | &mut self, | ||
| 173 | node: &Arc<dyn NodeRuntime>, | ||
| 174 | prettier_dir: Option<&Path>, | ||
| 175 | worktree_id: Option<WorktreeId>, | ||
| 176 | cx: &mut ModelContext<'_, Project>, | ||
| 177 | ) -> Option<Task<anyhow::Result<PrettierTask>>> { | ||
| 178 | if self.attempt > prettier::FAIL_THRESHOLD { | ||
| 179 | match prettier_dir { | ||
| 180 | Some(prettier_dir) => log::warn!( | ||
| 181 | "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting" | ||
| 182 | ), | ||
| 183 | None => log::warn!("Default prettier exceeded launch threshold, not starting"), | ||
| 184 | } | ||
| 185 | return None; | ||
| 186 | } | ||
| 187 | Some(match &self.prettier { | ||
| 188 | Some(prettier_task) => Task::ready(Ok(prettier_task.clone())), | ||
| 189 | None => match prettier_dir { | ||
| 190 | Some(prettier_dir) => { | ||
| 191 | let new_task = start_prettier( | ||
| 192 | Arc::clone(node), | ||
| 193 | prettier_dir.to_path_buf(), | ||
| 194 | worktree_id, | ||
| 195 | cx, | ||
| 196 | ); | ||
| 197 | self.attempt += 1; | ||
| 198 | self.prettier = Some(new_task.clone()); | ||
| 199 | Task::ready(Ok(new_task)) | ||
| 200 | } | ||
| 201 | None => { | ||
| 202 | self.attempt += 1; | ||
| 203 | let node = Arc::clone(node); | ||
| 204 | cx.spawn(|project, mut cx| async move { | ||
| 205 | project | ||
| 206 | .update(&mut cx, |_, cx| { | ||
| 207 | start_default_prettier(node, worktree_id, cx) | ||
| 208 | })? | ||
| 209 | .await | ||
| 210 | }) | ||
| 211 | } | ||
| 212 | }, | ||
| 213 | }) | ||
| 214 | } | ||
| 215 | } | ||
| 216 | |||
| 217 | fn start_default_prettier( | ||
| 218 | node: Arc<dyn NodeRuntime>, | ||
| 219 | worktree_id: Option<WorktreeId>, | ||
| 220 | cx: &mut ModelContext<'_, Project>, | ||
| 221 | ) -> Task<anyhow::Result<PrettierTask>> { | ||
| 222 | cx.spawn(|project, mut cx| async move { | ||
| 223 | loop { | ||
| 224 | let installation_task = project.update(&mut cx, |project, _| { | ||
| 225 | match &project.default_prettier.prettier { | ||
| 226 | PrettierInstallation::NotInstalled { | ||
| 227 | installation_task, .. | ||
| 228 | } => ControlFlow::Continue(installation_task.clone()), | ||
| 229 | PrettierInstallation::Installed(default_prettier) => { | ||
| 230 | ControlFlow::Break(default_prettier.clone()) | ||
| 231 | } | ||
| 232 | } | ||
| 233 | })?; | ||
| 234 | match installation_task { | ||
| 235 | ControlFlow::Continue(None) => { | ||
| 236 | anyhow::bail!("Default prettier is not installed and cannot be started") | ||
| 237 | } | ||
| 238 | ControlFlow::Continue(Some(installation_task)) => { | ||
| 239 | log::info!("Waiting for default prettier to install"); | ||
| 240 | if let Err(e) = installation_task.await { | ||
| 241 | project.update(&mut cx, |project, _| { | ||
| 242 | if let PrettierInstallation::NotInstalled { | ||
| 243 | installation_task, | ||
| 244 | attempts, | ||
| 245 | .. | ||
| 246 | } = &mut project.default_prettier.prettier | ||
| 247 | { | ||
| 248 | *installation_task = None; | ||
| 249 | *attempts += 1; | ||
| 250 | } | ||
| 251 | })?; | ||
| 252 | anyhow::bail!( | ||
| 253 | "Cannot start default prettier due to its installation failure: {e:#}" | ||
| 254 | ); | ||
| 255 | } | ||
| 256 | let new_default_prettier = project.update(&mut cx, |project, cx| { | ||
| 257 | let new_default_prettier = | ||
| 258 | start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); | ||
| 259 | project.default_prettier.prettier = | ||
| 260 | PrettierInstallation::Installed(PrettierInstance { | ||
| 261 | attempt: 0, | ||
| 262 | prettier: Some(new_default_prettier.clone()), | ||
| 263 | }); | ||
| 264 | new_default_prettier | ||
| 265 | })?; | ||
| 266 | return Ok(new_default_prettier); | ||
| 267 | } | ||
| 268 | ControlFlow::Break(instance) => match instance.prettier { | ||
| 269 | Some(instance) => return Ok(instance), | ||
| 270 | None => { | ||
| 271 | let new_default_prettier = project.update(&mut cx, |project, cx| { | ||
| 272 | let new_default_prettier = | ||
| 273 | start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); | ||
| 274 | project.default_prettier.prettier = | ||
| 275 | PrettierInstallation::Installed(PrettierInstance { | ||
| 276 | attempt: instance.attempt + 1, | ||
| 277 | prettier: Some(new_default_prettier.clone()), | ||
| 278 | }); | ||
| 279 | new_default_prettier | ||
| 280 | })?; | ||
| 281 | return Ok(new_default_prettier); | ||
| 282 | } | ||
| 283 | }, | ||
| 284 | } | ||
| 285 | } | ||
| 286 | }) | ||
| 287 | } | ||
| 288 | |||
| 289 | fn start_prettier( | ||
| 290 | node: Arc<dyn NodeRuntime>, | ||
| 291 | prettier_dir: PathBuf, | ||
| 292 | worktree_id: Option<WorktreeId>, | ||
| 293 | cx: &mut ModelContext<'_, Project>, | ||
| 294 | ) -> PrettierTask { | ||
| 295 | cx.spawn(|project, mut cx| async move { | ||
| 296 | log::info!("Starting prettier at path {prettier_dir:?}"); | ||
| 297 | let new_server_id = project.update(&mut cx, |project, _| { | ||
| 298 | project.languages.next_language_server_id() | ||
| 299 | })?; | ||
| 300 | |||
| 301 | let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) | ||
| 302 | .await | ||
| 303 | .context("default prettier spawn") | ||
| 304 | .map(Arc::new) | ||
| 305 | .map_err(Arc::new)?; | ||
| 306 | register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); | ||
| 307 | Ok(new_prettier) | ||
| 308 | }) | ||
| 309 | .shared() | ||
| 310 | } | ||
| 311 | |||
| 312 | fn register_new_prettier( | ||
| 313 | project: &WeakModel<Project>, | ||
| 314 | prettier: &Prettier, | ||
| 315 | worktree_id: Option<WorktreeId>, | ||
| 316 | new_server_id: LanguageServerId, | ||
| 317 | cx: &mut AsyncAppContext, | ||
| 318 | ) { | ||
| 319 | let prettier_dir = prettier.prettier_dir(); | ||
| 320 | let is_default = prettier.is_default(); | ||
| 321 | if is_default { | ||
| 322 | log::info!("Started default prettier in {prettier_dir:?}"); | ||
| 323 | } else { | ||
| 324 | log::info!("Started prettier in {prettier_dir:?}"); | ||
| 325 | } | ||
| 326 | if let Some(prettier_server) = prettier.server() { | ||
| 327 | project | ||
| 328 | .update(cx, |project, cx| { | ||
| 329 | let name = if is_default { | ||
| 330 | LanguageServerName(Arc::from("prettier (default)")) | ||
| 331 | } else { | ||
| 332 | let worktree_path = worktree_id | ||
| 333 | .and_then(|id| project.worktree_for_id(id, cx)) | ||
| 334 | .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); | ||
| 335 | let name = match worktree_path { | ||
| 336 | Some(worktree_path) => { | ||
| 337 | if prettier_dir == worktree_path.as_ref() { | ||
| 338 | let name = prettier_dir | ||
| 339 | .file_name() | ||
| 340 | .and_then(|name| name.to_str()) | ||
| 341 | .unwrap_or_default(); | ||
| 342 | format!("prettier ({name})") | ||
| 343 | } else { | ||
| 344 | let dir_to_display = prettier_dir | ||
| 345 | .strip_prefix(worktree_path.as_ref()) | ||
| 346 | .ok() | ||
| 347 | .unwrap_or(prettier_dir); | ||
| 348 | format!("prettier ({})", dir_to_display.display()) | ||
| 349 | } | ||
| 350 | } | ||
| 351 | None => format!("prettier ({})", prettier_dir.display()), | ||
| 352 | }; | ||
| 353 | LanguageServerName(Arc::from(name)) | ||
| 354 | }; | ||
| 355 | project | ||
| 356 | .supplementary_language_servers | ||
| 357 | .insert(new_server_id, (name, Arc::clone(prettier_server))); | ||
| 358 | cx.emit(Event::LanguageServerAdded(new_server_id)); | ||
| 359 | }) | ||
| 360 | .ok(); | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | async fn install_prettier_packages( | ||
| 365 | plugins_to_install: HashSet<&'static str>, | ||
| 366 | node: Arc<dyn NodeRuntime>, | ||
| 367 | ) -> anyhow::Result<()> { | ||
| 368 | let packages_to_versions = | ||
| 369 | future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( | ||
| 370 | |package_name| async { | ||
| 371 | let returned_package_name = package_name.to_string(); | ||
| 372 | let latest_version = node | ||
| 373 | .npm_package_latest_version(package_name) | ||
| 374 | .await | ||
| 375 | .with_context(|| { | ||
| 376 | format!("fetching latest npm version for package {returned_package_name}") | ||
| 377 | })?; | ||
| 378 | anyhow::Ok((returned_package_name, latest_version)) | ||
| 379 | }, | ||
| 380 | )) | ||
| 381 | .await | ||
| 382 | .context("fetching latest npm versions")?; | ||
| 383 | |||
| 384 | log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); | ||
| 385 | let borrowed_packages = packages_to_versions | ||
| 386 | .iter() | ||
| 387 | .map(|(package, version)| (package.as_str(), version.as_str())) | ||
| 388 | .collect::<Vec<_>>(); | ||
| 389 | node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) | ||
| 390 | .await | ||
| 391 | .context("fetching formatter packages")?; | ||
| 392 | anyhow::Ok(()) | ||
| 393 | } | ||
| 394 | |||
| 395 | async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> { | ||
| 396 | let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); | ||
| 397 | fs.save( | ||
| 398 | &prettier_wrapper_path, | ||
| 399 | &text::Rope::from(prettier::PRETTIER_SERVER_JS), | ||
| 400 | text::LineEnding::Unix, | ||
| 401 | ) | ||
| 402 | .await | ||
| 403 | .with_context(|| { | ||
| 404 | format!( | ||
| 405 | "writing {} file at {prettier_wrapper_path:?}", | ||
| 406 | prettier::PRETTIER_SERVER_FILE | ||
| 407 | ) | ||
| 408 | })?; | ||
| 409 | Ok(()) | ||
| 410 | } | ||
| 411 | |||
| 412 | impl Project { | ||
| 413 | pub fn update_prettier_settings( | ||
| 414 | &self, | ||
| 415 | worktree: &Model<Worktree>, | ||
| 416 | changes: &[(Arc<Path>, ProjectEntryId, PathChange)], | ||
| 417 | cx: &mut ModelContext<'_, Project>, | ||
| 418 | ) { | ||
| 419 | let prettier_config_files = Prettier::CONFIG_FILE_NAMES | ||
| 420 | .iter() | ||
| 421 | .map(Path::new) | ||
| 422 | .collect::<HashSet<_>>(); | ||
| 423 | |||
| 424 | let prettier_config_file_changed = changes | ||
| 425 | .iter() | ||
| 426 | .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) | ||
| 427 | .filter(|(path, _, _)| { | ||
| 428 | !path | ||
| 429 | .components() | ||
| 430 | .any(|component| component.as_os_str().to_string_lossy() == "node_modules") | ||
| 431 | }) | ||
| 432 | .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); | ||
| 433 | let current_worktree_id = worktree.read(cx).id(); | ||
| 434 | if let Some((config_path, _, _)) = prettier_config_file_changed { | ||
| 435 | log::info!( | ||
| 436 | "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" | ||
| 437 | ); | ||
| 438 | let prettiers_to_reload = | ||
| 439 | self.prettiers_per_worktree | ||
| 440 | .get(¤t_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 | } | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | mod ignore; | 1 | mod ignore; |
| 2 | mod lsp_command; | 2 | mod lsp_command; |
| 3 | mod prettier_support; | ||
| 3 | pub mod project_settings; | 4 | pub mod project_settings; |
| 4 | pub mod search; | 5 | pub mod search; |
| 5 | pub mod terminals; | 6 | pub 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 | }; |
| 32 | use itertools::Itertools; | 33 | use itertools::Itertools; |
| 33 | use language::{ | 34 | use 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 | |||
| 54 | use node_runtime::NodeRuntime; | 53 | use node_runtime::NodeRuntime; |
| 55 | use parking_lot::Mutex; | 54 | use parking_lot::Mutex; |
| 56 | use postage::watch; | 55 | use postage::watch; |
| 57 | use prettier::Prettier; | 56 | use prettier_support::{DefaultPrettier, PrettierInstance}; |
| 58 | use project_settings::{LspSettings, ProjectSettings}; | 57 | use project_settings::{LspSettings, ProjectSettings}; |
| 59 | use rand::prelude::*; | 58 | use rand::prelude::*; |
| 60 | use search::SearchQuery; | 59 | use 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 | |||
| 83 | use terminals::Terminals; | 82 | use terminals::Terminals; |
| 84 | use text::Anchor; | 83 | use text::Anchor; |
| 85 | use util::{ | 84 | use 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 | ||
| 93 | pub use fs::*; | 89 | pub 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 | |||
| 174 | struct DefaultPrettier { | ||
| 175 | instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>, | ||
| 176 | installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>, | ||
| 177 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 178 | installed_plugins: HashSet<&'static str>, | ||
| 179 | } | 168 | } |
| 180 | 169 | ||
| 181 | struct DelayedDebounced { | 170 | struct 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. | ||
| 534 | enum FormatOperation { | ||
| 535 | Lsp(Vec<(Range<Anchor>, String)>), | ||
| 536 | External(Diff), | ||
| 537 | Prettier(Diff), | ||
| 538 | } | ||
| 539 | |||
| 543 | impl FormatTrigger { | 540 | impl 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_formatted | 4137 | 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 server | 4177 | // 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)) = project | 4225 | 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.await | 4229 | 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)) = project | 4248 | 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.await | 4252 | 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(¤t_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 | |||
| 8880 | fn start_default_prettier( | ||
| 8881 | node: Arc<dyn NodeRuntime>, | ||
| 8882 | worktree_id: Option<WorktreeId>, | ||
| 8883 | cx: &mut ModelContext<'_, Project>, | ||
| 8884 | ) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> { | ||
| 8885 | cx.spawn(|project, mut cx| async move { | ||
| 8886 | loop { | ||
| 8887 | let default_prettier_installing = match project.update(&mut cx, |project, _| { | ||
| 8888 | project | ||
| 8889 | .default_prettier | ||
| 8890 | .as_ref() | ||
| 8891 | .and_then(|default_prettier| default_prettier.installation_process.clone()) | ||
| 8892 | }) { | ||
| 8893 | Ok(installation) => installation, | ||
| 8894 | Err(e) => { | ||
| 8895 | return Task::ready(Err(Arc::new( | ||
| 8896 | e.context("project is gone during default prettier installation"), | ||
| 8897 | ))) | ||
| 8898 | .shared() | ||
| 8899 | } | ||
| 8900 | }; | ||
| 8901 | match default_prettier_installing { | ||
| 8902 | Some(installation_task) => { | ||
| 8903 | if installation_task.await.is_ok() { | ||
| 8904 | break; | ||
| 8905 | } | ||
| 8906 | } | ||
| 8907 | None => break, | ||
| 8908 | } | ||
| 8909 | } | ||
| 8910 | |||
| 8911 | match project.update(&mut cx, |project, cx| { | ||
| 8912 | match project | ||
| 8913 | .default_prettier | ||
| 8914 | .as_mut() | ||
| 8915 | .and_then(|default_prettier| default_prettier.instance.as_mut()) | ||
| 8916 | { | ||
| 8917 | Some(default_prettier) => default_prettier.clone(), | ||
| 8918 | None => { | ||
| 8919 | let new_default_prettier = | ||
| 8920 | start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx); | ||
| 8921 | project | ||
| 8922 | .default_prettier | ||
| 8923 | .get_or_insert_with(|| DefaultPrettier { | ||
| 8924 | instance: None, | ||
| 8925 | installation_process: None, | ||
| 8926 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 8927 | installed_plugins: HashSet::default(), | ||
| 8928 | }) | ||
| 8929 | .instance = Some(new_default_prettier.clone()); | ||
| 8930 | new_default_prettier | ||
| 8931 | } | ||
| 8932 | } | ||
| 8933 | }) { | ||
| 8934 | Ok(task) => task, | ||
| 8935 | Err(e) => Task::ready(Err(Arc::new( | ||
| 8936 | e.context("project is gone during default prettier startup"), | ||
| 8937 | ))) | ||
| 8938 | .shared(), | ||
| 8939 | } | ||
| 8940 | }) | ||
| 8941 | } | ||
| 8942 | |||
| 8943 | fn start_prettier( | ||
| 8944 | node: Arc<dyn NodeRuntime>, | ||
| 8945 | prettier_dir: PathBuf, | ||
| 8946 | worktree_id: Option<WorktreeId>, | ||
| 8947 | cx: &mut ModelContext<'_, Project>, | ||
| 8948 | ) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> { | ||
| 8949 | cx.spawn(|project, mut cx| async move { | ||
| 8950 | let new_server_id = project.update(&mut cx, |project, _| { | ||
| 8951 | project.languages.next_language_server_id() | ||
| 8952 | })?; | ||
| 8953 | let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) | ||
| 8954 | .await | ||
| 8955 | .context("default prettier spawn") | ||
| 8956 | .map(Arc::new) | ||
| 8957 | .map_err(Arc::new)?; | ||
| 8958 | register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx); | ||
| 8959 | Ok(new_prettier) | ||
| 8960 | }) | ||
| 8961 | .shared() | ||
| 8962 | } | ||
| 8963 | |||
| 8964 | fn register_new_prettier( | ||
| 8965 | project: &WeakModel<Project>, | ||
| 8966 | prettier: &Prettier, | ||
| 8967 | worktree_id: Option<WorktreeId>, | ||
| 8968 | new_server_id: LanguageServerId, | ||
| 8969 | cx: &mut AsyncAppContext, | ||
| 8970 | ) { | ||
| 8971 | let prettier_dir = prettier.prettier_dir(); | ||
| 8972 | let is_default = prettier.is_default(); | ||
| 8973 | if is_default { | ||
| 8974 | log::info!("Started default prettier in {prettier_dir:?}"); | ||
| 8975 | } else { | ||
| 8976 | log::info!("Started prettier in {prettier_dir:?}"); | ||
| 8977 | } | ||
| 8978 | if let Some(prettier_server) = prettier.server() { | ||
| 8979 | project | ||
| 8980 | .update(cx, |project, cx| { | ||
| 8981 | let name = if is_default { | ||
| 8982 | LanguageServerName(Arc::from("prettier (default)")) | ||
| 8983 | } else { | ||
| 8984 | let worktree_path = worktree_id | ||
| 8985 | .and_then(|id| project.worktree_for_id(id, cx)) | ||
| 8986 | .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path())); | ||
| 8987 | let name = match worktree_path { | ||
| 8988 | Some(worktree_path) => { | ||
| 8989 | if prettier_dir == worktree_path.as_ref() { | ||
| 8990 | let name = prettier_dir | ||
| 8991 | .file_name() | ||
| 8992 | .and_then(|name| name.to_str()) | ||
| 8993 | .unwrap_or_default(); | ||
| 8994 | format!("prettier ({name})") | ||
| 8995 | } else { | ||
| 8996 | let dir_to_display = prettier_dir | ||
| 8997 | .strip_prefix(worktree_path.as_ref()) | ||
| 8998 | .ok() | ||
| 8999 | .unwrap_or(prettier_dir); | ||
| 9000 | format!("prettier ({})", dir_to_display.display()) | ||
| 9001 | } | ||
| 9002 | } | ||
| 9003 | None => format!("prettier ({})", prettier_dir.display()), | ||
| 9004 | }; | ||
| 9005 | LanguageServerName(Arc::from(name)) | ||
| 9006 | }; | ||
| 9007 | project | ||
| 9008 | .supplementary_language_servers | ||
| 9009 | .insert(new_server_id, (name, Arc::clone(prettier_server))); | ||
| 9010 | cx.emit(Event::LanguageServerAdded(new_server_id)); | ||
| 9011 | }) | ||
| 9012 | .ok(); | ||
| 9013 | } | ||
| 9014 | } | ||
| 9015 | |||
| 9016 | #[cfg(not(any(test, feature = "test-support")))] | ||
| 9017 | async fn install_default_prettier( | ||
| 9018 | plugins_to_install: HashSet<&'static str>, | ||
| 9019 | node: Arc<dyn NodeRuntime>, | ||
| 9020 | fs: Arc<dyn Fs>, | ||
| 9021 | ) -> anyhow::Result<()> { | ||
| 9022 | let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE); | ||
| 9023 | // method creates parent directory if it doesn't exist | ||
| 9024 | fs.save( | ||
| 9025 | &prettier_wrapper_path, | ||
| 9026 | &text::Rope::from(prettier::PRETTIER_SERVER_JS), | ||
| 9027 | text::LineEnding::Unix, | ||
| 9028 | ) | ||
| 9029 | .await | ||
| 9030 | .with_context(|| { | ||
| 9031 | format!( | ||
| 9032 | "writing {} file at {prettier_wrapper_path:?}", | ||
| 9033 | prettier::PRETTIER_SERVER_FILE | ||
| 9034 | ) | ||
| 9035 | })?; | ||
| 9036 | |||
| 9037 | let packages_to_versions = | ||
| 9038 | future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map( | ||
| 9039 | |package_name| async { | ||
| 9040 | let returned_package_name = package_name.to_string(); | ||
| 9041 | let latest_version = node | ||
| 9042 | .npm_package_latest_version(package_name) | ||
| 9043 | .await | ||
| 9044 | .with_context(|| { | ||
| 9045 | format!("fetching latest npm version for package {returned_package_name}") | ||
| 9046 | })?; | ||
| 9047 | anyhow::Ok((returned_package_name, latest_version)) | ||
| 9048 | }, | ||
| 9049 | )) | ||
| 9050 | .await | ||
| 9051 | .context("fetching latest npm versions")?; | ||
| 9052 | |||
| 9053 | log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); | ||
| 9054 | let borrowed_packages = packages_to_versions | ||
| 9055 | .iter() | ||
| 9056 | .map(|(package, version)| (package.as_str(), version.as_str())) | ||
| 9057 | .collect::<Vec<_>>(); | ||
| 9058 | node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages) | ||
| 9059 | .await | ||
| 9060 | .context("fetching formatter packages")?; | ||
| 9061 | anyhow::Ok(()) | ||
| 9062 | } | 8436 | } |
| 9063 | 8437 | ||
| 9064 | fn subscribe_for_copilot_events( | 8438 | fn subscribe_for_copilot_events( |
| @@ -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 langauge | 47 | // FIXME: Associate a type with the languages and have the file's langauge |
| 49 | // override these associations | 48 | // override these associations |
| 50 | maybe!({ | 49 | maybe!({ |
| 51 | let suffix = path.icon_suffix()?; | 50 | let suffix = path.icon_suffix()?; |
| 52 | 51 | ||
| 53 | this.suffixes | 52 | 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_TYPE | 64 | EXPANDED_DIRECTORY_TYPE |
| 69 | } else { | 65 | } else { |
| 70 | COLLAPSED_DIRECTORY_TYPE | 66 | COLLAPSED_DIRECTORY_TYPE |
| 71 | }; | 67 | }; |
| 72 | 68 | ||
| 73 | this.types | 69 | 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_TYPE | 78 | EXPANDED_CHEVRON_TYPE |
| 86 | } else { | 79 | } else { |
| 87 | COLLAPSED_CHEVRON_TYPE | 80 | COLLAPSED_CHEVRON_TYPE |
| 88 | }; | 81 | }; |
| 89 | 82 | ||
| 90 | this.types | 83 | 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 | } |
| @@ -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 | None | 1288 | 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 | }; |
| @@ -1,4 +1,4 @@ | |||
| 1 | use gpui::{IntoElement, MouseDownEvent, WindowContext}; | 1 | use gpui::{ClickEvent, IntoElement, WindowContext}; |
| 2 | use ui::{Button, ButtonVariant, IconButton}; | 2 | use ui::{Button, ButtonVariant, IconButton}; |
| 3 | 3 | ||
| 4 | use crate::mode::SearchMode; | 4 | use crate::mode::SearchMode; |
| @@ -6,7 +6,7 @@ use crate::mode::SearchMode; · 1 unmodified line | |||
| 6 | pub(super) fn render_nav_button( | 6 | pub(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 | |||
| 21 | pub(crate) fn render_search_mode_button( | 21 | pub(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::Filled | 27 | ButtonVariant::Filled |
| @@ -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 | }; |
| 5 | use theme2::ActiveTheme; | 5 | use ui::prelude::*; |
| 6 | 6 | ||
| 7 | actions!(ActionA, ActionB, ActionC); | 7 | actions!(ActionA, ActionB, ActionC); |
| 8 | 8 | ||
| @@ -4,7 +4,7 @@ use gpui::{ · 3 unmodified lines | |||
| 4 | }; | 4 | }; |
| 5 | use picker::{Picker, PickerDelegate}; | 5 | use picker::{Picker, PickerDelegate}; |
| 6 | use std::sync::Arc; | 6 | use std::sync::Arc; |
| 7 | use theme2::ActiveTheme; | 7 | use ui::prelude::*; |
| 8 | use ui::{Label, ListItem}; | 8 | use ui::{Label, ListItem}; |
| 9 | 9 | ||
| 10 | pub struct PickerStory { | 10 | pub struct PickerStory { |
| @@ -1,5 +1,5 @@ | |||
| 1 | use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; | 1 | use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; |
| 2 | use theme2::ActiveTheme; | 2 | use ui::prelude::*; |
| 3 | use ui::Tooltip; | 3 | use ui::Tooltip; |
| 4 | 4 | ||
| 5 | pub struct ScrollStory; | 5 | pub struct ScrollStory; |
| @@ -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(), |
| @@ -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 | } |
| @@ -0,0 +1,29 @@ | |||
| 1 | [package] | ||
| 2 | name = "theme_selector2" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2021" | ||
| 5 | publish = false | ||
| 6 | |||
| 7 | [lib] | ||
| 8 | path = "src/theme_selector.rs" | ||
| 9 | doctest = false | ||
| 10 | |||
| 11 | [dependencies] | ||
| 12 | editor = { package = "editor2", path = "../editor2" } | ||
| 13 | fuzzy = { package = "fuzzy2", path = "../fuzzy2" } | ||
| 14 | fs = { package = "fs2", path = "../fs2" } | ||
| 15 | gpui = { package = "gpui2", path = "../gpui2" } | ||
| 16 | ui = { package = "ui2", path = "../ui2" } | ||
| 17 | picker = { package = "picker2", path = "../picker2" } | ||
| 18 | theme = { package = "theme2", path = "../theme2" } | ||
| 19 | settings = { package = "settings2", path = "../settings2" } | ||
| 20 | feature_flags = { package = "feature_flags2", path = "../feature_flags2" } | ||
| 21 | workspace = { package = "workspace2", path = "../workspace2" } | ||
| 22 | util = { path = "../util" } | ||
| 23 | log.workspace = true | ||
| 24 | parking_lot.workspace = true | ||
| 25 | postage.workspace = true | ||
| 26 | smol.workspace = true | ||
| 27 | |||
| 28 | [dev-dependencies] | ||
| 29 | editor = { package = "editor2", path = "../editor2", features = ["test-support"] } | ||
| @@ -0,0 +1,276 @@ | |||
| 1 | use feature_flags::FeatureFlagAppExt; | ||
| 2 | use fs::Fs; | ||
| 3 | use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; | ||
| 4 | use gpui::{ | ||
| 5 | actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, | ||
| 6 | SharedString, View, ViewContext, VisualContext, WeakView, | ||
| 7 | }; | ||
| 8 | use picker::{Picker, PickerDelegate}; | ||
| 9 | use settings::{update_settings_file, SettingsStore}; | ||
| 10 | use std::sync::Arc; | ||
| 11 | use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings}; | ||
| 12 | use ui::ListItem; | ||
| 13 | use util::ResultExt; | ||
| 14 | use workspace::{ui::HighlightedLabel, Workspace}; | ||
| 15 | |||
| 16 | actions!(Toggle, Reload); | ||
| 17 | |||
| 18 | pub fn init(cx: &mut AppContext) { | ||
| 19 | cx.observe_new_views( | ||
| 20 | |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| { | ||
| 21 | workspace.register_action(toggle); | ||
| 22 | }, | ||
| 23 | ) | ||
| 24 | .detach(); | ||
| 25 | } | ||
| 26 | |||
| 27 | pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { | ||
| 28 | let fs = workspace.app_state().fs.clone(); | ||
| 29 | workspace.toggle_modal(cx, |cx| { | ||
| 30 | ThemeSelector::new( | ||
| 31 | ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx), | ||
| 32 | cx, | ||
| 33 | ) | ||
| 34 | }); | ||
| 35 | } | ||
| 36 | |||
| 37 | #[cfg(debug_assertions)] | ||
| 38 | pub fn reload(cx: &mut AppContext) { | ||
| 39 | let current_theme_name = cx.theme().name.clone(); | ||
| 40 | let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| { | ||
| 41 | registry.clear(); | ||
| 42 | registry.get(¤t_theme_name) | ||
| 43 | }); | ||
| 44 | match current_theme { | ||
| 45 | Ok(theme) => { | ||
| 46 | ThemeSelectorDelegate::set_theme(theme, cx); | ||
| 47 | log::info!("reloaded theme {}", current_theme_name); | ||
| 48 | } | ||
| 49 | Err(error) => { | ||
| 50 | log::error!("failed to load theme {}: {:?}", current_theme_name, error) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | pub struct ThemeSelector { | ||
| 56 | picker: View<Picker<ThemeSelectorDelegate>>, | ||
| 57 | } | ||
| 58 | |||
| 59 | impl EventEmitter<DismissEvent> for ThemeSelector {} | ||
| 60 | |||
| 61 | impl FocusableView for ThemeSelector { | ||
| 62 | fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { | ||
| 63 | self.picker.focus_handle(cx) | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | impl Render for ThemeSelector { | ||
| 68 | type Element = View<Picker<ThemeSelectorDelegate>>; | ||
| 69 | |||
| 70 | fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { | ||
| 71 | self.picker.clone() | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | impl ThemeSelector { | ||
| 76 | pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self { | ||
| 77 | let picker = cx.build_view(|cx| Picker::new(delegate, cx)); | ||
| 78 | Self { picker } | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | pub struct ThemeSelectorDelegate { | ||
| 83 | fs: Arc<dyn Fs>, | ||
| 84 | theme_names: Vec<SharedString>, | ||
| 85 | matches: Vec<StringMatch>, | ||
| 86 | original_theme: Arc<Theme>, | ||
| 87 | selection_completed: bool, | ||
| 88 | selected_index: usize, | ||
| 89 | view: WeakView<ThemeSelector>, | ||
| 90 | } | ||
| 91 | |||
| 92 | impl ThemeSelectorDelegate { | ||
| 93 | fn new( | ||
| 94 | weak_view: WeakView<ThemeSelector>, | ||
| 95 | fs: Arc<dyn Fs>, | ||
| 96 | cx: &mut ViewContext<ThemeSelector>, | ||
| 97 | ) -> Self { | ||
| 98 | let original_theme = cx.theme().clone(); | ||
| 99 | |||
| 100 | let staff_mode = cx.is_staff(); | ||
| 101 | let registry = cx.global::<Arc<ThemeRegistry>>(); | ||
| 102 | let theme_names = registry.list(staff_mode).collect::<Vec<_>>(); | ||
| 103 | //todo!(theme sorting) | ||
| 104 | // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); | ||
| 105 | let matches = theme_names | ||
| 106 | .iter() | ||
| 107 | .map(|meta| StringMatch { | ||
| 108 | candidate_id: 0, | ||
| 109 | score: 0.0, | ||
| 110 | positions: Default::default(), | ||
| 111 | string: meta.to_string(), | ||
| 112 | }) | ||
| 113 | .collect(); | ||
| 114 | let mut this = Self { | ||
| 115 | fs, | ||
| 116 | theme_names, | ||
| 117 | matches, | ||
| 118 | original_theme: original_theme.clone(), | ||
| 119 | selected_index: 0, | ||
| 120 | selection_completed: false, | ||
| 121 | view: weak_view, | ||
| 122 | }; | ||
| 123 | this.select_if_matching(&original_theme.name); | ||
| 124 | this | ||
| 125 | } | ||
| 126 | |||
| 127 | fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) { | ||
| 128 | if let Some(mat) = self.matches.get(self.selected_index) { | ||
| 129 | let registry = cx.global::<Arc<ThemeRegistry>>(); | ||
| 130 | match registry.get(&mat.string) { | ||
| 131 | Ok(theme) => { | ||
| 132 | Self::set_theme(theme, cx); | ||
| 133 | } | ||
| 134 | Err(error) => { | ||
| 135 | log::error!("error loading theme {}: {}", mat.string, error) | ||
| 136 | } | ||
| 137 | } | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | fn select_if_matching(&mut self, theme_name: &str) { | ||
| 142 | self.selected_index = self | ||
| 143 | .matches | ||
| 144 | .iter() | ||
| 145 | .position(|mat| mat.string == theme_name) | ||
| 146 | .unwrap_or(self.selected_index); | ||
| 147 | } | ||
| 148 | |||
| 149 | fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) { | ||
| 150 | cx.update_global(|store: &mut SettingsStore, cx| { | ||
| 151 | let mut theme_settings = store.get::<ThemeSettings>(None).clone(); | ||
| 152 | theme_settings.active_theme = theme; | ||
| 153 | store.override_global(theme_settings); | ||
| 154 | cx.refresh(); | ||
| 155 | }); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | impl PickerDelegate for ThemeSelectorDelegate { | ||
| 160 | type ListItem = ui::ListItem; | ||
| 161 | |||
| 162 | fn placeholder_text(&self) -> Arc<str> { | ||
| 163 | "Select Theme...".into() | ||
| 164 | } | ||
| 165 | |||
| 166 | fn match_count(&self) -> usize { | ||
| 167 | self.matches.len() | ||
| 168 | } | ||
| 169 | |||
| 170 | fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) { | ||
| 171 | self.selection_completed = true; | ||
| 172 | |||
| 173 | let theme_name = cx.theme().name.clone(); | ||
| 174 | update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| { | ||
| 175 | settings.theme = Some(theme_name.to_string()); | ||
| 176 | }); | ||
| 177 | |||
| 178 | self.view | ||
| 179 | .update(cx, |_, cx| { | ||
| 180 | cx.emit(DismissEvent); | ||
| 181 | }) | ||
| 182 | .ok(); | ||
| 183 | } | ||
| 184 | |||
| 185 | fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) { | ||
| 186 | if !self.selection_completed { | ||
| 187 | Self::set_theme(self.original_theme.clone(), cx); | ||
| 188 | self.selection_completed = true; | ||
| 189 | } | ||
| 190 | } | ||
| 191 | |||
| 192 | fn selected_index(&self) -> usize { | ||
| 193 | self.selected_index | ||
| 194 | } | ||
| 195 | |||
| 196 | fn set_selected_index( | ||
| 197 | &mut self, | ||
| 198 | ix: usize, | ||
| 199 | cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>, | ||
| 200 | ) { | ||
| 201 | self.selected_index = ix; | ||
| 202 | self.show_selected_theme(cx); | ||
| 203 | } | ||
| 204 | |||
| 205 | fn update_matches( | ||
| 206 | &mut self, | ||
| 207 | query: String, | ||
| 208 | cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>, | ||
| 209 | ) -> gpui::Task<()> { | ||
| 210 | let background = cx.background_executor().clone(); | ||
| 211 | let candidates = self | ||
| 212 | .theme_names | ||
| 213 | .iter() | ||
| 214 | .enumerate() | ||
| 215 | .map(|(id, meta)| StringMatchCandidate { | ||
| 216 | id, | ||
| 217 | char_bag: meta.as_ref().into(), | ||
| 218 | string: meta.to_string(), | ||
| 219 | }) | ||
| 220 | .collect::<Vec<_>>(); | ||
| 221 | |||
| 222 | cx.spawn(|this, mut cx| async move { | ||
| 223 | let matches = if query.is_empty() { | ||
| 224 | candidates | ||
| 225 | .into_iter() | ||
| 226 | .enumerate() | ||
| 227 | .map(|(index, candidate)| StringMatch { | ||
| 228 | candidate_id: index, | ||
| 229 | string: candidate.string, | ||
| 230 | positions: Vec::new(), | ||
| 231 | score: 0.0, | ||
| 232 | }) | ||
| 233 | .collect() | ||
| 234 | } else { | ||
| 235 | match_strings( | ||
| 236 | &candidates, | ||
| 237 | &query, | ||
| 238 | false, | ||
| 239 | 100, | ||
| 240 | &Default::default(), | ||
| 241 | background, | ||
| 242 | ) | ||
| 243 | .await | ||
| 244 | }; | ||
| 245 | |||
| 246 | this.update(&mut cx, |this, cx| { | ||
| 247 | this.delegate.matches = matches; | ||
| 248 | this.delegate.selected_index = this | ||
| 249 | .delegate | ||
| 250 | .selected_index | ||
| 251 | .min(this.delegate.matches.len().saturating_sub(1)); | ||
| 252 | this.delegate.show_selected_theme(cx); | ||
| 253 | }) | ||
| 254 | .log_err(); | ||
| 255 | }) | ||
| 256 | } | ||
| 257 | |||
| 258 | fn render_match( | ||
| 259 | &self, | ||
| 260 | ix: usize, | ||
| 261 | selected: bool, | ||
| 262 | _cx: &mut ViewContext<Picker<Self>>, | ||
| 263 | ) -> Option<Self::ListItem> { | ||
| 264 | let theme_match = &self.matches[ix]; | ||
| 265 | |||
| 266 | Some( | ||
| 267 | ListItem::new(ix) | ||
| 268 | .inset(true) | ||
| 269 | .selected(selected) | ||
| 270 | .child(HighlightedLabel::new( | ||
| 271 | theme_match.string.clone(), | ||
| 272 | theme_match.positions.clone(), | ||
| 273 | )), | ||
| 274 | ) | ||
| 275 | } | ||
| 276 | } | ||
| @@ -0,0 +1,5 @@ | |||
| 1 | use gpui::{ClickEvent, WindowContext}; | ||
| 2 | |||
| 3 | pub trait Clickable { | ||
| 4 | fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self; | ||
| 5 | } | ||
| @@ -1,12 +1,12 @@ | |||
| 1 | mod avatar; | 1 | mod avatar; |
| 2 | mod button; | 2 | mod button; |
| 3 | mod button2; | ||
| 3 | mod checkbox; | 4 | mod checkbox; |
| 4 | mod context_menu; | 5 | mod context_menu; |
| 5 | mod disclosure; | 6 | mod disclosure; |
| 6 | mod divider; | 7 | mod divider; |
| 7 | mod icon; | 8 | mod icon; |
| 8 | mod icon_button; | 9 | mod icon_button; |
| 9 | mod input; | ||
| 10 | mod keybinding; | 10 | mod keybinding; |
| 11 | mod label; | 11 | mod label; |
| 12 | mod list; | 12 | mod list; |
| @@ -21,13 +21,13 @@ mod stories; · 8 unmodified lines | |||
| 21 | 21 | ||
| 22 | pub use avatar::*; | 22 | pub use avatar::*; |
| 23 | pub use button::*; | 23 | pub use button::*; |
| 24 | pub use button2::*; | ||
| 24 | pub use checkbox::*; | 25 | pub use checkbox::*; |
| 25 | pub use context_menu::*; | 26 | pub use context_menu::*; |
| 26 | pub use disclosure::*; | 27 | pub use disclosure::*; |
| 27 | pub use divider::*; | 28 | pub use divider::*; |
| 28 | pub use icon::*; | 29 | pub use icon::*; |
| 29 | pub use icon_button::*; | 30 | pub use icon_button::*; |
| 30 | pub use input::*; | ||
| 31 | pub use keybinding::*; | 31 | pub use keybinding::*; |
| 32 | pub use label::*; | 32 | pub use label::*; |
| 33 | pub use list::*; | 33 | pub use list::*; |
| @@ -1,9 +1,7 @@ | |||
| 1 | use std::rc::Rc; | ||
| 2 | |||
| 3 | use gpui::{ | 1 | use gpui::{ |
| 4 | DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent, | 2 | ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext, |
| 5 | StatefulInteractiveElement, WindowContext, | ||
| 6 | }; | 3 | }; |
| 4 | use std::rc::Rc; | ||
| 7 | 5 | ||
| 8 | use crate::prelude::*; | 6 | use crate::prelude::*; |
| 9 | use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; | 7 | use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; |
| @@ -67,7 +65,7 @@ impl ButtonVariant { · 57 unmodified lines | |||
| 67 | #[derive(IntoElement)] | 65 | #[derive(IntoElement)] |
| 68 | pub struct Button { | 66 | pub 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 | self | 166 | 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 | self | 171 | self |
| 177 | } | 172 | } |
| @@ -0,0 +1,413 @@ | |||
| 1 | use gpui::{ | ||
| 2 | rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful, | ||
| 3 | StatefulInteractiveElement, WindowContext, | ||
| 4 | }; | ||
| 5 | use smallvec::SmallVec; | ||
| 6 | |||
| 7 | use crate::{h_stack, prelude::*}; | ||
| 8 | |||
| 9 | // 🚧 Heavily WIP 🚧 | ||
| 10 | |||
| 11 | // #[derive(Default, PartialEq, Clone, Copy)] | ||
| 12 | // pub enum ButtonType2 { | ||
| 13 | // #[default] | ||
| 14 | // DefaultButton, | ||
| 15 | // IconButton, | ||
| 16 | // ButtonLike, | ||
| 17 | // SplitButton, | ||
| 18 | // ToggleButton, | ||
| 19 | // } | ||
| 20 | |||
| 21 | #[derive(Default, PartialEq, Clone, Copy)] | ||
| 22 | pub enum IconPosition2 { | ||
| 23 | #[default] | ||
| 24 | Before, | ||
| 25 | After, | ||
| 26 | } | ||
| 27 | |||
| 28 | #[derive(Default, PartialEq, Clone, Copy)] | ||
| 29 | pub enum ButtonStyle2 { | ||
| 30 | #[default] | ||
| 31 | Filled, | ||
| 32 | // Tinted, | ||
| 33 | Subtle, | ||
| 34 | Transparent, | ||
| 35 | } | ||
| 36 | |||
| 37 | #[derive(Debug, Clone, Copy)] | ||
| 38 | pub struct ButtonStyle { | ||
| 39 | pub background: Hsla, | ||
| 40 | pub border_color: Hsla, | ||
| 41 | pub label_color: Hsla, | ||
| 42 | pub icon_color: Hsla, | ||
| 43 | } | ||
| 44 | |||
| 45 | impl ButtonStyle2 { | ||
| 46 | pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle { | ||
| 47 | match self { | ||
| 48 | ButtonStyle2::Filled => ButtonStyle { | ||
| 49 | background: cx.theme().colors().element_background, | ||
| 50 | border_color: gpui::transparent_black(), | ||
| 51 | label_color: Color::Default.color(cx), | ||
| 52 | icon_color: Color::Default.color(cx), | ||
| 53 | }, | ||
| 54 | ButtonStyle2::Subtle => ButtonStyle { | ||
| 55 | background: cx.theme().colors().ghost_element_background, | ||
| 56 | border_color: gpui::transparent_black(), | ||
| 57 | label_color: Color::Default.color(cx), | ||
| 58 | icon_color: Color::Default.color(cx), | ||
| 59 | }, | ||
| 60 | ButtonStyle2::Transparent => ButtonStyle { | ||
| 61 | background: gpui::transparent_black(), | ||
| 62 | border_color: gpui::transparent_black(), | ||
| 63 | label_color: Color::Default.color(cx), | ||
| 64 | icon_color: Color::Default.color(cx), | ||
| 65 | }, | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle { | ||
| 70 | match self { | ||
| 71 | ButtonStyle2::Filled => ButtonStyle { | ||
| 72 | background: cx.theme().colors().element_hover, | ||
| 73 | border_color: gpui::transparent_black(), | ||
| 74 | label_color: Color::Default.color(cx), | ||
| 75 | icon_color: Color::Default.color(cx), | ||
| 76 | }, | ||
| 77 | ButtonStyle2::Subtle => ButtonStyle { | ||
| 78 | background: cx.theme().colors().ghost_element_hover, | ||
| 79 | border_color: gpui::transparent_black(), | ||
| 80 | label_color: Color::Default.color(cx), | ||
| 81 | icon_color: Color::Default.color(cx), | ||
| 82 | }, | ||
| 83 | ButtonStyle2::Transparent => ButtonStyle { | ||
| 84 | background: gpui::transparent_black(), | ||
| 85 | border_color: gpui::transparent_black(), | ||
| 86 | // TODO: These are not great | ||
| 87 | label_color: Color::Muted.color(cx), | ||
| 88 | // TODO: These are not great | ||
| 89 | icon_color: Color::Muted.color(cx), | ||
| 90 | }, | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | pub fn active(self, cx: &mut WindowContext) -> ButtonStyle { | ||
| 95 | match self { | ||
| 96 | ButtonStyle2::Filled => ButtonStyle { | ||
| 97 | background: cx.theme().colors().element_active, | ||
| 98 | border_color: gpui::transparent_black(), | ||
| 99 | label_color: Color::Default.color(cx), | ||
| 100 | icon_color: Color::Default.color(cx), | ||
| 101 | }, | ||
| 102 | ButtonStyle2::Subtle => ButtonStyle { | ||
| 103 | background: cx.theme().colors().ghost_element_active, | ||
| 104 | border_color: gpui::transparent_black(), | ||
| 105 | label_color: Color::Default.color(cx), | ||
| 106 | icon_color: Color::Default.color(cx), | ||
| 107 | }, | ||
| 108 | ButtonStyle2::Transparent => ButtonStyle { | ||
| 109 | background: gpui::transparent_black(), | ||
| 110 | border_color: gpui::transparent_black(), | ||
| 111 | // TODO: These are not great | ||
| 112 | label_color: Color::Muted.color(cx), | ||
| 113 | // TODO: These are not great | ||
| 114 | icon_color: Color::Muted.color(cx), | ||
| 115 | }, | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle { | ||
| 120 | match self { | ||
| 121 | ButtonStyle2::Filled => ButtonStyle { | ||
| 122 | background: cx.theme().colors().element_background, | ||
| 123 | border_color: cx.theme().colors().border_focused, | ||
| 124 | label_color: Color::Default.color(cx), | ||
| 125 | icon_color: Color::Default.color(cx), | ||
| 126 | }, | ||
| 127 | ButtonStyle2::Subtle => ButtonStyle { | ||
| 128 | background: cx.theme().colors().ghost_element_background, | ||
| 129 | border_color: cx.theme().colors().border_focused, | ||
| 130 | label_color: Color::Default.color(cx), | ||
| 131 | icon_color: Color::Default.color(cx), | ||
| 132 | }, | ||
| 133 | ButtonStyle2::Transparent => ButtonStyle { | ||
| 134 | background: gpui::transparent_black(), | ||
| 135 | border_color: cx.theme().colors().border_focused, | ||
| 136 | label_color: Color::Accent.color(cx), | ||
| 137 | icon_color: Color::Accent.color(cx), | ||
| 138 | }, | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle { | ||
| 143 | match self { | ||
| 144 | ButtonStyle2::Filled => ButtonStyle { | ||
| 145 | background: cx.theme().colors().element_disabled, | ||
| 146 | border_color: cx.theme().colors().border_disabled, | ||
| 147 | label_color: Color::Disabled.color(cx), | ||
| 148 | icon_color: Color::Disabled.color(cx), | ||
| 149 | }, | ||
| 150 | ButtonStyle2::Subtle => ButtonStyle { | ||
| 151 | background: cx.theme().colors().ghost_element_disabled, | ||
| 152 | border_color: cx.theme().colors().border_disabled, | ||
| 153 | label_color: Color::Disabled.color(cx), | ||
| 154 | icon_color: Color::Disabled.color(cx), | ||
| 155 | }, | ||
| 156 | ButtonStyle2::Transparent => ButtonStyle { | ||
| 157 | background: gpui::transparent_black(), | ||
| 158 | border_color: gpui::transparent_black(), | ||
| 159 | label_color: Color::Disabled.color(cx), | ||
| 160 | icon_color: Color::Disabled.color(cx), | ||
| 161 | }, | ||
| 162 | } | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | #[derive(Default, PartialEq, Clone, Copy)] | ||
| 167 | pub enum ButtonSize2 { | ||
| 168 | #[default] | ||
| 169 | Default, | ||
| 170 | Compact, | ||
| 171 | None, | ||
| 172 | } | ||
| 173 | |||
| 174 | impl ButtonSize2 { | ||
| 175 | fn height(self) -> Rems { | ||
| 176 | match self { | ||
| 177 | ButtonSize2::Default => rems(22. / 16.), | ||
| 178 | ButtonSize2::Compact => rems(18. / 16.), | ||
| 179 | ButtonSize2::None => rems(16. / 16.), | ||
| 180 | } | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | // pub struct Button { | ||
| 185 | // id: ElementId, | ||
| 186 | // icon: Option<Icon>, | ||
| 187 | // icon_color: Option<Color>, | ||
| 188 | // icon_position: Option<IconPosition2>, | ||
| 189 | // label: Option<Label>, | ||
| 190 | // label_color: Option<Color>, | ||
| 191 | // appearance: ButtonAppearance2, | ||
| 192 | // state: InteractionState, | ||
| 193 | // selected: bool, | ||
| 194 | // disabled: bool, | ||
| 195 | // tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>, | ||
| 196 | // width: Option<DefiniteLength>, | ||
| 197 | // action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>, | ||
| 198 | // secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>, | ||
| 199 | // /// Used to pass down some content to the button | ||
| 200 | // /// to enable creating custom buttons. | ||
| 201 | // children: SmallVec<[AnyElement; 2]>, | ||
| 202 | // } | ||
| 203 | |||
| 204 | pub trait ButtonCommon: Clickable { | ||
| 205 | fn id(&self) -> &ElementId; | ||
| 206 | fn style(self, style: ButtonStyle2) -> Self; | ||
| 207 | fn disabled(self, disabled: bool) -> Self; | ||
| 208 | fn size(self, size: ButtonSize2) -> Self; | ||
| 209 | fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self; | ||
| 210 | // fn width(&mut self, width: DefiniteLength) -> &mut Self; | ||
| 211 | } | ||
| 212 | |||
| 213 | // pub struct LabelButton { | ||
| 214 | // // Base properties... | ||
| 215 | // id: ElementId, | ||
| 216 | // appearance: ButtonAppearance, | ||
| 217 | // state: InteractionState, | ||
| 218 | // disabled: bool, | ||
| 219 | // size: ButtonSize, | ||
| 220 | // tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>, | ||
| 221 | // width: Option<DefiniteLength>, | ||
| 222 | // // Button-specific properties... | ||
| 223 | // label: Option<SharedString>, | ||
| 224 | // label_color: Option<Color>, | ||
| 225 | // icon: Option<Icon>, | ||
| 226 | // icon_color: Option<Color>, | ||
| 227 | // icon_position: Option<IconPosition>, | ||
| 228 | // // Define more fields for additional properties as needed | ||
| 229 | // } | ||
| 230 | |||
| 231 | // impl ButtonCommon for LabelButton { | ||
| 232 | // fn id(&self) -> &ElementId { | ||
| 233 | // &self.id | ||
| 234 | // } | ||
| 235 | |||
| 236 | // fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self { | ||
| 237 | // self.style= style; | ||
| 238 | // self | ||
| 239 | // } | ||
| 240 | // // implement methods from ButtonCommon trait... | ||
| 241 | // } | ||
| 242 | |||
| 243 | // impl LabelButton { | ||
| 244 | // pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self { | ||
| 245 | // Self { | ||
| 246 | // id: id.into(), | ||
| 247 | // label: Some(label.into()), | ||
| 248 | // // initialize other fields with default values... | ||
| 249 | // } | ||
| 250 | // } | ||
| 251 | |||
| 252 | // // ... Define other builder methods specific to Button type... | ||
| 253 | // } | ||
| 254 | |||
| 255 | // TODO: Icon Button | ||
| 256 | |||
| 257 | #[derive(IntoElement)] | ||
| 258 | pub struct ButtonLike { | ||
| 259 | id: ElementId, | ||
| 260 | style: ButtonStyle2, | ||
| 261 | disabled: bool, | ||
| 262 | size: ButtonSize2, | ||
| 263 | tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>, | ||
| 264 | on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, | ||
| 265 | children: SmallVec<[AnyElement; 2]>, | ||
| 266 | } | ||
| 267 | |||
| 268 | impl ButtonLike { | ||
| 269 | pub fn children( | ||
| 270 | &mut self, | ||
| 271 | children: impl IntoIterator<Item = impl Into<AnyElement>>, | ||
| 272 | ) -> &mut Self { | ||
| 273 | self.children = children.into_iter().map(Into::into).collect(); | ||
| 274 | self | ||
| 275 | } | ||
| 276 | |||
| 277 | pub fn new(id: impl Into<ElementId>) -> Self { | ||
| 278 | Self { | ||
| 279 | id: id.into(), | ||
| 280 | style: ButtonStyle2::default(), | ||
| 281 | disabled: false, | ||
| 282 | size: ButtonSize2::Default, | ||
| 283 | tooltip: None, | ||
| 284 | children: SmallVec::new(), | ||
| 285 | on_click: None, | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | impl Clickable for ButtonLike { | ||
| 291 | fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { | ||
| 292 | self.on_click = Some(Box::new(handler)); | ||
| 293 | self | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | // impl Selectable for ButtonLike { | ||
| 298 | // fn selected(&mut self, selected: bool) -> &mut Self { | ||
| 299 | // todo!() | ||
| 300 | // } | ||
| 301 | |||
| 302 | // fn selected_tooltip( | ||
| 303 | // &mut self, | ||
| 304 | // tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>, | ||
| 305 | // ) -> &mut Self { | ||
| 306 | // todo!() | ||
| 307 | // } | ||
| 308 | // } | ||
| 309 | |||
| 310 | impl ButtonCommon for ButtonLike { | ||
| 311 | fn id(&self) -> &ElementId { | ||
| 312 | &self.id | ||
| 313 | } | ||
| 314 | |||
| 315 | fn style(mut self, style: ButtonStyle2) -> Self { | ||
| 316 | self.style = style; | ||
| 317 | self | ||
| 318 | } | ||
| 319 | |||
| 320 | fn disabled(mut self, disabled: bool) -> Self { | ||
| 321 | self.disabled = disabled; | ||
| 322 | self | ||
| 323 | } | ||
| 324 | |||
| 325 | fn size(mut self, size: ButtonSize2) -> Self { | ||
| 326 | self.size = size; | ||
| 327 | self | ||
| 328 | } | ||
| 329 | |||
| 330 | fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { | ||
| 331 | self.tooltip = Some(Box::new(tooltip)); | ||
| 332 | self | ||
| 333 | } | ||
| 334 | } | ||
| 335 | |||
| 336 | impl RenderOnce for ButtonLike { | ||
| 337 | type Rendered = Stateful<Div>; | ||
| 338 | |||
| 339 | fn render(self, cx: &mut WindowContext) -> Self::Rendered { | ||
| 340 | h_stack() | ||
| 341 | .id(self.id.clone()) | ||
| 342 | .h(self.size.height()) | ||
| 343 | .rounded_md() | ||
| 344 | .cursor_pointer() | ||
| 345 | .gap_1() | ||
| 346 | .px_1() | ||
| 347 | .bg(self.style.enabled(cx).background) | ||
| 348 | .hover(|hover| hover.bg(self.style.hovered(cx).background)) | ||
| 349 | .active(|active| active.bg(self.style.active(cx).background)) | ||
| 350 | .when_some( | ||
| 351 | self.on_click.filter(|_| !self.disabled), | ||
| 352 | |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)), | ||
| 353 | ) | ||
| 354 | .when_some(self.tooltip, |this, tooltip| { | ||
| 355 | this.tooltip(move |cx| tooltip(cx)) | ||
| 356 | }) | ||
| 357 | .children(self.children) | ||
| 358 | } | ||
| 359 | } | ||
| 360 | |||
| 361 | impl ParentElement for ButtonLike { | ||
| 362 | fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { | ||
| 363 | &mut self.children | ||
| 364 | } | ||
| 365 | } | ||
| 366 | |||
| 367 | // pub struct ToggleButton { | ||
| 368 | // // based on either IconButton2 or Button, with additional 'selected: bool' property | ||
| 369 | // } | ||
| 370 | |||
| 371 | // impl ButtonCommon for ToggleButton { | ||
| 372 | // fn id(&self) -> &ElementId { | ||
| 373 | // &self.id | ||
| 374 | // } | ||
| 375 | // // ... Implement other methods from ButtonCommon trait with builder patterns... | ||
| 376 | // } | ||
| 377 | |||
| 378 | // impl ToggleButton { | ||
| 379 | // pub fn new() -> Self { | ||
| 380 | // // Initialize with default values | ||
| 381 | // Self { | ||
| 382 | // // ... initialize fields, possibly with defaults or required parameters... | ||
| 383 | // } | ||
| 384 | // } | ||
| 385 | |||
| 386 | // // ... Define other builder methods specific to ToggleButton type... | ||
| 387 | // } | ||
| 388 | |||
| 389 | // pub struct SplitButton { | ||
| 390 | // // Base properties... | ||
| 391 | // id: ElementId, | ||
| 392 | // // Button-specific properties, possibly including a DefaultButton | ||
| 393 | // secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>, | ||
| 394 | // // More fields as necessary... | ||
| 395 | // } | ||
| 396 | |||
| 397 | // impl ButtonCommon for SplitButton { | ||
| 398 | // fn id(&self) -> &ElementId { | ||
| 399 | // &self.id | ||
| 400 | // } | ||
| 401 | // // ... Implement other methods from ButtonCommon trait with builder patterns... | ||
| 402 | // } | ||
| 403 | |||
| 404 | // impl SplitButton { | ||
| 405 | // pub fn new(id: impl Into<ElementId>) -> Self { | ||
| 406 | // Self { | ||
| 407 | // id: id.into(), | ||
| 408 | // // ... initialize other fields with default values... | ||
| 409 | // } | ||
| 410 | // } | ||
| 411 | |||
| 412 | // // ... Define other builder methods specific to SplitButton type... | ||
| 413 | // } | ||
| @@ -1,19 +1,30 @@ | |||
| 1 | use gpui::{div, Element, ParentElement}; | 1 | use std::rc::Rc; |
| 2 | 2 | ||
| 3 | use crate::{Color, Icon, IconElement, IconSize, Toggle}; | 3 | use gpui::{div, ClickEvent, Element, IntoElement, ParentElement, WindowContext}; |
| 4 | 4 | ||
| 5 | pub fn disclosure_control(toggle: Toggle) -> impl Element { | 5 | use crate::{Color, Icon, IconButton, IconSize, Toggle}; |
| 6 | |||
| 7 | pub fn disclosure_control( | ||
| 8 | toggle: Toggle, | ||
| 9 | on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, | ||
| 10 | ) -> impl Element { | ||
| 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 | } |
| @@ -1,25 +1,26 @@ | |||
| 1 | use crate::{h_stack, prelude::*, Icon, IconElement}; | 1 | use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; |
| 2 | use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful}; | 2 | use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful}; |
| 3 | 3 | ||
| 4 | #[derive(IntoElement)] | 4 | #[derive(IntoElement)] |
| 5 | pub struct IconButton { | 5 | pub 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 | ||
| 16 | impl RenderOnce for IconButton { | 17 | impl 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 | self | 99 | 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 | self | 104 | 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 | self | 109 | self |
| 105 | } | 110 | } |
| 106 | 111 | ||
| @@ -109,16 +114,18 @@ impl IconButton { · 2 unmodified lines | |||
| 109 | self | 114 | 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 | self | 124 | 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 | self | 129 | self |
| 123 | } | 130 | } |
| 124 | 131 | ||
| @@ -1,108 +0,0 @@ | |||
| 1 | use crate::{prelude::*, Label}; | ||
| 2 | use gpui::{prelude::*, Div, IntoElement, Stateful}; | ||
| 3 | |||
| 4 | #[derive(Default, PartialEq)] | ||
| 5 | pub enum InputVariant { | ||
| 6 | #[default] | ||
| 7 | Ghost, | ||
| 8 | Filled, | ||
| 9 | } | ||
| 10 | |||
| 11 | #[derive(IntoElement)] | ||
| 12 | pub struct Input { | ||
| 13 | placeholder: SharedString, | ||
| 14 | value: String, | ||
| 15 | state: InteractionState, | ||
| 16 | variant: InputVariant, | ||
| 17 | disabled: bool, | ||
| 18 | is_active: bool, | ||
| 19 | } | ||
| 20 | |||
| 21 | impl RenderOnce for Input { | ||
| 22 | type Rendered = Stateful<Div>; | ||
| 23 | |||
| 24 | fn render(self, cx: &mut WindowContext) -> Self::Rendered { | ||
| 25 | let (input_bg, input_hover_bg, input_active_bg) = match self.variant { | ||
| 26 | InputVariant::Ghost => ( | ||
| 27 | cx.theme().colors().ghost_element_background, | ||
| 28 | cx.theme().colors().ghost_element_hover, | ||
| 29 | cx.theme().colors().ghost_element_active, | ||
| 30 | ), | ||
| 31 | InputVariant::Filled => ( | ||
| 32 | cx.theme().colors().element_background, | ||
| 33 | cx.theme().colors().element_hover, | ||
| 34 | cx.theme().colors().element_active, | ||
| 35 | ), | ||
| 36 | }; | ||
| 37 | |||
| 38 | let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled { | ||
| 39 | Color::Disabled | ||
| 40 | } else { | ||
| 41 | Color::Placeholder | ||
| 42 | }); | ||
| 43 | |||
| 44 | let label = Label::new(self.value.clone()).color(if self.disabled { | ||
| 45 | Color::Disabled | ||
| 46 | } else { | ||
| 47 | Color::Default | ||
| 48 | }); | ||
| 49 | |||
| 50 | div() | ||
| 51 | .id("input") | ||
| 52 | .h_7() | ||
| 53 | .w_full() | ||
| 54 | .px_2() | ||
| 55 | .border() | ||
| 56 | .border_color(cx.theme().styles.system.transparent) | ||
| 57 | .bg(input_bg) | ||
| 58 | .hover(|style| style.bg(input_hover_bg)) | ||
| 59 | .active(|style| style.bg(input_active_bg)) | ||
| 60 | .flex() | ||
| 61 | .items_center() | ||
| 62 | .child(div().flex().items_center().text_ui_sm().map(move |this| { | ||
| 63 | if self.value.is_empty() { | ||
| 64 | this.child(placeholder_label) | ||
| 65 | } else { | ||
| 66 | this.child(label) | ||
| 67 | } | ||
| 68 | })) | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | impl Input { | ||
| 73 | pub fn new(placeholder: impl Into<SharedString>) -> Self { | ||
| 74 | Self { | ||
| 75 | placeholder: placeholder.into(), | ||
| 76 | value: "".to_string(), | ||
| 77 | state: InteractionState::default(), | ||
| 78 | variant: InputVariant::default(), | ||
| 79 | disabled: false, | ||
| 80 | is_active: false, | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn value(mut self, value: String) -> Self { | ||
| 85 | self.value = value; | ||
| 86 | self | ||
| 87 | } | ||
| 88 | |||
| 89 | pub fn state(mut self, state: InteractionState) -> Self { | ||
| 90 | self.state = state; | ||
| 91 | self | ||
| 92 | } | ||
| 93 | |||
| 94 | pub fn variant(mut self, variant: InputVariant) -> Self { | ||
| 95 | self.variant = variant; | ||
| 96 | self | ||
| 97 | } | ||
| 98 | |||
| 99 | pub fn disabled(mut self, disabled: bool) -> Self { | ||
| 100 | self.disabled = disabled; | ||
| 101 | self | ||
| 102 | } | ||
| 103 | |||
| 104 | pub fn is_active(mut self, is_active: bool) -> Self { | ||
| 105 | self.is_active = is_active; | ||
| 106 | self | ||
| 107 | } | ||
| 108 | } | ||
| @@ -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 | ||
| 31 | impl ListHeader { | 33 | impl 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 | self | 48 | 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 | self | 61 | self |
| @@ -57,13 +69,18 @@ impl ListHeader { · 7 unmodified lines | |||
| 57 | self.meta = meta; | 69 | self.meta = meta; |
| 58 | self | 70 | self |
| 59 | } | 71 | } |
| 72 | |||
| 73 | pub fn selected(mut self, selected: bool) -> Self { | ||
| 74 | self.selected = selected; | ||
| 75 | self | ||
| 76 | } | ||
| 60 | } | 77 | } |
| 61 | 78 | ||
| 62 | impl RenderOnce for ListHeader { | 79 | impl 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 | self | 252 | 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 | self | 265 | 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 click | 327 | 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 the | 329 | .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 | } |
| @@ -4,18 +4,15 @@ mod checkbox; · 3 unmodified lines | |||
| 4 | mod context_menu; | 4 | mod context_menu; |
| 5 | mod icon; | 5 | mod icon; |
| 6 | mod icon_button; | 6 | mod icon_button; |
| 7 | mod input; | ||
| 8 | mod keybinding; | 7 | mod keybinding; |
| 9 | mod label; | 8 | mod label; |
| 10 | mod list_item; | 9 | mod list_item; |
| 11 | |||
| 12 | pub use avatar::*; | 10 | pub use avatar::*; |
| 13 | pub use button::*; | 11 | pub use button::*; |
| 14 | pub use checkbox::*; | 12 | pub use checkbox::*; |
| 15 | pub use context_menu::*; | 13 | pub use context_menu::*; |
| 16 | pub use icon::*; | 14 | pub use icon::*; |
| 17 | pub use icon_button::*; | 15 | pub use icon_button::*; |
| 18 | pub use input::*; | ||
| 19 | pub use keybinding::*; | 16 | pub use keybinding::*; |
| 20 | pub use label::*; | 17 | pub use label::*; |
| 21 | pub use list_item::*; | 18 | pub use list_item::*; |
| @@ -1,9 +1,8 @@ | |||
| 1 | use gpui::{rems, Div, Render}; | 1 | use gpui::{Div, Render}; |
| 2 | use story::Story; | 2 | use story::Story; |
| 3 | use strum::IntoEnumIterator; | ||
| 4 | 3 | ||
| 5 | use crate::prelude::*; | 4 | use crate::prelude::*; |
| 6 | use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label}; | 5 | use crate::{h_stack, Button, Icon, IconPosition}; |
| 7 | 6 | ||
| 8 | pub struct ButtonStory; | 7 | pub 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") |
| @@ -1,18 +0,0 @@ | |||
| 1 | use gpui::{Div, Render}; | ||
| 2 | use story::Story; | ||
| 3 | |||
| 4 | use crate::prelude::*; | ||
| 5 | use crate::Input; | ||
| 6 | |||
| 7 | pub struct InputStory; | ||
| 8 | |||
| 9 | impl Render for InputStory { | ||
| 10 | type Element = Div; | ||
| 11 | |||
| 12 | fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { | ||
| 13 | Story::container() | ||
| 14 | .child(Story::title_for::<Input>()) | ||
| 15 | .child(Story::label("Default")) | ||
| 16 | .child(div().flex().child(Input::new("Search"))) | ||
| 17 | } | ||
| 18 | } | ||
| @@ -2,7 +2,7 @@ use gpui::{Div, Render}; · 1 unmodified line | |||
| 2 | use story::Story; | 2 | use story::Story; |
| 3 | 3 | ||
| 4 | use crate::prelude::*; | 4 | use crate::prelude::*; |
| 5 | use crate::ListItem; | 5 | use crate::{Icon, ListItem}; |
| 6 | 6 | ||
| 7 | pub struct ListItemStory; | 7 | pub 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 | } |
| @@ -0,0 +1,6 @@ | |||
| 1 | use gpui::DefiniteLength; | ||
| 2 | |||
| 3 | pub trait FixedWidth { | ||
| 4 | fn width(self, width: DefiniteLength) -> Self; | ||
| 5 | fn full_width(self) -> Self; | ||
| 6 | } | ||
| @@ -3,62 +3,9 @@ pub use gpui::{ · 2 unmodified lines | |||
| 3 | ViewContext, WindowContext, | 3 | ViewContext, WindowContext, |
| 4 | }; | 4 | }; |
| 5 | 5 | ||
| 6 | pub use crate::clickable::*; | ||
| 7 | pub use crate::fixed::*; | ||
| 8 | pub use crate::selectable::*; | ||
| 6 | pub use crate::StyledExt; | 9 | pub use crate::StyledExt; |
| 7 | pub use crate::{ButtonVariant, Color}; | 10 | pub use crate::{ButtonVariant, Color}; |
| 8 | pub use theme::ActiveTheme; | 11 | pub use theme::ActiveTheme; |
| 9 | |||
| 10 | use strum::EnumIter; | ||
| 11 | |||
| 12 | #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] | ||
| 13 | pub enum IconSide { | ||
| 14 | #[default] | ||
| 15 | Left, | ||
| 16 | Right, | ||
| 17 | } | ||
| 18 | |||
| 19 | #[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)] | ||
| 20 | pub enum InteractionState { | ||
| 21 | /// An element that is enabled and not hovered, active, focused, or disabled. | ||
| 22 | /// | ||
| 23 | /// This is often referred to as the "default" state. | ||
| 24 | #[default] | ||
| 25 | Enabled, | ||
| 26 | /// An element that is hovered. | ||
| 27 | Hovered, | ||
| 28 | /// An element has an active mouse down or touch start event on it. | ||
| 29 | Active, | ||
| 30 | /// An element that is focused using the keyboard. | ||
| 31 | Focused, | ||
| 32 | /// An element that is disabled. | ||
| 33 | Disabled, | ||
| 34 | /// A toggleable element that is selected, like the active button in a | ||
| 35 | /// button toggle group. | ||
| 36 | Selected, | ||
| 37 | } | ||
| 38 | |||
| 39 | impl InteractionState { | ||
| 40 | pub fn if_enabled(&self, enabled: bool) -> Self { | ||
| 41 | if enabled { | ||
| 42 | *self | ||
| 43 | } else { | ||
| 44 | InteractionState::Disabled | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] | ||
| 50 | pub enum Selection { | ||
| 51 | #[default] | ||
| 52 | Unselected, | ||
| 53 | Indeterminate, | ||
| 54 | Selected, | ||
| 55 | } | ||
| 56 | |||
| 57 | impl Selection { | ||
| 58 | pub fn inverse(&self) -> Self { | ||
| 59 | match self { | ||
| 60 | Self::Unselected | Self::Indeterminate => Self::Selected, | ||
| 61 | Self::Selected => Self::Unselected, | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | use gpui::{AnyView, WindowContext}; | ||
| 2 | |||
| 3 | pub trait Selectable { | ||
| 4 | fn selected(self, selected: bool) -> Self; | ||
| 5 | fn selected_tooltip( | ||
| 6 | self, | ||
| 7 | tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>, | ||
| 8 | ) -> Self; | ||
| 9 | } | ||
| 10 | |||
| 11 | #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] | ||
| 12 | pub enum Selection { | ||
| 13 | #[default] | ||
| 14 | Unselected, | ||
| 15 | Indeterminate, | ||
| 16 | Selected, | ||
| 17 | } | ||
| 18 | |||
| 19 | impl Selection { | ||
| 20 | pub fn inverse(&self) -> Self { | ||
| 21 | match self { | ||
| 22 | Self::Unselected | Self::Indeterminate => Self::Selected, | ||
| 23 | Self::Selected => Self::Unselected, | ||
| 24 | } | ||
| 25 | } | ||
| 26 | } | ||
| @@ -1,7 +1,7 @@ | |||
| 1 | use gpui::{Hsla, WindowContext}; | 1 | use gpui::{Hsla, WindowContext}; |
| 2 | use theme::ActiveTheme; | 2 | use theme::ActiveTheme; |
| 3 | 3 | ||
| 4 | #[derive(Default, PartialEq, Copy, Clone)] | 4 | #[derive(Debug, Default, PartialEq, Copy, Clone)] |
| 5 | pub enum Color { | 5 | pub enum Color { |
| 6 | #[default] | 6 | #[default] |
| 7 | Default, | 7 | Default, |
| @@ -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 | ||
| 15 | mod clickable; | ||
| 15 | mod components; | 16 | mod components; |
| 17 | mod fixed; | ||
| 16 | pub mod prelude; | 18 | pub mod prelude; |
| 19 | mod selectable; | ||
| 17 | mod styled_ext; | 20 | mod styled_ext; |
| 18 | mod styles; | 21 | mod styles; |
| 19 | pub mod utils; | 22 | pub mod utils; |
| 20 | 23 | ||
| 24 | pub use clickable::*; | ||
| 21 | pub use components::*; | 25 | pub use components::*; |
| 26 | pub use fixed::*; | ||
| 22 | pub use prelude::*; | 27 | pub use prelude::*; |
| 28 | pub use selectable::*; | ||
| 23 | pub use styled_ext::*; | 29 | pub use styled_ext::*; |
| 24 | pub use styles::*; | 30 | pub use styles::*; |
| @@ -0,0 +1,37 @@ | |||
| 1 | [package] | ||
| 2 | name = "welcome2" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2021" | ||
| 5 | publish = false | ||
| 6 | |||
| 7 | [lib] | ||
| 8 | path = "src/welcome.rs" | ||
| 9 | |||
| 10 | [features] | ||
| 11 | test-support = [] | ||
| 12 | |||
| 13 | [dependencies] | ||
| 14 | client = { package = "client2", path = "../client2" } | ||
| 15 | editor = { package = "editor2", path = "../editor2" } | ||
| 16 | fs = { package = "fs2", path = "../fs2" } | ||
| 17 | fuzzy = { package = "fuzzy2", path = "../fuzzy2" } | ||
| 18 | gpui = { package = "gpui2", path = "../gpui2" } | ||
| 19 | ui = { package = "ui2", path = "../ui2" } | ||
| 20 | db = { package = "db2", path = "../db2" } | ||
| 21 | install_cli = { package = "install_cli2", path = "../install_cli2" } | ||
| 22 | project = { package = "project2", path = "../project2" } | ||
| 23 | settings = { package = "settings2", path = "../settings2" } | ||
| 24 | theme = { package = "theme2", path = "../theme2" } | ||
| 25 | theme_selector = { package = "theme_selector2", path = "../theme_selector2" } | ||
| 26 | util = { path = "../util" } | ||
| 27 | picker = { package = "picker2", path = "../picker2" } | ||
| 28 | workspace = { package = "workspace2", path = "../workspace2" } | ||
| 29 | # vim = { package = "vim2", path = "../vim2" } | ||
| 30 | |||
| 31 | anyhow.workspace = true | ||
| 32 | log.workspace = true | ||
| 33 | schemars.workspace = true | ||
| 34 | serde.workspace = true | ||
| 35 | |||
| 36 | [dev-dependencies] | ||
| 37 | editor = { package = "editor2", path = "../editor2", features = ["test-support"] } | ||
| @@ -0,0 +1,208 @@ | |||
| 1 | use super::base_keymap_setting::BaseKeymap; | ||
| 2 | use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; | ||
| 3 | use gpui::{ | ||
| 4 | actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task, | ||
| 5 | View, ViewContext, VisualContext, WeakView, | ||
| 6 | }; | ||
| 7 | use picker::{Picker, PickerDelegate}; | ||
| 8 | use project::Fs; | ||
| 9 | use settings::{update_settings_file, Settings}; | ||
| 10 | use std::sync::Arc; | ||
| 11 | use ui::ListItem; | ||
| 12 | use util::ResultExt; | ||
| 13 | use workspace::{ui::HighlightedLabel, Workspace}; | ||
| 14 | |||
| 15 | actions!(ToggleBaseKeymapSelector); | ||
| 16 | |||
| 17 | pub fn init(cx: &mut AppContext) { | ||
| 18 | cx.observe_new_views(|workspace: &mut Workspace, _cx| { | ||
| 19 | workspace.register_action(toggle); | ||
| 20 | }) | ||
| 21 | .detach(); | ||
| 22 | } | ||
| 23 | |||
| 24 | pub fn toggle( | ||
| 25 | workspace: &mut Workspace, | ||
| 26 | _: &ToggleBaseKeymapSelector, | ||
| 27 | cx: &mut ViewContext<Workspace>, | ||
| 28 | ) { | ||
| 29 | let fs = workspace.app_state().fs.clone(); | ||
| 30 | workspace.toggle_modal(cx, |cx| { | ||
| 31 | BaseKeymapSelector::new( | ||
| 32 | BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx), | ||
| 33 | cx, | ||
| 34 | ) | ||
| 35 | }); | ||
| 36 | } | ||
| 37 | |||
| 38 | pub struct BaseKeymapSelector { | ||
| 39 | focus_handle: gpui::FocusHandle, | ||
| 40 | picker: View<Picker<BaseKeymapSelectorDelegate>>, | ||
| 41 | } | ||
| 42 | |||
| 43 | impl FocusableView for BaseKeymapSelector { | ||
| 44 | fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { | ||
| 45 | self.focus_handle.clone() | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | impl EventEmitter<DismissEvent> for BaseKeymapSelector {} | ||
| 50 | |||
| 51 | impl BaseKeymapSelector { | ||
| 52 | pub fn new( | ||
| 53 | delegate: BaseKeymapSelectorDelegate, | ||
| 54 | cx: &mut ViewContext<BaseKeymapSelector>, | ||
| 55 | ) -> Self { | ||
| 56 | let picker = cx.build_view(|cx| Picker::new(delegate, cx)); | ||
| 57 | let focus_handle = cx.focus_handle(); | ||
| 58 | Self { | ||
| 59 | focus_handle, | ||
| 60 | picker, | ||
| 61 | } | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | impl Render for BaseKeymapSelector { | ||
| 66 | type Element = View<Picker<BaseKeymapSelectorDelegate>>; | ||
| 67 | |||
| 68 | fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { | ||
| 69 | self.picker.clone() | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | pub struct BaseKeymapSelectorDelegate { | ||
| 74 | view: WeakView<BaseKeymapSelector>, | ||
| 75 | matches: Vec<StringMatch>, | ||
| 76 | selected_index: usize, | ||
| 77 | fs: Arc<dyn Fs>, | ||
| 78 | } | ||
| 79 | |||
| 80 | impl BaseKeymapSelectorDelegate { | ||
| 81 | fn new( | ||
| 82 | weak_view: WeakView<BaseKeymapSelector>, | ||
| 83 | fs: Arc<dyn Fs>, | ||
| 84 | cx: &mut ViewContext<BaseKeymapSelector>, | ||
| 85 | ) -> Self { | ||
| 86 | let base = BaseKeymap::get(None, cx); | ||
| 87 | let selected_index = BaseKeymap::OPTIONS | ||
| 88 | .iter() | ||
| 89 | .position(|(_, value)| value == base) | ||
| 90 | .unwrap_or(0); | ||
| 91 | Self { | ||
| 92 | view: weak_view, | ||
| 93 | matches: Vec::new(), | ||
| 94 | selected_index, | ||
| 95 | fs, | ||
| 96 | } | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | impl PickerDelegate for BaseKeymapSelectorDelegate { | ||
| 101 | type ListItem = ui::ListItem; | ||
| 102 | |||
| 103 | fn placeholder_text(&self) -> Arc<str> { | ||
| 104 | "Select a base keymap...".into() | ||
| 105 | } | ||
| 106 | |||
| 107 | fn match_count(&self) -> usize { | ||
| 108 | self.matches.len() | ||
| 109 | } | ||
| 110 | |||
| 111 | fn selected_index(&self) -> usize { | ||
| 112 | self.selected_index | ||
| 113 | } | ||
| 114 | |||
| 115 | fn set_selected_index( | ||
| 116 | &mut self, | ||
| 117 | ix: usize, | ||
| 118 | _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>, | ||
| 119 | ) { | ||
| 120 | self.selected_index = ix; | ||
| 121 | } | ||
| 122 | |||
| 123 | fn update_matches( | ||
| 124 | &mut self, | ||
| 125 | query: String, | ||
| 126 | cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>, | ||
| 127 | ) -> Task<()> { | ||
| 128 | let background = cx.background_executor().clone(); | ||
| 129 | let candidates = BaseKeymap::names() | ||
| 130 | .enumerate() | ||
| 131 | .map(|(id, name)| StringMatchCandidate { | ||
| 132 | id, | ||
| 133 | char_bag: name.into(), | ||
| 134 | string: name.into(), | ||
| 135 | }) | ||
| 136 | .collect::<Vec<_>>(); | ||
| 137 | |||
| 138 | cx.spawn(|this, mut cx| async move { | ||
| 139 | let matches = if query.is_empty() { | ||
| 140 | candidates | ||
| 141 | .into_iter() | ||
| 142 | .enumerate() | ||
| 143 | .map(|(index, candidate)| StringMatch { | ||
| 144 | candidate_id: index, | ||
| 145 | string: candidate.string, | ||
| 146 | positions: Vec::new(), | ||
| 147 | score: 0.0, | ||
| 148 | }) | ||
| 149 | .collect() | ||
| 150 | } else { | ||
| 151 | match_strings( | ||
| 152 | &candidates, | ||
| 153 | &query, | ||
| 154 | false, | ||
| 155 | 100, | ||
| 156 | &Default::default(), | ||
| 157 | background, | ||
| 158 | ) | ||
| 159 | .await | ||
| 160 | }; | ||
| 161 | |||
| 162 | this.update(&mut cx, |this, _| { | ||
| 163 | this.delegate.matches = matches; | ||
| 164 | this.delegate.selected_index = this | ||
| 165 | .delegate | ||
| 166 | .selected_index | ||
| 167 | .min(this.delegate.matches.len().saturating_sub(1)); | ||
| 168 | }) | ||
| 169 | .log_err(); | ||
| 170 | }) | ||
| 171 | } | ||
| 172 | |||
| 173 | fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) { | ||
| 174 | if let Some(selection) = self.matches.get(self.selected_index) { | ||
| 175 | let base_keymap = BaseKeymap::from_names(&selection.string); | ||
| 176 | update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| { | ||
| 177 | *setting = Some(base_keymap) | ||
| 178 | }); | ||
| 179 | } | ||
| 180 | |||
| 181 | self.view | ||
| 182 | .update(cx, |_, cx| { | ||
| 183 | cx.emit(DismissEvent); | ||
| 184 | }) | ||
| 185 | .ok(); | ||
| 186 | } | ||
| 187 | |||
| 188 | fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {} | ||
| 189 | |||
| 190 | fn render_match( | ||
| 191 | &self, | ||
| 192 | ix: usize, | ||
| 193 | selected: bool, | ||
| 194 | _cx: &mut gpui::ViewContext<Picker<Self>>, | ||
| 195 | ) -> Option<Self::ListItem> { | ||
| 196 | let keymap_match = &self.matches[ix]; | ||
| 197 | |||
| 198 | Some( | ||
| 199 | ListItem::new(ix) | ||
| 200 | .selected(selected) | ||
| 201 | .inset(true) | ||
| 202 | .child(HighlightedLabel::new( | ||
| 203 | keymap_match.string.clone(), | ||
| 204 | keymap_match.positions.clone(), | ||
| 205 | )), | ||
| 206 | ) | ||
| 207 | } | ||
| 208 | } | ||
| @@ -0,0 +1,65 @@ | |||
| 1 | use schemars::JsonSchema; | ||
| 2 | use serde::{Deserialize, Serialize}; | ||
| 3 | use settings::Settings; | ||
| 4 | |||
| 5 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] | ||
| 6 | pub enum BaseKeymap { | ||
| 7 | #[default] | ||
| 8 | VSCode, | ||
| 9 | JetBrains, | ||
| 10 | SublimeText, | ||
| 11 | Atom, | ||
| 12 | TextMate, | ||
| 13 | } | ||
| 14 | |||
| 15 | impl BaseKeymap { | ||
| 16 | pub const OPTIONS: [(&'static str, Self); 5] = [ | ||
| 17 | ("VSCode (Default)", Self::VSCode), | ||
| 18 | ("Atom", Self::Atom), | ||
| 19 | ("JetBrains", Self::JetBrains), | ||
| 20 | ("Sublime Text", Self::SublimeText), | ||
| 21 | ("TextMate", Self::TextMate), | ||
| 22 | ]; | ||
| 23 | |||
| 24 | pub fn asset_path(&self) -> Option<&'static str> { | ||
| 25 | match self { | ||
| 26 | BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), | ||
| 27 | BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), | ||
| 28 | BaseKeymap::Atom => Some("keymaps/atom.json"), | ||
| 29 | BaseKeymap::TextMate => Some("keymaps/textmate.json"), | ||
| 30 | BaseKeymap::VSCode => None, | ||
| 31 | } | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn names() -> impl Iterator<Item = &'static str> { | ||
| 35 | Self::OPTIONS.iter().map(|(name, _)| *name) | ||
| 36 | } | ||
| 37 | |||
| 38 | pub fn from_names(option: &str) -> BaseKeymap { | ||
| 39 | Self::OPTIONS | ||
| 40 | .iter() | ||
| 41 | .copied() | ||
| 42 | .find_map(|(name, value)| (name == option).then(|| value)) | ||
| 43 | .unwrap_or_default() | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | impl Settings for BaseKeymap { | ||
| 48 | const KEY: Option<&'static str> = Some("base_keymap"); | ||
| 49 | |||
| 50 | type FileContent = Option<Self>; | ||
| 51 | |||
| 52 | fn load( | ||
| 53 | default_value: &Self::FileContent, | ||
| 54 | user_values: &[&Self::FileContent], | ||
| 55 | _: &mut gpui::AppContext, | ||
| 56 | ) -> anyhow::Result<Self> | ||
| 57 | where | ||
| 58 | Self: Sized, | ||
| 59 | { | ||
| 60 | Ok(user_values | ||
| 61 | .first() | ||
| 62 | .and_then(|v| **v) | ||
| 63 | .unwrap_or(default_value.unwrap())) | ||
| 64 | } | ||
| 65 | } | ||
| @@ -0,0 +1,281 @@ | |||
| 1 | mod base_keymap_picker; | ||
| 2 | mod base_keymap_setting; | ||
| 3 | |||
| 4 | use db::kvp::KEY_VALUE_STORE; | ||
| 5 | use gpui::{ | ||
| 6 | div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, | ||
| 7 | FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View, | ||
| 8 | ViewContext, VisualContext, WeakView, WindowContext, | ||
| 9 | }; | ||
| 10 | use settings::{Settings, SettingsStore}; | ||
| 11 | use std::sync::Arc; | ||
| 12 | use workspace::{ | ||
| 13 | dock::DockPosition, | ||
| 14 | item::{Item, ItemEvent}, | ||
| 15 | open_new, AppState, Welcome, Workspace, WorkspaceId, | ||
| 16 | }; | ||
| 17 | |||
| 18 | pub use base_keymap_setting::BaseKeymap; | ||
| 19 | |||
| 20 | pub const FIRST_OPEN: &str = "first_open"; | ||
| 21 | |||
| 22 | pub fn init(cx: &mut AppContext) { | ||
| 23 | BaseKeymap::register(cx); | ||
| 24 | |||
| 25 | cx.observe_new_views(|workspace: &mut Workspace, _cx| { | ||
| 26 | workspace.register_action(|workspace, _: &Welcome, cx| { | ||
| 27 | let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx)); | ||
| 28 | workspace.add_item(Box::new(welcome_page), cx) | ||
| 29 | }); | ||
| 30 | }) | ||
| 31 | .detach(); | ||
| 32 | |||
| 33 | base_keymap_picker::init(cx); | ||
| 34 | } | ||
| 35 | |||
| 36 | pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) { | ||
| 37 | open_new(&app_state, cx, |workspace, cx| { | ||
| 38 | workspace.toggle_dock(DockPosition::Left, cx); | ||
| 39 | let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx)); | ||
| 40 | workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); | ||
| 41 | cx.focus_view(&welcome_page); | ||
| 42 | cx.notify(); | ||
| 43 | }) | ||
| 44 | .detach(); | ||
| 45 | |||
| 46 | db::write_and_log(cx, || { | ||
| 47 | KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) | ||
| 48 | }); | ||
| 49 | } | ||
| 50 | |||
| 51 | pub struct WelcomePage { | ||
| 52 | workspace: WeakView<Workspace>, | ||
| 53 | focus_handle: FocusHandle, | ||
| 54 | _settings_subscription: Subscription, | ||
| 55 | } | ||
| 56 | |||
| 57 | impl Render for WelcomePage { | ||
| 58 | type Element = Focusable<Div>; | ||
| 59 | |||
| 60 | fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element { | ||
| 61 | // todo!(welcome_ui) | ||
| 62 | // let self_handle = cx.handle(); | ||
| 63 | // let theme = cx.theme(); | ||
| 64 | // let width = theme.welcome.page_width; | ||
| 65 | |||
| 66 | // let telemetry_settings = TelemetrySettings::get(None, cx); | ||
| 67 | // let vim_mode_setting = VimModeSettings::get(cx); | ||
| 68 | |||
| 69 | div() | ||
| 70 | .track_focus(&self.focus_handle) | ||
| 71 | .child(div().size_full().bg(red()).child("Welcome!")) | ||
| 72 | //todo!() | ||
| 73 | // PaneBackdrop::new( | ||
| 74 | // self_handle.id(), | ||
| 75 | // Flex::column() | ||
| 76 | // .with_child( | ||
| 77 | // Flex::column() | ||
| 78 | // .with_child( | ||
| 79 | // theme::ui::svg(&theme.welcome.logo) | ||
| 80 | // .aligned() | ||
| 81 | // .contained() | ||
| 82 | // .aligned(), | ||
| 83 | // ) | ||
| 84 | // .with_child( | ||
| 85 | // Label::new( | ||
| 86 | // "Code at the speed of thought", | ||
| 87 | // theme.welcome.logo_subheading.text.clone(), | ||
| 88 | // ) | ||
| 89 | // .aligned() | ||
| 90 | // .contained() | ||
| 91 | // .with_style(theme.welcome.logo_subheading.container), | ||
| 92 | // ) | ||
| 93 | // .contained() | ||
| 94 | // .with_style(theme.welcome.heading_group) | ||
| 95 | // .constrained() | ||
| 96 | // .with_width(width), | ||
| 97 | // ) | ||
| 98 | // .with_child( | ||
| 99 | // Flex::column() | ||
| 100 | // .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>( | ||
| 101 | // "Choose a theme", | ||
| 102 | // width, | ||
| 103 | // &theme.welcome.button, | ||
| 104 | // cx, | ||
| 105 | // |_, this, cx| { | ||
| 106 | // if let Some(workspace) = this.workspace.upgrade(cx) { | ||
| 107 | // workspace.update(cx, |workspace, cx| { | ||
| 108 | // theme_selector::toggle(workspace, &Default::default(), cx) | ||
| 109 | // }) | ||
| 110 | // } | ||
| 111 | // }, | ||
| 112 | // )) | ||
| 113 | // .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>( | ||
| 114 | // "Choose a keymap", | ||
| 115 | // width, | ||
| 116 | // &theme.welcome.button, | ||
| 117 | // cx, | ||
| 118 | // |_, this, cx| { | ||
| 119 | // if let Some(workspace) = this.workspace.upgrade(cx) { | ||
| 120 | // workspace.update(cx, |workspace, cx| { | ||
| 121 | // base_keymap_picker::toggle( | ||
| 122 | // workspace, | ||
| 123 | // &Default::default(), | ||
| 124 | // cx, | ||
| 125 | // ) | ||
| 126 | // }) | ||
| 127 | // } | ||
| 128 | // }, | ||
| 129 | // )) | ||
| 130 | // .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>( | ||
| 131 | // "Install the CLI", | ||
| 132 | // width, | ||
| 133 | // &theme.welcome.button, | ||
| 134 | // cx, | ||
| 135 | // |_, _, cx| { | ||
| 136 | // cx.app_context() | ||
| 137 | // .spawn(|cx| async move { install_cli::install_cli(&cx).await }) | ||
| 138 | // .detach_and_log_err(cx); | ||
| 139 | // }, | ||
| 140 | // )) | ||
| 141 | // .contained() | ||
| 142 | // .with_style(theme.welcome.button_group) | ||
| 143 | // .constrained() | ||
| 144 | // .with_width(width), | ||
| 145 | // ) | ||
| 146 | // .with_child( | ||
| 147 | // Flex::column() | ||
| 148 | // .with_child( | ||
| 149 | // theme::ui::checkbox::<Diagnostics, Self, _>( | ||
| 150 | // "Enable vim mode", | ||
| 151 | // &theme.welcome.checkbox, | ||
| 152 | // vim_mode_setting, | ||
| 153 | // 0, | ||
| 154 | // cx, | ||
| 155 | // |this, checked, cx| { | ||
| 156 | // if let Some(workspace) = this.workspace.upgrade(cx) { | ||
| 157 | // let fs = workspace.read(cx).app_state().fs.clone(); | ||
| 158 | // update_settings_file::<VimModeSetting>( | ||
| 159 | // fs, | ||
| 160 | // cx, | ||
| 161 | // move |setting| *setting = Some(checked), | ||
| 162 | // ) | ||
| 163 | // } | ||
| 164 | // }, | ||
| 165 | // ) | ||
| 166 | // .contained() | ||
| 167 | // .with_style(theme.welcome.checkbox_container), | ||
| 168 | // ) | ||
| 169 | // .with_child( | ||
| 170 | // theme::ui::checkbox_with_label::<Metrics, _, Self, _>( | ||
| 171 | // Flex::column() | ||
| 172 | // .with_child( | ||
| 173 | // Label::new( | ||
| 174 | // "Send anonymous usage data", | ||
| 175 | // theme.welcome.checkbox.label.text.clone(), | ||
| 176 | // ) | ||
| 177 | // .contained() | ||
| 178 | // .with_style(theme.welcome.checkbox.label.container), | ||
| 179 | // ) | ||
| 180 | // .with_child( | ||
| 181 | // Label::new( | ||
| 182 | // "Help > View Telemetry", | ||
| 183 | // theme.welcome.usage_note.text.clone(), | ||
| 184 | // ) | ||
| 185 | // .contained() | ||
| 186 | // .with_style(theme.welcome.usage_note.container), | ||
| 187 | // ), | ||
| 188 | // &theme.welcome.checkbox, | ||
| 189 | // telemetry_settings.metrics, | ||
| 190 | // 0, | ||
| 191 | // cx, | ||
| 192 | // |this, checked, cx| { | ||
| 193 | // if let Some(workspace) = this.workspace.upgrade(cx) { | ||
| 194 | // let fs = workspace.read(cx).app_state().fs.clone(); | ||
| 195 | // update_settings_file::<TelemetrySettings>( | ||
| 196 | // fs, | ||
| 197 | // cx, | ||
| 198 | // move |setting| setting.metrics = Some(checked), | ||
| 199 | // ) | ||
| 200 | // } | ||
| 201 | // }, | ||
| 202 | // ) | ||
| 203 | // .contained() | ||
| 204 | // .with_style(theme.welcome.checkbox_container), | ||
| 205 | // ) | ||
| 206 | // .with_child( | ||
| 207 | // theme::ui::checkbox::<Diagnostics, Self, _>( | ||
| 208 | // "Send crash reports", | ||
| 209 | // &theme.welcome.checkbox, | ||
| 210 | // telemetry_settings.diagnostics, | ||
| 211 | // 1, | ||
| 212 | // cx, | ||
| 213 | // |this, checked, cx| { | ||
| 214 | // if let Some(workspace) = this.workspace.upgrade(cx) { | ||
| 215 | // let fs = workspace.read(cx).app_state().fs.clone(); | ||
| 216 | // update_settings_file::<TelemetrySettings>( | ||
| 217 | // fs, | ||
| 218 | // cx, | ||
| 219 | // move |setting| setting.diagnostics = Some(checked), | ||
| 220 | // ) | ||
| 221 | // } | ||
| 222 | // }, | ||
| 223 | // ) | ||
| 224 | // .contained() | ||
| 225 | // .with_style(theme.welcome.checkbox_container), | ||
| 226 | // ) | ||
| 227 | // .contained() | ||
| 228 | // .with_style(theme.welcome.checkbox_group) | ||
| 229 | // .constrained() | ||
| 230 | // .with_width(width), | ||
| 231 | // ) | ||
| 232 | // .constrained() | ||
| 233 | // .with_max_width(width) | ||
| 234 | // .contained() | ||
| 235 | // .with_uniform_padding(10.) | ||
| 236 | // .aligned() | ||
| 237 | // .into_any(), | ||
| 238 | // ) | ||
| 239 | // .into_any_named("welcome page") | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | impl WelcomePage { | ||
| 244 | pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self { | ||
| 245 | WelcomePage { | ||
| 246 | focus_handle: cx.focus_handle(), | ||
| 247 | workspace: workspace.weak_handle(), | ||
| 248 | _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()), | ||
| 249 | } | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | impl EventEmitter<ItemEvent> for WelcomePage {} | ||
| 254 | |||
| 255 | impl FocusableView for WelcomePage { | ||
| 256 | fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { | ||
| 257 | self.focus_handle.clone() | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 261 | impl Item for WelcomePage { | ||
| 262 | fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement { | ||
| 263 | "Welcome to Zed!".into_any() | ||
| 264 | } | ||
| 265 | |||
| 266 | fn show_toolbar(&self) -> bool { | ||
| 267 | false | ||
| 268 | } | ||
| 269 | |||
| 270 | fn clone_on_split( | ||
| 271 | &self, | ||
| 272 | _workspace_id: WorkspaceId, | ||
| 273 | cx: &mut ViewContext<Self>, | ||
| 274 | ) -> Option<View<Self>> { | ||
| 275 | Some(cx.build_view(|cx| WelcomePage { | ||
| 276 | focus_handle: cx.focus_handle(), | ||
| 277 | workspace: self.workspace.clone(), | ||
| 278 | _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()), | ||
| 279 | })) | ||
| 280 | } | ||
| 281 | } | ||
| @@ -7,8 +7,8 @@ use gpui::{ · 6 unmodified lines | |||
| 7 | use schemars::JsonSchema; | 7 | use schemars::JsonSchema; |
| 8 | use serde::{Deserialize, Serialize}; | 8 | use serde::{Deserialize, Serialize}; |
| 9 | use std::sync::Arc; | 9 | use std::sync::Arc; |
| 10 | use theme2::ActiveTheme; | 10 | use ui::prelude::*; |
| 11 | use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip}; | 11 | use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip}; |
| 12 | 12 | ||
| 13 | pub enum PanelEvent { | 13 | pub 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| { |
| @@ -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::Enabled | 1486 | .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::Enabled | 1492 | .disabled(!self.can_navigate_forward()), |
| 1495 | .if_enabled(self.can_navigate_forward()), | ||
| 1496 | ), | ||
| 1497 | ), | 1493 | ), |
| 1498 | ), | 1494 | ), |
| 1499 | ), | 1495 | ), |
| @@ -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 | }; |
| 8 | use theme2::ActiveTheme; | 8 | use ui::prelude::*; |
| 9 | use ui::{h_stack, Button, Icon, IconButton}; | 9 | use ui::{h_stack, Button, Icon, IconButton}; |
| 10 | use util::ResultExt; | 10 | use util::ResultExt; |
| 11 | 11 | ||
| @@ -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 | }; |
| 6 | use theme2::ActiveTheme; | 6 | use ui::prelude::*; |
| 7 | use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label}; | 7 | use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label}; |
| 8 | 8 | ||
| 9 | pub enum ToolbarItemEvent { | 9 | pub enum ToolbarItemEvent { |
| @@ -1808,22 +1808,22 @@ impl Workspace { · 1807 unmodified lines | |||
| 1808 | pane | 1808 | 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 | // true | 1819 | true |
| 1820 | // } else { | 1820 | } else { |
| 1821 | // false | 1821 | false |
| 1822 | // } | 1822 | } |
| 1823 | // } else { | 1823 | } else { |
| 1824 | // false | 1824 | 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_pane | 1829 | self.active_pane |
| @@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"] · 2 unmodified lines | |||
| 3 | description = "The fast, collaborative code editor." | 3 | description = "The fast, collaborative code editor." |
| 4 | edition = "2021" | 4 | edition = "2021" |
| 5 | name = "zed" | 5 | name = "zed" |
| 6 | version = "0.115.0" | 6 | version = "0.116.0" |
| 7 | publish = false | 7 | publish = false |
| 8 | 8 | ||
| 9 | [lib] | 9 | [lib] |
| @@ -66,12 +66,12 @@ shellexpand = "2.1.0" · 65 unmodified lines | |||
| 66 | text = { package = "text2", path = "../text2" } | 66 | text = { package = "text2", path = "../text2" } |
| 67 | terminal_view = { package = "terminal_view2", path = "../terminal_view2" } | 67 | terminal_view = { package = "terminal_view2", path = "../terminal_view2" } |
| 68 | theme = { package = "theme2", path = "../theme2" } | 68 | theme = { package = "theme2", path = "../theme2" } |
| 69 | # theme_selector = { path = "../theme_selector" } | 69 | theme_selector = { package = "theme_selector2", path = "../theme_selector2" } |
| 70 | util = { path = "../util" } | 70 | util = { path = "../util" } |
| 71 | # semantic_index = { path = "../semantic_index" } | 71 | # semantic_index = { path = "../semantic_index" } |
| 72 | # vim = { path = "../vim" } | 72 | # vim = { path = "../vim" } |
| 73 | workspace = { package = "workspace2", path = "../workspace2" } | 73 | workspace = { package = "workspace2", path = "../workspace2" } |
| 74 | # welcome = { path = "../welcome" } | 74 | welcome = { package = "welcome2", path = "../welcome2" } |
| 75 | zed_actions = {package = "zed_actions2", path = "../zed_actions2"} | 75 | zed_actions = {package = "zed_actions2", path = "../zed_actions2"} |
| 76 | anyhow.workspace = true | 76 | anyhow.workspace = true |
| 77 | async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } | 77 | async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } |
| @@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE; · 12 unmodified lines | |||
| 13 | use editor::Editor; | 13 | use editor::Editor; |
| 14 | use fs::RealFs; | 14 | use fs::RealFs; |
| 15 | use futures::StreamExt; | 15 | use futures::StreamExt; |
| 16 | use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; | 16 | use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; |
| 17 | use isahc::{prelude::Configurable, Request}; | 17 | use isahc::{prelude::Configurable, Request}; |
| 18 | use language::LanguageRegistry; | 18 | use language::LanguageRegistry; |
| 19 | use log::LevelFilter; | 19 | use 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 | }; |
| 50 | use uuid::Uuid; | 50 | use uuid::Uuid; |
| 51 | use welcome::{show_welcome_experience, FIRST_OPEN}; | ||
| 51 | use workspace::{AppState, WorkspaceStore}; | 52 | use workspace::{AppState, WorkspaceStore}; |
| 52 | use zed2::{ | 53 | use 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 | // .await | 287 | // .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 | .await | 367 | .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<()> { | 710 | async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> { |
| 714 | // let mut events = fs | 711 | 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 | // .await | 719 | 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) | 726 | fn 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 = fs | 735 | .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 | |||
| 783 | pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { | ||
| 784 | // &[ | ||
| 785 | // ("Go to file", &file_finder::Toggle), | ||
| 786 | // ("Open command palette", &command_palette::Toggle), | ||
| 787 | // ("Open recent projects", &recent_projects::OpenRecent), | ||
| 788 | // ("Change your settings", &zed_actions::OpenSettings), | ||
| 789 | // ] | ||
| 790 | // todo!() | ||
| 791 | &[] | ||
| 792 | } | 746 | } |
| 747 | |||
| 748 | #[cfg(not(debug_assertions))] | ||
| 749 | async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> { | ||
| 750 | None | ||
| 751 | } | ||
| 752 | |||
| 753 | #[cfg(not(debug_assertions))] | ||
| 754 | fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {} | ||
| @@ -11,7 +11,7 @@ graph_file=target/crate-graph.html · 10 unmodified lines | |||
| 11 | cargo depgraph \ | 11 | cargo 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_file | 16 | | dot -Tsvg > $graph_file |
| 17 | 17 | ||