Unified · Split
| @@ -9471,6 +9471,27 @@ dependencies = [ · 9470 unmodified lines | ||
| 9471 | 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 | 9495 | [[package]] |
| 9475 | 9496 | name = "thiserror" |
| 9476 | 9497 | version = "1.0.48" |
| @@ -11054,6 +11075,31 @@ dependencies = [ · 1577 unmodified lines | ||
| 11054 | 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 | 11103 | [[package]] |
| 11058 | 11104 | name = "which" |
| 11059 | 11105 | version = "4.4.2" |
| @@ -11508,7 +11554,7 @@ dependencies = [ · 448 unmodified lines | ||
| 11508 | 11554 | |
| 11509 | 11555 | [[package]] |
| 11510 | 11556 | name = "zed" |
| 11511 | −version = "0.115.0" | |
| 11557 | +version = "0.116.0" | |
| 11512 | 11558 | dependencies = [ |
| 11513 | 11559 | "activity_indicator", |
| 11514 | 11560 | "ai", |
| @@ -11720,6 +11766,7 @@ dependencies = [ · 205 unmodified lines | ||
| 11720 | 11766 | "terminal_view2", |
| 11721 | 11767 | "text2", |
| 11722 | 11768 | "theme2", |
| 11769 | + "theme_selector2", | |
| 11723 | 11770 | "thiserror", |
| 11724 | 11771 | "tiny_http", |
| 11725 | 11772 | "toml 0.5.11", |
| @@ -11757,6 +11804,7 @@ dependencies = [ · 31 unmodified lines | ||
| 11757 | 11804 | "urlencoding", |
| 11758 | 11805 | "util", |
| 11759 | 11806 | "uuid 1.4.1", |
| 11807 | + "welcome2", | |
| 11760 | 11808 | "workspace2", |
| 11761 | 11809 | "zed_actions2", |
| 11762 | 11810 | ] |
| @@ -107,6 +107,7 @@ members = [ · 106 unmodified lines | ||
| 107 | 107 | "crates/theme2", |
| 108 | 108 | "crates/theme_importer", |
| 109 | 109 | "crates/theme_selector", |
| 110 | + "crates/theme_selector2", | |
| 110 | 111 | "crates/ui2", |
| 111 | 112 | "crates/util", |
| 112 | 113 | "crates/semantic_index", |
| @@ -115,6 +116,7 @@ members = [ · 2 unmodified lines | ||
| 115 | 116 | "crates/vcs_menu", |
| 116 | 117 | "crates/workspace2", |
| 117 | 118 | "crates/welcome", |
| 119 | + "crates/welcome2", | |
| 118 | 120 | "crates/xtask", |
| 119 | 121 | "crates/zed", |
| 120 | 122 | "crates/zed2", |
| @@ -14,8 +14,8 @@ use client::{ · 13 unmodified lines | ||
| 14 | 14 | use collections::HashSet; |
| 15 | 15 | use futures::{channel::oneshot, future::Shared, Future, FutureExt}; |
| 16 | 16 | use gpui::{ |
| 17 | − AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, | |
| 18 | − View, ViewContext, VisualContext, WeakModel, WeakView, | |
| 17 | + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel, | |
| 18 | + Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle, | |
| 19 | 19 | }; |
| 20 | 20 | pub use participant::ParticipantLocation; |
| 21 | 21 | use postage::watch; |
| @@ -334,12 +334,55 @@ impl ActiveCall { · 312 unmodified lines | ||
| 334 | 334 | pub fn join_channel( |
| 335 | 335 | &mut self, |
| 336 | 336 | channel_id: u64, |
| 337 | + requesting_window: Option<WindowHandle<Workspace>>, | |
| 337 | 338 | cx: &mut ModelContext<Self>, |
| 338 | 339 | ) -> Task<Result<Option<Model<Room>>>> { |
| 339 | 340 | if let Some(room) = self.room().cloned() { |
| 340 | 341 | if room.read(cx).channel_id() == Some(channel_id) { |
| 341 | − return Task::ready(Ok(Some(room))); | |
| 342 | − } else { | |
| 342 | + return cx.spawn(|_, _| async move { | |
| 343 | + todo!(); | |
| 344 | + // let future = room.update(&mut cx, |room, cx| { | |
| 345 | + // room.most_active_project(cx).map(|(host, project)| { | |
| 346 | + // room.join_project(project, host, app_state.clone(), cx) | |
| 347 | + // }) | |
| 348 | + // }) | |
| 349 | + | |
| 350 | + // if let Some(future) = future { | |
| 351 | + // future.await?; | |
| 352 | + // } | |
| 353 | + | |
| 354 | + // Ok(Some(room)) | |
| 355 | + }); | |
| 356 | + } | |
| 357 | + | |
| 358 | + let should_prompt = room.update(cx, |room, _| { | |
| 359 | + room.channel_id().is_some() | |
| 360 | + && room.is_sharing_project() | |
| 361 | + && room.remote_participants().len() > 0 | |
| 362 | + }); | |
| 363 | + if should_prompt && requesting_window.is_some() { | |
| 364 | + return cx.spawn(|this, mut cx| async move { | |
| 365 | + let answer = requesting_window.unwrap().update(&mut cx, |_, cx| { | |
| 366 | + cx.prompt( | |
| 367 | + PromptLevel::Warning, | |
| 368 | + "Leaving this call will unshare your current project.\nDo you want to switch channels?", | |
| 369 | + &["Yes, Join Channel", "Cancel"], | |
| 370 | + ) | |
| 371 | + })?; | |
| 372 | + if answer.await? == 1 { | |
| 373 | + return Ok(None); | |
| 374 | + } | |
| 375 | + | |
| 376 | + room.update(&mut cx, |room, cx| room.clear_state(cx))?; | |
| 377 | + | |
| 378 | + this.update(&mut cx, |this, cx| { | |
| 379 | + this.join_channel(channel_id, requesting_window, cx) | |
| 380 | + })? | |
| 381 | + .await | |
| 382 | + }); | |
| 383 | + } | |
| 384 | + | |
| 385 | + if room.read(cx).channel_id().is_some() { | |
| 343 | 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 { | |
| 697 | − read_credentials_from_keychain(cx).await.is_some() | |
| 696 | + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { | |
| 697 | + read_credentials_from_keychain(cx).is_some() | |
| 698 | 698 | } |
| 699 | 699 | |
| 700 | 700 | #[async_recursion(?Send)] |
| @@ -725,7 +725,7 @@ impl Client { · 24 unmodified lines | ||
| 725 | 725 | let mut read_from_keychain = false; |
| 726 | 726 | let mut credentials = self.state.read().credentials.clone(); |
| 727 | 727 | if credentials.is_none() && try_keychain { |
| 728 | − credentials = read_credentials_from_keychain(cx).await; | |
| 728 | + credentials = read_credentials_from_keychain(cx); | |
| 729 | 729 | read_from_keychain = credentials.is_some(); |
| 730 | 730 | } |
| 731 | 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 | 1328 | if IMPERSONATE_LOGIN.is_some() { |
| 1329 | 1329 | return None; |
| 1330 | 1330 | } |
| @@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member( · 363 unmodified lines | ||
| 364 | 364 | let active_call_b = cx_b.read(ActiveCall::global); |
| 365 | 365 | |
| 366 | 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 | 369 | .await |
| 369 | 370 | .is_ok()); |
| 370 | 371 | } |
| @@ -394,7 +395,9 @@ async fn test_channel_room( · 23 unmodified lines | ||
| 394 | 395 | let active_call_b = cx_b.read(ActiveCall::global); |
| 395 | 396 | |
| 396 | 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 | 401 | .await |
| 399 | 402 | .unwrap(); |
| 400 | 403 | |
| @@ -442,7 +445,9 @@ async fn test_channel_room( · 41 unmodified lines | ||
| 442 | 445 | }); |
| 443 | 446 | |
| 444 | 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 | 451 | .await |
| 447 | 452 | .unwrap(); |
| 448 | 453 | |
| @@ -559,12 +564,16 @@ async fn test_channel_room( · 110 unmodified lines | ||
| 559 | 564 | }); |
| 560 | 565 | |
| 561 | 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 | 570 | .await |
| 564 | 571 | .unwrap(); |
| 565 | 572 | |
| 566 | 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 | 577 | .await |
| 569 | 578 | .unwrap(); |
| 570 | 579 | |
| @@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo · 37 unmodified lines | ||
| 608 | 617 | let active_call_a = cx_a.read(ActiveCall::global); |
| 609 | 618 | |
| 610 | 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 | 623 | .await |
| 613 | 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 | 639 | active_call_a |
| 629 | 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 | 643 | .await |
| 633 | 644 | .unwrap(); |
| @@ -793,7 +804,7 @@ async fn test_call_from_channel( · 159 unmodified lines | ||
| 793 | 804 | let active_call_b = cx_b.read(ActiveCall::global); |
| 794 | 805 | |
| 795 | 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 | 808 | .await |
| 798 | 809 | .unwrap(); |
| 799 | 810 | |
| @@ -1286,7 +1297,7 @@ async fn test_guest_access( · 486 unmodified lines | ||
| 1286 | 1297 | |
| 1287 | 1298 | // Non-members should not be allowed to join |
| 1288 | 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 | 1301 | .await |
| 1291 | 1302 | .is_err()); |
| 1292 | 1303 | |
| @@ -1308,7 +1319,7 @@ async fn test_guest_access( · 15 unmodified lines | ||
| 1308 | 1319 | |
| 1309 | 1320 | // Client B joins channel A as a guest |
| 1310 | 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 | 1323 | .await |
| 1313 | 1324 | .unwrap(); |
| 1314 | 1325 | |
| @@ -1341,7 +1352,7 @@ async fn test_guest_access( · 26 unmodified lines | ||
| 1341 | 1352 | assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); |
| 1342 | 1353 | |
| 1343 | 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 | 1356 | .await |
| 1346 | 1357 | .unwrap(); |
| 1347 | 1358 | |
| @@ -1372,7 +1383,7 @@ async fn test_invite_access( · 24 unmodified lines | ||
| 1372 | 1383 | |
| 1373 | 1384 | // should not be allowed to join |
| 1374 | 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 | 1387 | .await |
| 1377 | 1388 | .is_err()); |
| 1378 | 1389 | |
| @@ -1390,7 +1401,7 @@ async fn test_invite_access( · 11 unmodified lines | ||
| 1390 | 1401 | .unwrap(); |
| 1391 | 1402 | |
| 1392 | 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 | 1405 | .await |
| 1395 | 1406 | .unwrap(); |
| 1396 | 1407 | |
| @@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 509 unmodified lines | ||
| 510 | 510 | |
| 511 | 511 | // Simultaneously join channel 1 and then channel 2 |
| 512 | 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 | 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 | 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 | 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 | 545 | b_invite.await.unwrap(); |
| 544 | 546 | c_invite.await.unwrap(); |
| @@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( · 22 unmodified lines | ||
| 567 | 569 | .unwrap(); |
| 568 | 570 | |
| 569 | 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 | 575 | let b_invite = active_call_a.update(cx_a, |call, cx| { |
| 573 | 576 | call.invite(client_b.user_id().unwrap(), None, cx) |
| @@ -17,6 +17,7 @@ mod contact_finder; · 16 unmodified lines | ||
| 17 | 17 | // Client, Contact, User, UserStore, |
| 18 | 18 | // }; |
| 19 | 19 | use contact_finder::ContactFinder; |
| 20 | +use menu::Confirm; | |
| 20 | 21 | use rpc::proto; |
| 21 | 22 | // use context_menu::{ContextMenu, ContextMenuItem}; |
| 22 | 23 | // use db::kvp::KEY_VALUE_STORE; |
| @@ -90,10 +91,10 @@ use rpc::proto; · 67 unmodified lines | ||
| 90 | 91 | // channel_id: ChannelId, |
| 91 | 92 | // } |
| 92 | 93 | |
| 93 | −// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] | |
| 94 | −// pub struct OpenChannelNotes { | |
| 95 | −// pub channel_id: ChannelId, | |
| 96 | −// } | |
| 94 | +#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)] | |
| 95 | +pub struct OpenChannelNotes { | |
| 96 | + pub channel_id: ChannelId, | |
| 97 | +} | |
| 97 | 98 | |
| 98 | 99 | // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 99 | 100 | // pub struct JoinChannelCall { |
| @@ -160,26 +161,26 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; · 60 unmodified lines | ||
| 160 | 161 | use std::{iter::once, mem, sync::Arc}; |
| 161 | 162 | |
| 162 | 163 | use call::ActiveCall; |
| 163 | −use channel::{Channel, ChannelId, ChannelStore}; | |
| 164 | +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; | |
| 164 | 165 | use client::{Client, Contact, User, UserStore}; |
| 165 | 166 | use db::kvp::KEY_VALUE_STORE; |
| 166 | 167 | use editor::Editor; |
| 167 | −use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; | |
| 168 | +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; | |
| 168 | 169 | use fuzzy::{match_strings, StringMatchCandidate}; |
| 169 | 170 | use gpui::{ |
| 170 | − actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, | |
| 171 | − FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, | |
| 172 | − Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, | |
| 173 | − WeakView, | |
| 171 | + actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div, | |
| 172 | + EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, | |
| 173 | + ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, | |
| 174 | + ViewContext, VisualContext, WeakView, | |
| 174 | 175 | }; |
| 175 | 176 | use project::Fs; |
| 176 | 177 | use serde_derive::{Deserialize, Serialize}; |
| 177 | −use settings::Settings; | |
| 178 | +use settings::{Settings, SettingsStore}; | |
| 178 | 179 | use ui::{ |
| 179 | − h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, Label, List, ListHeader, ListItem, | |
| 180 | − Toggle, Tooltip, | |
| 180 | + h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List, | |
| 181 | + ListHeader, ListItem, Toggle, Tooltip, | |
| 181 | 182 | }; |
| 182 | −use util::{maybe, ResultExt}; | |
| 183 | +use util::{maybe, ResultExt, TryFutureExt}; | |
| 183 | 184 | use workspace::{ |
| 184 | 185 | dock::{DockPosition, Panel, PanelEvent}, |
| 185 | 186 | notifications::NotifyResultExt, |
| @@ -293,10 +294,10 @@ pub enum ChannelEditingState { · 107 unmodified lines | ||
| 293 | 294 | } |
| 294 | 295 | |
| 295 | 296 | impl ChannelEditingState { |
| 296 | − fn pending_name(&self) -> Option<&str> { | |
| 297 | + fn pending_name(&self) -> Option<String> { | |
| 297 | 298 | match self { |
| 298 | − ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), | |
| 299 | − ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), | |
| 299 | + ChannelEditingState::Create { pending_name, .. } => pending_name.clone(), | |
| 300 | + ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(), | |
| 300 | 301 | } |
| 301 | 302 | } |
| 302 | 303 | } |
| @@ -306,10 +307,10 @@ pub struct CollabPanel { · 3 unmodified lines | ||
| 306 | 307 | fs: Arc<dyn Fs>, |
| 307 | 308 | focus_handle: FocusHandle, |
| 308 | 309 | // channel_clipboard: Option<ChannelMoveClipboard>, |
| 309 | − // pending_serialization: Task<Option<()>>, | |
| 310 | + pending_serialization: Task<Option<()>>, | |
| 310 | 311 | // context_menu: ViewHandle<ContextMenu>, |
| 311 | 312 | filter_editor: View<Editor>, |
| 312 | − // channel_name_editor: ViewHandle<Editor>, | |
| 313 | + channel_name_editor: View<Editor>, | |
| 313 | 314 | channel_editing_state: Option<ChannelEditingState>, |
| 314 | 315 | entries: Vec<ListEntry>, |
| 315 | 316 | selection: Option<usize>, |
| @@ -322,17 +323,17 @@ pub struct CollabPanel { · 6 unmodified lines | ||
| 322 | 323 | subscriptions: Vec<Subscription>, |
| 323 | 324 | collapsed_sections: Vec<Section>, |
| 324 | 325 | collapsed_channels: Vec<ChannelId>, |
| 325 | − // drag_target_channel: ChannelDragTarget, | |
| 326 | + drag_target_channel: ChannelDragTarget, | |
| 326 | 327 | workspace: WeakView<Workspace>, |
| 327 | 328 | // context_menu_on_selected: bool, |
| 328 | 329 | } |
| 329 | 330 | |
| 330 | −// #[derive(PartialEq, Eq)] | |
| 331 | −// enum ChannelDragTarget { | |
| 332 | −// None, | |
| 333 | −// Root, | |
| 334 | −// Channel(ChannelId), | |
| 335 | −// } | |
| 331 | +#[derive(PartialEq, Eq)] | |
| 332 | +enum ChannelDragTarget { | |
| 333 | + None, | |
| 334 | + Root, | |
| 335 | + Channel(ChannelId), | |
| 336 | +} | |
| 336 | 337 | |
| 337 | 338 | #[derive(Serialize, Deserialize)] |
| 338 | 339 | struct SerializedCollabPanel { |
| @@ -438,28 +439,21 @@ impl CollabPanel { · 99 unmodified lines | ||
| 438 | 439 | // }) |
| 439 | 440 | // .detach(); |
| 440 | 441 | |
| 441 | − // let channel_name_editor = cx.add_view(|cx| { | |
| 442 | − // Editor::single_line( | |
| 443 | − // Some(Arc::new(|theme| { | |
| 444 | − // theme.collab_panel.user_query_editor.clone() | |
| 445 | − // })), | |
| 446 | − // cx, | |
| 447 | − // ) | |
| 448 | − // }); | |
| 449 | − | |
| 450 | − // cx.subscribe(&channel_name_editor, |this, _, event, cx| { | |
| 451 | − // if let editor::Event::Blurred = event { | |
| 452 | − // if let Some(state) = &this.channel_editing_state { | |
| 453 | − // if state.pending_name().is_some() { | |
| 454 | − // return; | |
| 455 | − // } | |
| 456 | − // } | |
| 457 | − // this.take_editing_state(cx); | |
| 458 | − // this.update_entries(false, cx); | |
| 459 | − // cx.notify(); | |
| 460 | − // } | |
| 461 | − // }) | |
| 462 | − // .detach(); | |
| 442 | + let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx)); | |
| 443 | + | |
| 444 | + cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| { | |
| 445 | + if let editor::EditorEvent::Blurred = event { | |
| 446 | + if let Some(state) = &this.channel_editing_state { | |
| 447 | + if state.pending_name().is_some() { | |
| 448 | + return; | |
| 449 | + } | |
| 450 | + } | |
| 451 | + this.take_editing_state(cx); | |
| 452 | + this.update_entries(false, cx); | |
| 453 | + cx.notify(); | |
| 454 | + } | |
| 455 | + }) | |
| 456 | + .detach(); | |
| 463 | 457 | |
| 464 | 458 | // let list_state = |
| 465 | 459 | // ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| { |
| @@ -597,9 +591,9 @@ impl CollabPanel { · 131 unmodified lines | ||
| 597 | 591 | focus_handle: cx.focus_handle(), |
| 598 | 592 | // channel_clipboard: None, |
| 599 | 593 | fs: workspace.app_state().fs.clone(), |
| 600 | − // pending_serialization: Task::ready(None), | |
| 594 | + pending_serialization: Task::ready(None), | |
| 601 | 595 | // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), |
| 602 | − // channel_name_editor, | |
| 596 | + channel_name_editor, | |
| 603 | 597 | filter_editor, |
| 604 | 598 | entries: Vec::default(), |
| 605 | 599 | channel_editing_state: None, |
| @@ -614,59 +608,58 @@ impl CollabPanel { · 8 unmodified lines | ||
| 614 | 608 | workspace: workspace.weak_handle(), |
| 615 | 609 | client: workspace.app_state().client.clone(), |
| 616 | 610 | // context_menu_on_selected: true, |
| 617 | − // drag_target_channel: ChannelDragTarget::None, | |
| 611 | + drag_target_channel: ChannelDragTarget::None, | |
| 618 | 612 | // list_state, |
| 619 | 613 | }; |
| 620 | 614 | |
| 621 | 615 | this.update_entries(false, cx); |
| 622 | 616 | |
| 623 | − // // Update the dock position when the setting changes. | |
| 624 | − // let mut old_dock_position = this.position(cx); | |
| 625 | − // this.subscriptions | |
| 626 | − // .push( | |
| 627 | − // cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| { | |
| 628 | − // let new_dock_position = this.position(cx); | |
| 629 | − // if new_dock_position != old_dock_position { | |
| 630 | − // old_dock_position = new_dock_position; | |
| 631 | − // cx.emit(Event::DockPositionChanged); | |
| 632 | − // } | |
| 633 | − // cx.notify(); | |
| 634 | − // }), | |
| 635 | − // ); | |
| 617 | + // Update the dock position when the setting changes. | |
| 618 | + let mut old_dock_position = this.position(cx); | |
| 619 | + this.subscriptions.push(cx.observe_global::<SettingsStore>( | |
| 620 | + move |this: &mut Self, cx| { | |
| 621 | + let new_dock_position = this.position(cx); | |
| 622 | + if new_dock_position != old_dock_position { | |
| 623 | + old_dock_position = new_dock_position; | |
| 624 | + cx.emit(PanelEvent::ChangePosition); | |
| 625 | + } | |
| 626 | + cx.notify(); | |
| 627 | + }, | |
| 628 | + )); | |
| 636 | 629 | |
| 637 | − // let active_call = ActiveCall::global(cx); | |
| 630 | + let active_call = ActiveCall::global(cx); | |
| 638 | 631 | this.subscriptions |
| 639 | 632 | .push(cx.observe(&this.user_store, |this, _, cx| { |
| 640 | 633 | this.update_entries(true, cx) |
| 641 | 634 | })); |
| 642 | − // this.subscriptions | |
| 643 | − // .push(cx.observe(&this.channel_store, |this, _, cx| { | |
| 644 | − // this.update_entries(true, cx) | |
| 645 | − // })); | |
| 646 | − // this.subscriptions | |
| 647 | − // .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); | |
| 648 | − // this.subscriptions | |
| 649 | − // .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| { | |
| 650 | − // this.update_entries(true, cx) | |
| 651 | − // })); | |
| 652 | − // this.subscriptions.push(cx.subscribe( | |
| 653 | − // &this.channel_store, | |
| 654 | − // |this, _channel_store, e, cx| match e { | |
| 655 | − // ChannelEvent::ChannelCreated(channel_id) | |
| 656 | − // | ChannelEvent::ChannelRenamed(channel_id) => { | |
| 657 | − // if this.take_editing_state(cx) { | |
| 658 | − // this.update_entries(false, cx); | |
| 659 | − // this.selection = this.entries.iter().position(|entry| { | |
| 660 | − // if let ListEntry::Channel { channel, .. } = entry { | |
| 661 | − // channel.id == *channel_id | |
| 662 | − // } else { | |
| 663 | − // false | |
| 664 | − // } | |
| 665 | − // }); | |
| 666 | − // } | |
| 667 | − // } | |
| 668 | − // }, | |
| 669 | − // )); | |
| 635 | + this.subscriptions | |
| 636 | + .push(cx.observe(&this.channel_store, |this, _, cx| { | |
| 637 | + this.update_entries(true, cx) | |
| 638 | + })); | |
| 639 | + this.subscriptions | |
| 640 | + .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); | |
| 641 | + this.subscriptions | |
| 642 | + .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| { | |
| 643 | + this.update_entries(true, cx) | |
| 644 | + })); | |
| 645 | + this.subscriptions.push(cx.subscribe( | |
| 646 | + &this.channel_store, | |
| 647 | + |this, _channel_store, e, cx| match e { | |
| 648 | + ChannelEvent::ChannelCreated(channel_id) | |
| 649 | + | ChannelEvent::ChannelRenamed(channel_id) => { | |
| 650 | + if this.take_editing_state(cx) { | |
| 651 | + this.update_entries(false, cx); | |
| 652 | + this.selection = this.entries.iter().position(|entry| { | |
| 653 | + if let ListEntry::Channel { channel, .. } = entry { | |
| 654 | + channel.id == *channel_id | |
| 655 | + } else { | |
| 656 | + false | |
| 657 | + } | |
| 658 | + }); | |
| 659 | + } | |
| 660 | + } | |
| 661 | + }, | |
| 662 | + )); | |
| 670 | 663 | |
| 671 | 664 | this |
| 672 | 665 | }) |
| @@ -696,10 +689,9 @@ impl CollabPanel { · 23 unmodified lines | ||
| 696 | 689 | if let Some(serialized_panel) = serialized_panel { |
| 697 | 690 | panel.update(cx, |panel, cx| { |
| 698 | 691 | panel.width = serialized_panel.width; |
| 699 | − //todo!(collapsed_channels) | |
| 700 | − // panel.collapsed_channels = serialized_panel | |
| 701 | − // .collapsed_channels | |
| 702 | − // .unwrap_or_else(|| Vec::new()); | |
| 692 | + panel.collapsed_channels = serialized_panel | |
| 693 | + .collapsed_channels | |
| 694 | + .unwrap_or_else(|| Vec::new()); | |
| 703 | 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>) { | |
| 711 | − // let width = self.width; | |
| 712 | − // let collapsed_channels = self.collapsed_channels.clone(); | |
| 713 | − // self.pending_serialization = cx.background().spawn( | |
| 714 | − // async move { | |
| 715 | − // KEY_VALUE_STORE | |
| 716 | − // .write_kvp( | |
| 717 | − // COLLABORATION_PANEL_KEY.into(), | |
| 718 | − // serde_json::to_string(&SerializedCollabPanel { | |
| 719 | − // width, | |
| 720 | − // collapsed_channels: Some(collapsed_channels), | |
| 721 | − // })?, | |
| 722 | − // ) | |
| 723 | − // .await?; | |
| 724 | − // anyhow::Ok(()) | |
| 725 | − // } | |
| 726 | − // .log_err(), | |
| 727 | − // ); | |
| 728 | − // } | |
| 702 | + fn serialize(&mut self, cx: &mut ViewContext<Self>) { | |
| 703 | + let width = self.width; | |
| 704 | + let collapsed_channels = self.collapsed_channels.clone(); | |
| 705 | + self.pending_serialization = cx.background_executor().spawn( | |
| 706 | + async move { | |
| 707 | + KEY_VALUE_STORE | |
| 708 | + .write_kvp( | |
| 709 | + COLLABORATION_PANEL_KEY.into(), | |
| 710 | + serde_json::to_string(&SerializedCollabPanel { | |
| 711 | + width, | |
| 712 | + collapsed_channels: Some(collapsed_channels), | |
| 713 | + })?, | |
| 714 | + ) | |
| 715 | + .await?; | |
| 716 | + anyhow::Ok(()) | |
| 717 | + } | |
| 718 | + .log_err(), | |
| 719 | + ); | |
| 720 | + } | |
| 729 | 721 | |
| 730 | 722 | fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) { |
| 731 | 723 | let channel_store = self.channel_store.read(cx); |
| @@ -1456,16 +1448,16 @@ impl CollabPanel { · 724 unmodified lines | ||
| 1456 | 1448 | // .into_any() |
| 1457 | 1449 | // } |
| 1458 | 1450 | |
| 1459 | − // fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool { | |
| 1460 | − // if let Some(_) = self.channel_editing_state.take() { | |
| 1461 | − // self.channel_name_editor.update(cx, |editor, cx| { | |
| 1462 | − // editor.set_text("", cx); | |
| 1463 | − // }); | |
| 1464 | − // true | |
| 1465 | − // } else { | |
| 1466 | − // false | |
| 1467 | − // } | |
| 1468 | − // } | |
| 1451 | + fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool { | |
| 1452 | + if let Some(_) = self.channel_editing_state.take() { | |
| 1453 | + self.channel_name_editor.update(cx, |editor, cx| { | |
| 1454 | + editor.set_text("", cx); | |
| 1455 | + }); | |
| 1456 | + true | |
| 1457 | + } else { | |
| 1458 | + false | |
| 1459 | + } | |
| 1460 | + } | |
| 1469 | 1461 | |
| 1470 | 1462 | // fn render_contact_placeholder( |
| 1471 | 1463 | // &self, |
| @@ -1501,67 +1493,6 @@ impl CollabPanel { · 29 unmodified lines | ||
| 1501 | 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 | 1496 | // fn render_channel_notes( |
| 1566 | 1497 | // &self, |
| 1567 | 1498 | // channel_id: ChannelId, |
| @@ -1754,109 +1685,6 @@ impl CollabPanel { · 186 unmodified lines | ||
| 1754 | 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 | 1688 | // fn has_subchannels(&self, ix: usize) -> bool { |
| 1861 | 1689 | // self.entries.get(ix).map_or(false, |entry| { |
| 1862 | 1690 | // if let ListEntry::Channel { has_children, .. } = entry { |
| @@ -2054,148 +1882,148 @@ impl CollabPanel { · 191 unmodified lines | ||
| 2054 | 1882 | // cx.notify(); |
| 2055 | 1883 | // } |
| 2056 | 1884 | |
| 2057 | − // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { | |
| 2058 | − // if self.confirm_channel_edit(cx) { | |
| 2059 | − // return; | |
| 2060 | − // } | |
| 1885 | + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { | |
| 1886 | + if self.confirm_channel_edit(cx) { | |
| 1887 | + return; | |
| 1888 | + } | |
| 2061 | 1889 | |
| 2062 | − // if let Some(selection) = self.selection { | |
| 2063 | − // if let Some(entry) = self.entries.get(selection) { | |
| 2064 | − // match entry { | |
| 2065 | − // ListEntry::Header(section) => match section { | |
| 2066 | − // Section::ActiveCall => Self::leave_call(cx), | |
| 2067 | − // Section::Channels => self.new_root_channel(cx), | |
| 2068 | − // Section::Contacts => self.toggle_contact_finder(cx), | |
| 2069 | − // Section::ContactRequests | |
| 2070 | − // | Section::Online | |
| 2071 | − // | Section::Offline | |
| 2072 | − // | Section::ChannelInvites => { | |
| 2073 | − // self.toggle_section_expanded(*section, cx); | |
| 2074 | − // } | |
| 2075 | − // }, | |
| 2076 | − // ListEntry::Contact { contact, calling } => { | |
| 2077 | − // if contact.online && !contact.busy && !calling { | |
| 2078 | − // self.call(contact.user.id, Some(self.project.clone()), cx); | |
| 2079 | − // } | |
| 2080 | − // } | |
| 2081 | − // ListEntry::ParticipantProject { | |
| 2082 | − // project_id, | |
| 2083 | − // host_user_id, | |
| 2084 | − // .. | |
| 2085 | − // } => { | |
| 2086 | − // if let Some(workspace) = self.workspace.upgrade(cx) { | |
| 2087 | − // let app_state = workspace.read(cx).app_state().clone(); | |
| 2088 | − // workspace::join_remote_project( | |
| 2089 | − // *project_id, | |
| 2090 | − // *host_user_id, | |
| 2091 | − // app_state, | |
| 2092 | − // cx, | |
| 2093 | − // ) | |
| 2094 | − // .detach_and_log_err(cx); | |
| 2095 | − // } | |
| 2096 | − // } | |
| 2097 | − // ListEntry::ParticipantScreen { peer_id, .. } => { | |
| 2098 | − // let Some(peer_id) = peer_id else { | |
| 2099 | − // return; | |
| 2100 | − // }; | |
| 2101 | − // if let Some(workspace) = self.workspace.upgrade(cx) { | |
| 2102 | − // workspace.update(cx, |workspace, cx| { | |
| 2103 | − // workspace.open_shared_screen(*peer_id, cx) | |
| 2104 | − // }); | |
| 2105 | − // } | |
| 2106 | − // } | |
| 2107 | − // ListEntry::Channel { channel, .. } => { | |
| 2108 | − // let is_active = maybe!({ | |
| 2109 | − // let call_channel = ActiveCall::global(cx) | |
| 2110 | − // .read(cx) | |
| 2111 | − // .room()? | |
| 2112 | − // .read(cx) | |
| 2113 | − // .channel_id()?; | |
| 2114 | − | |
| 2115 | − // Some(call_channel == channel.id) | |
| 2116 | − // }) | |
| 2117 | − // .unwrap_or(false); | |
| 2118 | − // if is_active { | |
| 2119 | − // self.open_channel_notes( | |
| 2120 | − // &OpenChannelNotes { | |
| 2121 | − // channel_id: channel.id, | |
| 2122 | − // }, | |
| 2123 | − // cx, | |
| 2124 | − // ) | |
| 2125 | − // } else { | |
| 2126 | − // self.join_channel(channel.id, cx) | |
| 2127 | − // } | |
| 2128 | − // } | |
| 2129 | − // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), | |
| 2130 | − // _ => {} | |
| 2131 | − // } | |
| 2132 | − // } | |
| 2133 | − // } | |
| 2134 | − // } | |
| 1890 | + // if let Some(selection) = self.selection { | |
| 1891 | + // if let Some(entry) = self.entries.get(selection) { | |
| 1892 | + // match entry { | |
| 1893 | + // ListEntry::Header(section) => match section { | |
| 1894 | + // Section::ActiveCall => Self::leave_call(cx), | |
| 1895 | + // Section::Channels => self.new_root_channel(cx), | |
| 1896 | + // Section::Contacts => self.toggle_contact_finder(cx), | |
| 1897 | + // Section::ContactRequests | |
| 1898 | + // | Section::Online | |
| 1899 | + // | Section::Offline | |
| 1900 | + // | Section::ChannelInvites => { | |
| 1901 | + // self.toggle_section_expanded(*section, cx); | |
| 1902 | + // } | |
| 1903 | + // }, | |
| 1904 | + // ListEntry::Contact { contact, calling } => { | |
| 1905 | + // if contact.online && !contact.busy && !calling { | |
| 1906 | + // self.call(contact.user.id, Some(self.project.clone()), cx); | |
| 1907 | + // } | |
| 1908 | + // } | |
| 1909 | + // ListEntry::ParticipantProject { | |
| 1910 | + // project_id, | |
| 1911 | + // host_user_id, | |
| 1912 | + // .. | |
| 1913 | + // } => { | |
| 1914 | + // if let Some(workspace) = self.workspace.upgrade(cx) { | |
| 1915 | + // let app_state = workspace.read(cx).app_state().clone(); | |
| 1916 | + // workspace::join_remote_project( | |
| 1917 | + // *project_id, | |
| 1918 | + // *host_user_id, | |
| 1919 | + // app_state, | |
| 1920 | + // cx, | |
| 1921 | + // ) | |
| 1922 | + // .detach_and_log_err(cx); | |
| 1923 | + // } | |
| 1924 | + // } | |
| 1925 | + // ListEntry::ParticipantScreen { peer_id, .. } => { | |
| 1926 | + // let Some(peer_id) = peer_id else { | |
| 1927 | + // return; | |
| 1928 | + // }; | |
| 1929 | + // if let Some(workspace) = self.workspace.upgrade(cx) { | |
| 1930 | + // workspace.update(cx, |workspace, cx| { | |
| 1931 | + // workspace.open_shared_screen(*peer_id, cx) | |
| 1932 | + // }); | |
| 1933 | + // } | |
| 1934 | + // } | |
| 1935 | + // ListEntry::Channel { channel, .. } => { | |
| 1936 | + // let is_active = maybe!({ | |
| 1937 | + // let call_channel = ActiveCall::global(cx) | |
| 1938 | + // .read(cx) | |
| 1939 | + // .room()? | |
| 1940 | + // .read(cx) | |
| 1941 | + // .channel_id()?; | |
| 1942 | + | |
| 1943 | + // Some(call_channel == channel.id) | |
| 1944 | + // }) | |
| 1945 | + // .unwrap_or(false); | |
| 1946 | + // if is_active { | |
| 1947 | + // self.open_channel_notes( | |
| 1948 | + // &OpenChannelNotes { | |
| 1949 | + // channel_id: channel.id, | |
| 1950 | + // }, | |
| 1951 | + // cx, | |
| 1952 | + // ) | |
| 1953 | + // } else { | |
| 1954 | + // self.join_channel(channel.id, cx) | |
| 1955 | + // } | |
| 1956 | + // } | |
| 1957 | + // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), | |
| 1958 | + // _ => {} | |
| 1959 | + // } | |
| 1960 | + // } | |
| 1961 | + // } | |
| 1962 | + } | |
| 2135 | 1963 | |
| 2136 | − // fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) { | |
| 2137 | − // if self.channel_editing_state.is_some() { | |
| 2138 | − // self.channel_name_editor.update(cx, |editor, cx| { | |
| 2139 | − // editor.insert(" ", cx); | |
| 2140 | − // }); | |
| 2141 | − // } | |
| 2142 | − // } | |
| 1964 | + fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) { | |
| 1965 | + if self.channel_editing_state.is_some() { | |
| 1966 | + self.channel_name_editor.update(cx, |editor, cx| { | |
| 1967 | + editor.insert(" ", cx); | |
| 1968 | + }); | |
| 1969 | + } | |
| 1970 | + } | |
| 2143 | 1971 | |
| 2144 | − // fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool { | |
| 2145 | − // if let Some(editing_state) = &mut self.channel_editing_state { | |
| 2146 | − // match editing_state { | |
| 2147 | − // ChannelEditingState::Create { | |
| 2148 | − // location, | |
| 2149 | − // pending_name, | |
| 2150 | − // .. | |
| 2151 | − // } => { | |
| 2152 | − // if pending_name.is_some() { | |
| 2153 | − // return false; | |
| 2154 | − // } | |
| 2155 | − // let channel_name = self.channel_name_editor.read(cx).text(cx); | |
| 1972 | + fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool { | |
| 1973 | + if let Some(editing_state) = &mut self.channel_editing_state { | |
| 1974 | + match editing_state { | |
| 1975 | + ChannelEditingState::Create { | |
| 1976 | + location, | |
| 1977 | + pending_name, | |
| 1978 | + .. | |
| 1979 | + } => { | |
| 1980 | + if pending_name.is_some() { | |
| 1981 | + return false; | |
| 1982 | + } | |
| 1983 | + let channel_name = self.channel_name_editor.read(cx).text(cx); | |
| 2156 | 1984 | |
| 2157 | − // *pending_name = Some(channel_name.clone()); | |
| 1985 | + *pending_name = Some(channel_name.clone()); | |
| 2158 | 1986 | |
| 2159 | − // self.channel_store | |
| 2160 | − // .update(cx, |channel_store, cx| { | |
| 2161 | − // channel_store.create_channel(&channel_name, *location, cx) | |
| 2162 | − // }) | |
| 2163 | − // .detach(); | |
| 2164 | − // cx.notify(); | |
| 2165 | − // } | |
| 2166 | − // ChannelEditingState::Rename { | |
| 2167 | − // location, | |
| 2168 | − // pending_name, | |
| 2169 | − // } => { | |
| 2170 | − // if pending_name.is_some() { | |
| 2171 | − // return false; | |
| 2172 | − // } | |
| 2173 | − // let channel_name = self.channel_name_editor.read(cx).text(cx); | |
| 2174 | − // *pending_name = Some(channel_name.clone()); | |
| 2175 | − | |
| 2176 | − // self.channel_store | |
| 2177 | − // .update(cx, |channel_store, cx| { | |
| 2178 | − // channel_store.rename(*location, &channel_name, cx) | |
| 2179 | − // }) | |
| 2180 | − // .detach(); | |
| 2181 | − // cx.notify(); | |
| 2182 | − // } | |
| 2183 | − // } | |
| 2184 | − // cx.focus_self(); | |
| 2185 | − // true | |
| 2186 | − // } else { | |
| 2187 | − // false | |
| 2188 | − // } | |
| 2189 | − // } | |
| 1987 | + self.channel_store | |
| 1988 | + .update(cx, |channel_store, cx| { | |
| 1989 | + channel_store.create_channel(&channel_name, *location, cx) | |
| 1990 | + }) | |
| 1991 | + .detach(); | |
| 1992 | + cx.notify(); | |
| 1993 | + } | |
| 1994 | + ChannelEditingState::Rename { | |
| 1995 | + location, | |
| 1996 | + pending_name, | |
| 1997 | + } => { | |
| 1998 | + if pending_name.is_some() { | |
| 1999 | + return false; | |
| 2000 | + } | |
| 2001 | + let channel_name = self.channel_name_editor.read(cx).text(cx); | |
| 2002 | + *pending_name = Some(channel_name.clone()); | |
| 2003 | + | |
| 2004 | + self.channel_store | |
| 2005 | + .update(cx, |channel_store, cx| { | |
| 2006 | + channel_store.rename(*location, &channel_name, cx) | |
| 2007 | + }) | |
| 2008 | + .detach(); | |
| 2009 | + cx.notify(); | |
| 2010 | + } | |
| 2011 | + } | |
| 2012 | + cx.focus_self(); | |
| 2013 | + true | |
| 2014 | + } else { | |
| 2015 | + false | |
| 2016 | + } | |
| 2017 | + } | |
| 2190 | 2018 | |
| 2191 | − // fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) { | |
| 2192 | − // if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { | |
| 2193 | − // self.collapsed_sections.remove(ix); | |
| 2194 | − // } else { | |
| 2195 | − // self.collapsed_sections.push(section); | |
| 2196 | − // } | |
| 2197 | − // self.update_entries(false, cx); | |
| 2198 | − // } | |
| 2019 | + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) { | |
| 2020 | + if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { | |
| 2021 | + self.collapsed_sections.remove(ix); | |
| 2022 | + } else { | |
| 2023 | + self.collapsed_sections.push(section); | |
| 2024 | + } | |
| 2025 | + self.update_entries(false, cx); | |
| 2026 | + } | |
| 2199 | 2027 | |
| 2200 | 2028 | // fn collapse_selected_channel( |
| 2201 | 2029 | // &mut self, |
| @@ -2233,20 +2061,20 @@ impl CollabPanel { · 31 unmodified lines | ||
| 2233 | 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>) { | |
| 2237 | − // match self.collapsed_channels.binary_search(&channel_id) { | |
| 2238 | − // Ok(ix) => { | |
| 2239 | − // self.collapsed_channels.remove(ix); | |
| 2240 | − // } | |
| 2241 | − // Err(ix) => { | |
| 2242 | − // self.collapsed_channels.insert(ix, channel_id); | |
| 2243 | − // } | |
| 2244 | − // }; | |
| 2245 | − // self.serialize(cx); | |
| 2246 | − // self.update_entries(true, cx); | |
| 2247 | − // cx.notify(); | |
| 2248 | − // cx.focus_self(); | |
| 2249 | − // } | |
| 2064 | + fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) { | |
| 2065 | + match self.collapsed_channels.binary_search(&channel_id) { | |
| 2066 | + Ok(ix) => { | |
| 2067 | + self.collapsed_channels.remove(ix); | |
| 2068 | + } | |
| 2069 | + Err(ix) => { | |
| 2070 | + self.collapsed_channels.insert(ix, channel_id); | |
| 2071 | + } | |
| 2072 | + }; | |
| 2073 | + self.serialize(cx); | |
| 2074 | + self.update_entries(true, cx); | |
| 2075 | + cx.notify(); | |
| 2076 | + cx.focus_self(); | |
| 2077 | + } | |
| 2250 | 2078 | |
| 2251 | 2079 | fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { |
| 2252 | 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>) { | |
| 2274 | − // self.channel_editing_state = Some(ChannelEditingState::Create { | |
| 2275 | − // location: None, | |
| 2276 | − // pending_name: None, | |
| 2277 | − // }); | |
| 2278 | − // self.update_entries(false, cx); | |
| 2279 | − // self.select_channel_editor(); | |
| 2280 | − // cx.focus(self.channel_name_editor.as_any()); | |
| 2281 | − // cx.notify(); | |
| 2282 | − // } | |
| 2101 | + fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) { | |
| 2102 | + self.channel_editing_state = Some(ChannelEditingState::Create { | |
| 2103 | + location: None, | |
| 2104 | + pending_name: None, | |
| 2105 | + }); | |
| 2106 | + self.update_entries(false, cx); | |
| 2107 | + self.select_channel_editor(); | |
| 2108 | + cx.focus_view(&self.channel_name_editor); | |
| 2109 | + cx.notify(); | |
| 2110 | + } | |
| 2283 | 2111 | |
| 2284 | − // fn select_channel_editor(&mut self) { | |
| 2285 | − // self.selection = self.entries.iter().position(|entry| match entry { | |
| 2286 | − // ListEntry::ChannelEditor { .. } => true, | |
| 2287 | − // _ => false, | |
| 2288 | − // }); | |
| 2289 | − // } | |
| 2112 | + fn select_channel_editor(&mut self) { | |
| 2113 | + self.selection = self.entries.iter().position(|entry| match entry { | |
| 2114 | + ListEntry::ChannelEditor { .. } => true, | |
| 2115 | + _ => false, | |
| 2116 | + }); | |
| 2117 | + } | |
| 2290 | 2118 | |
| 2291 | 2119 | // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) { |
| 2292 | 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>) { | |
| 2350 | − // if let Some(workspace) = self.workspace.upgrade(cx) { | |
| 2351 | − // ChannelView::open(action.channel_id, workspace, cx).detach(); | |
| 2352 | − // } | |
| 2353 | − // } | |
| 2177 | + fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) { | |
| 2178 | + if let Some(workspace) = self.workspace.upgrade() { | |
| 2179 | + todo!(); | |
| 2180 | + // ChannelView::open(action.channel_id, workspace, cx).detach(); | |
| 2181 | + } | |
| 2182 | + } | |
| 2354 | 2183 | |
| 2355 | 2184 | // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) { |
| 2356 | 2185 | // let Some(channel) = self.selected_channel() else { |
| @@ -2439,44 +2268,38 @@ impl CollabPanel { · 82 unmodified lines | ||
| 2439 | 2268 | // // Should move to the filter editor if clicking on it |
| 2440 | 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>) { | |
| 2443 | − // let user_store = self.user_store.clone(); | |
| 2444 | − // let prompt_message = format!( | |
| 2445 | − // "Are you sure you want to remove \"{}\" from your contacts?", | |
| 2446 | − // github_login | |
| 2447 | − // ); | |
| 2448 | − // let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); | |
| 2449 | − // let window = cx.window(); | |
| 2450 | − // cx.spawn(|_, mut cx| async move { | |
| 2451 | − // if answer.next().await == Some(0) { | |
| 2452 | − // if let Err(e) = user_store | |
| 2453 | − // .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) | |
| 2454 | − // .await | |
| 2455 | − // { | |
| 2456 | − // window.prompt( | |
| 2457 | − // PromptLevel::Info, | |
| 2458 | − // &format!("Failed to remove contact: {}", e), | |
| 2459 | − // &["Ok"], | |
| 2460 | − // &mut cx, | |
| 2461 | − // ); | |
| 2462 | − // } | |
| 2463 | − // } | |
| 2464 | − // }) | |
| 2465 | − // .detach(); | |
| 2466 | − // } | |
| 2271 | + fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) { | |
| 2272 | + let user_store = self.user_store.clone(); | |
| 2273 | + let prompt_message = format!( | |
| 2274 | + "Are you sure you want to remove \"{}\" from your contacts?", | |
| 2275 | + github_login | |
| 2276 | + ); | |
| 2277 | + let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); | |
| 2278 | + let window = cx.window(); | |
| 2279 | + cx.spawn(|_, mut cx| async move { | |
| 2280 | + if answer.await? == 0 { | |
| 2281 | + user_store | |
| 2282 | + .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))? | |
| 2283 | + .await | |
| 2284 | + .notify_async_err(&mut cx); | |
| 2285 | + } | |
| 2286 | + anyhow::Ok(()) | |
| 2287 | + }) | |
| 2288 | + .detach_and_log_err(cx); | |
| 2289 | + } | |
| 2467 | 2290 | |
| 2468 | − // fn respond_to_contact_request( | |
| 2469 | − // &mut self, | |
| 2470 | − // user_id: u64, | |
| 2471 | − // accept: bool, | |
| 2472 | − // cx: &mut ViewContext<Self>, | |
| 2473 | − // ) { | |
| 2474 | − // self.user_store | |
| 2475 | − // .update(cx, |store, cx| { | |
| 2476 | − // store.respond_to_contact_request(user_id, accept, cx) | |
| 2477 | − // }) | |
| 2478 | − // .detach(); | |
| 2479 | − // } | |
| 2291 | + fn respond_to_contact_request( | |
| 2292 | + &mut self, | |
| 2293 | + user_id: u64, | |
| 2294 | + accept: bool, | |
| 2295 | + cx: &mut ViewContext<Self>, | |
| 2296 | + ) { | |
| 2297 | + self.user_store | |
| 2298 | + .update(cx, |store, cx| { | |
| 2299 | + store.respond_to_contact_request(user_id, accept, cx) | |
| 2300 | + }) | |
| 2301 | + .detach_and_log_err(cx); | |
| 2302 | + } | |
| 2480 | 2303 | |
| 2481 | 2304 | // fn respond_to_channel_invite( |
| 2482 | 2305 | // &mut self, |
| @@ -2504,21 +2327,22 @@ impl CollabPanel { · 21 unmodified lines | ||
| 2504 | 2327 | // .detach_and_log_err(cx); |
| 2505 | 2328 | // } |
| 2506 | 2329 | |
| 2507 | − // fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) { | |
| 2508 | − // let Some(workspace) = self.workspace.upgrade(cx) else { | |
| 2509 | − // return; | |
| 2510 | − // }; | |
| 2511 | − // let Some(handle) = cx.window().downcast::<Workspace>() else { | |
| 2512 | − // return; | |
| 2513 | − // }; | |
| 2514 | − // workspace::join_channel( | |
| 2515 | − // channel_id, | |
| 2516 | − // workspace.read(cx).app_state().clone(), | |
| 2517 | − // Some(handle), | |
| 2518 | − // cx, | |
| 2519 | − // ) | |
| 2520 | − // .detach_and_log_err(cx) | |
| 2521 | − // } | |
| 2330 | + fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) { | |
| 2331 | + let Some(handle) = cx.window_handle().downcast::<Workspace>() else { | |
| 2332 | + return; | |
| 2333 | + }; | |
| 2334 | + let active_call = ActiveCall::global(cx); | |
| 2335 | + cx.spawn(|_, mut cx| async move { | |
| 2336 | + active_call | |
| 2337 | + .update(&mut cx, |active_call, cx| { | |
| 2338 | + active_call.join_channel(channel_id, Some(handle), cx) | |
| 2339 | + }) | |
| 2340 | + .log_err()? | |
| 2341 | + .await | |
| 2342 | + .notify_async_err(&mut cx) | |
| 2343 | + }) | |
| 2344 | + .detach() | |
| 2345 | + } | |
| 2522 | 2346 | |
| 2523 | 2347 | // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) { |
| 2524 | 2348 | // let channel_id = action.channel_id; |
| @@ -2590,7 +2414,9 @@ impl CollabPanel { · 65 unmodified lines | ||
| 2590 | 2414 | } => self |
| 2591 | 2415 | .render_channel(&*channel, depth, has_children, is_selected, cx) |
| 2592 | 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 | 2516 | Some( |
| 2691 | 2517 | IconButton::new("add-channel", Icon::Plus) |
| 2692 | − .on_click(cx.listener(|this, _, cx| { | |
| 2693 | − todo!() | |
| 2694 | − // this.new_root_channel(cx) | |
| 2695 | − })) | |
| 2518 | + .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) | |
| 2696 | 2519 | .tooltip(|cx| Tooltip::text("Create a channel", cx)), |
| 2697 | 2520 | ) |
| 2698 | 2521 | } |
| @@ -2707,18 +2530,16 @@ impl CollabPanel { · 8 unmodified lines | ||
| 2707 | 2530 | | Section::Offline => true, |
| 2708 | 2531 | }; |
| 2709 | 2532 | |
| 2710 | − let mut header = ListHeader::new(text); | |
| 2711 | − if let Some(button) = button { | |
| 2712 | − header = header.right_button(button) | |
| 2713 | − } | |
| 2714 | − // todo!() is selected | |
| 2715 | − if can_collapse { | |
| 2716 | − // todo!() on click to toggle | |
| 2717 | − header = header.toggle(ui::Toggle::Toggled(is_collapsed)); | |
| 2718 | − } | |
| 2719 | − | |
| 2720 | − header | |
| 2533 | + ListHeader::new(text) | |
| 2534 | + .when_some(button, |el, button| el.right_button(button)) | |
| 2535 | + .selected(is_selected) | |
| 2536 | + .when(can_collapse, |el| { | |
| 2537 | + el.toggle(ui::Toggle::Toggled(is_collapsed)).on_toggle( | |
| 2538 | + cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)), | |
| 2539 | + ) | |
| 2540 | + }) | |
| 2721 | 2541 | } |
| 2542 | + | |
| 2722 | 2543 | fn render_contact( |
| 2723 | 2544 | &mut self, |
| 2724 | 2545 | contact: &Contact, |
| @@ -2742,10 +2563,32 @@ impl CollabPanel { · 17 unmodified lines | ||
| 2742 | 2563 | }) |
| 2743 | 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 | 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 | 2592 | // let event_handler = |
| 2750 | 2593 | // MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| { |
| 2751 | 2594 | // Flex::row() |
| @@ -2774,40 +2617,7 @@ impl CollabPanel { · 22 unmodified lines | ||
| 2774 | 2617 | // ) |
| 2775 | 2618 | // .with_children(status_badge) |
| 2776 | 2619 | // })) |
| 2777 | − // .with_child( | |
| 2778 | − // Label::new( | |
| 2779 | − // contact.user.github_login.clone(), | |
| 2780 | − // collab_theme.contact_username.text.clone(), | |
| 2781 | − // ) | |
| 2782 | − // .contained() | |
| 2783 | − // .with_style(collab_theme.contact_username.container) | |
| 2784 | − // .aligned() | |
| 2785 | − // .left() | |
| 2786 | − // .flex(1., true), | |
| 2787 | − // ) | |
| 2788 | − // .with_children(if state.hovered() { | |
| 2789 | − // Some( | |
| 2790 | − // MouseEventHandler::new::<Cancel, _>( | |
| 2791 | − // contact.user.id as usize, | |
| 2792 | − // cx, | |
| 2793 | − // |mouse_state, _| { | |
| 2794 | − // let button_style = | |
| 2795 | − // collab_theme.contact_button.style_for(mouse_state); | |
| 2796 | − // render_icon_button(button_style, "icons/x.svg") | |
| 2797 | − // .aligned() | |
| 2798 | − // .flex_float() | |
| 2799 | − // }, | |
| 2800 | − // ) | |
| 2801 | − // .with_padding(Padding::uniform(2.)) | |
| 2802 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 2803 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2804 | − // this.remove_contact(user_id, &github_login, cx); | |
| 2805 | − // }) | |
| 2806 | − // .flex_float(), | |
| 2807 | − // ) | |
| 2808 | − // } else { | |
| 2809 | − // None | |
| 2810 | − // }) | |
| 2620 | + | |
| 2811 | 2621 | // .with_children(if calling { |
| 2812 | 2622 | // Some( |
| 2813 | 2623 | // Label::new("Calling", collab_theme.calling_indicator.text.clone()) |
| @@ -2865,8 +2675,6 @@ impl CollabPanel { · 51 unmodified lines | ||
| 2865 | 2675 | // ) |
| 2866 | 2676 | // .into_any() |
| 2867 | 2677 | // }; |
| 2868 | − | |
| 2869 | − item | |
| 2870 | 2678 | } |
| 2871 | 2679 | |
| 2872 | 2680 | fn render_contact_request( |
| @@ -2877,104 +2685,48 @@ impl CollabPanel { · 4 unmodified lines | ||
| 2877 | 2685 | cx: &mut ViewContext<Self>, |
| 2878 | 2686 | ) -> impl IntoElement { |
| 2879 | 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()) | |
| 2882 | − .child(Label::new(github_login.clone())) | |
| 2883 | − .on_click(cx.listener(|this, _, cx| { | |
| 2884 | − todo!(); | |
| 2885 | − })); | |
| 2886 | − if let Some(avatar) = user.avatar.clone() { | |
| 2887 | − item = item.left_avatar(avatar); | |
| 2888 | − } | |
| 2889 | − // .with_children(user.avatar.clone().map(|avatar| { | |
| 2890 | − // Image::from_data(avatar) | |
| 2891 | − // .with_style(theme.contact_avatar) | |
| 2892 | − // .aligned() | |
| 2893 | − // .left() | |
| 2894 | − // })) | |
| 2895 | − // .with_child( | |
| 2896 | − // Label::new( | |
| 2897 | − // user.github_login.clone(), | |
| 2898 | − // theme.contact_username.text.clone(), | |
| 2899 | − // ) | |
| 2900 | − // .contained() | |
| 2901 | − // .with_style(theme.contact_username.container) | |
| 2902 | − // .aligned() | |
| 2903 | − // .left() | |
| 2904 | − // .flex(1., true), | |
| 2905 | − // ); | |
| 2906 | − | |
| 2907 | − // let user_id = user.id; | |
| 2908 | − // let github_login = user.github_login.clone(); | |
| 2909 | − // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); | |
| 2910 | − // let button_spacing = theme.contact_button_spacing; | |
| 2911 | − | |
| 2912 | − // if is_incoming { | |
| 2913 | − // row.add_child( | |
| 2914 | − // MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| { | |
| 2915 | − // let button_style = if is_contact_request_pending { | |
| 2916 | − // &theme.disabled_button | |
| 2917 | − // } else { | |
| 2918 | − // theme.contact_button.style_for(mouse_state) | |
| 2919 | − // }; | |
| 2920 | − // render_icon_button(button_style, "icons/x.svg").aligned() | |
| 2921 | − // }) | |
| 2922 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 2923 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2924 | − // this.respond_to_contact_request(user_id, false, cx); | |
| 2925 | − // }) | |
| 2926 | − // .contained() | |
| 2927 | − // .with_margin_right(button_spacing), | |
| 2928 | − // ); | |
| 2929 | − | |
| 2930 | − // row.add_child( | |
| 2931 | − // MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| { | |
| 2932 | − // let button_style = if is_contact_request_pending { | |
| 2933 | − // &theme.disabled_button | |
| 2934 | − // } else { | |
| 2935 | − // theme.contact_button.style_for(mouse_state) | |
| 2936 | − // }; | |
| 2937 | − // render_icon_button(button_style, "icons/check.svg") | |
| 2938 | − // .aligned() | |
| 2939 | − // .flex_float() | |
| 2940 | − // }) | |
| 2941 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 2942 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2943 | − // this.respond_to_contact_request(user_id, true, cx); | |
| 2944 | − // }), | |
| 2945 | − // ); | |
| 2946 | − // } else { | |
| 2947 | − // row.add_child( | |
| 2948 | − // MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| { | |
| 2949 | − // let button_style = if is_contact_request_pending { | |
| 2950 | − // &theme.disabled_button | |
| 2951 | − // } else { | |
| 2952 | − // theme.contact_button.style_for(mouse_state) | |
| 2953 | − // }; | |
| 2954 | − // render_icon_button(button_style, "icons/x.svg") | |
| 2955 | − // .aligned() | |
| 2956 | − // .flex_float() | |
| 2957 | − // }) | |
| 2958 | − // .with_padding(Padding::uniform(2.)) | |
| 2959 | − // .with_cursor_style(CursorStyle::PointingHand) | |
| 2960 | − // .on_click(MouseButton::Left, move |_, this, cx| { | |
| 2961 | − // this.remove_contact(user_id, &github_login, cx); | |
| 2962 | − // }) | |
| 2963 | − // .flex_float(), | |
| 2964 | − // ); | |
| 2965 | − // } | |
| 2696 | + let controls = if is_incoming { | |
| 2697 | + vec![ | |
| 2698 | + IconButton::new("remove_contact", Icon::Close) | |
| 2699 | + .on_click(cx.listener(move |this, _, cx| { | |
| 2700 | + this.respond_to_contact_request(user_id, false, cx); | |
| 2701 | + })) | |
| 2702 | + .color(color) | |
| 2703 | + .tooltip(|cx| Tooltip::text("Decline invite", cx)), | |
| 2704 | + IconButton::new("remove_contact", Icon::Check) | |
| 2705 | + .on_click(cx.listener(move |this, _, cx| { | |
| 2706 | + this.respond_to_contact_request(user_id, true, cx); | |
| 2707 | + })) | |
| 2708 | + .color(color) | |
| 2709 | + .tooltip(|cx| Tooltip::text("Accept invite", cx)), | |
| 2710 | + ] | |
| 2711 | + } else { | |
| 2712 | + let github_login = github_login.clone(); | |
| 2713 | + vec![IconButton::new("remove_contact", Icon::Close) | |
| 2714 | + .on_click(cx.listener(move |this, _, cx| { | |
| 2715 | + this.remove_contact(user_id, &github_login, cx); | |
| 2716 | + })) | |
| 2717 | + .color(color) | |
| 2718 | + .tooltip(|cx| Tooltip::text("Cancel invite", cx))] | |
| 2719 | + }; | |
| 2966 | 2720 | |
| 2967 | − // row.constrained() | |
| 2968 | − // .with_height(theme.row_height) | |
| 2969 | − // .contained() | |
| 2970 | − // .with_style( | |
| 2971 | − // *theme | |
| 2972 | − // .contact_row | |
| 2973 | − // .in_state(is_selected) | |
| 2974 | − // .style_for(&mut Default::default()), | |
| 2975 | − // ) | |
| 2976 | − // .into_any() | |
| 2977 | − item | |
| 2721 | + ListItem::new(github_login.clone()) | |
| 2722 | + .child( | |
| 2723 | + h_stack() | |
| 2724 | + .w_full() | |
| 2725 | + .justify_between() | |
| 2726 | + .child(Label::new(github_login.clone())) | |
| 2727 | + .child(h_stack().children(controls)), | |
| 2728 | + ) | |
| 2729 | + .when_some(user.avatar.clone(), |el, avatar| el.left_avatar(avatar)) | |
| 2978 | 2730 | } |
| 2979 | 2731 | |
| 2980 | 2732 | fn render_contact_placeholder( |
| @@ -2983,34 +2735,10 @@ impl CollabPanel { · 2 unmodified lines | ||
| 2983 | 2735 | cx: &mut ViewContext<Self>, |
| 2984 | 2736 | ) -> impl IntoElement { |
| 2985 | 2737 | ListItem::new("contact-placeholder") |
| 2738 | + .child(IconElement::new(Icon::Plus)) | |
| 2986 | 2739 | .child(Label::new("Add a Contact")) |
| 2987 | − .on_click(cx.listener(|this, _, cx| todo!())) | |
| 2988 | − // enum AddContacts {} | |
| 2989 | − // MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| { | |
| 2990 | − // let style = theme.list_empty_state.style_for(is_selected, state); | |
| 2991 | − // Flex::row() | |
| 2992 | − // .with_child( | |
| 2993 | − // Svg::new("icons/plus.svg") | |
| 2994 | − // .with_color(theme.list_empty_icon.color) | |
| 2995 | − // .constrained() | |
| 2996 | − // .with_width(theme.list_empty_icon.width) | |
| 2997 | − // .aligned() | |
| 2998 | − // .left(), | |
| 2999 | − // ) | |
| 3000 | − // .with_child( | |
| 3001 | − // Label::new("Add a contact", style.text.clone()) | |
| 3002 | − // .contained() | |
| 3003 | − // .with_style(theme.list_empty_label_container), | |
| 3004 | − // ) | |
| 3005 | − // .align_children_center() | |
| 3006 | − // .contained() | |
| 3007 | − // .with_style(style.container) | |
| 3008 | − // .into_any() | |
| 3009 | − // }) | |
| 3010 | − // .on_click(MouseButton::Left, |_, this, cx| { | |
| 3011 | − // this.toggle_contact_finder(cx); | |
| 3012 | − // }) | |
| 3013 | − // .into_any() | |
| 2740 | + .selected(is_selected) | |
| 2741 | + .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) | |
| 3014 | 2742 | } |
| 3015 | 2743 | |
| 3016 | 2744 | fn render_channel( |
| @@ -3023,6 +2751,15 @@ impl CollabPanel { · 6 unmodified lines | ||
| 3023 | 2751 | ) -> impl IntoElement { |
| 3024 | 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 | 2763 | let is_public = self |
| 3027 | 2764 | .channel_store |
| 3028 | 2765 | .read(cx) |
| @@ -3034,17 +2771,7 @@ impl CollabPanel { · 5 unmodified lines | ||
| 3034 | 2771 | .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()) |
| 3035 | 2772 | .unwrap_or(false); |
| 3036 | 2773 | |
| 3037 | − let is_active = maybe!({ | |
| 3038 | − let call_channel = ActiveCall::global(cx) | |
| 3039 | − .read(cx) | |
| 3040 | − .room()? | |
| 3041 | − .read(cx) | |
| 3042 | − .channel_id()?; | |
| 3043 | − Some(call_channel == channel_id) | |
| 3044 | − }) | |
| 3045 | − .unwrap_or(false); | |
| 3046 | − | |
| 3047 | − let has_messages_notification = channel.unseen_message_id.is_some() || true; | |
| 2774 | + let has_messages_notification = channel.unseen_message_id.is_some(); | |
| 3048 | 2775 | let has_notes_notification = channel.unseen_note_version.is_some(); |
| 3049 | 2776 | |
| 3050 | 2777 | const FACEPILE_LIMIT: usize = 3; |
| @@ -3052,6 +2779,7 @@ impl CollabPanel { · 1 unmodified line | ||
| 3052 | 2779 | |
| 3053 | 2780 | let face_pile = if !participants.is_empty() { |
| 3054 | 2781 | let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); |
| 2782 | + let user = &participants[0]; | |
| 3055 | 2783 | |
| 3056 | 2784 | let result = FacePile { |
| 3057 | 2785 | faces: participants |
| @@ -3059,6 +2787,7 @@ impl CollabPanel { · 1 unmodified line | ||
| 3059 | 2787 | .filter_map(|user| Some(Avatar::data(user.avatar.clone()?).into_any_element())) |
| 3060 | 2788 | .take(FACEPILE_LIMIT) |
| 3061 | 2789 | .chain(if extra_count > 0 { |
| 2790 | + // todo!() @nate - this label looks wrong. | |
| 3062 | 2791 | Some(Label::new(format!("+{}", extra_count)).into_any_element()) |
| 3063 | 2792 | } else { |
| 3064 | 2793 | None |
| @@ -3081,7 +2810,7 @@ impl CollabPanel { · 16 unmodified lines | ||
| 3081 | 2810 | .w_full() |
| 3082 | 2811 | .justify_between() |
| 3083 | 2812 | .child( |
| 3084 | − div() | |
| 2813 | + h_stack() | |
| 3085 | 2814 | .id(channel_id as usize) |
| 3086 | 2815 | .child(Label::new(channel.name.clone())) |
| 3087 | 2816 | .children(face_pile.map(|face_pile| face_pile.render(cx))) |
| @@ -3092,11 +2821,10 @@ impl CollabPanel { · 4 unmodified lines | ||
| 3092 | 2821 | .child( |
| 3093 | 2822 | div() |
| 3094 | 2823 | .id("channel_chat") |
| 3095 | − .bg(gpui::blue()) | |
| 3096 | 2824 | .when(!has_messages_notification, |el| el.invisible()) |
| 3097 | 2825 | .group_hover("", |style| style.visible()) |
| 3098 | 2826 | .child( |
| 3099 | − IconButton::new("test_chat", Icon::MessageBubbles) | |
| 2827 | + IconButton::new("channel_chat", Icon::MessageBubbles) | |
| 3100 | 2828 | .color(if has_messages_notification { |
| 3101 | 2829 | Color::Default |
| 3102 | 2830 | } else { |
| @@ -3111,20 +2839,16 @@ impl CollabPanel { · 8 unmodified lines | ||
| 3111 | 2839 | .when(!has_notes_notification, |el| el.invisible()) |
| 3112 | 2840 | .group_hover("", |style| style.visible()) |
| 3113 | 2841 | .child( |
| 3114 | − div().child("Notes").id("test_notes").tooltip(|cx| { | |
| 3115 | − Tooltip::text("Open channel notes", cx) | |
| 3116 | − }), | |
| 3117 | − ), // .child( | |
| 3118 | − // IconButton::new("channel_notes", Icon::File) | |
| 3119 | − // .color(if has_notes_notification { | |
| 3120 | − // Color::Default | |
| 3121 | − // } else { | |
| 3122 | − // Color::Muted | |
| 3123 | − // }) | |
| 3124 | − // .tooltip(|cx| { | |
| 3125 | − // Tooltip::text("Open channel notes", cx) | |
| 3126 | − // }), | |
| 3127 | − // ), | |
| 2842 | + IconButton::new("channel_notes", Icon::File) | |
| 2843 | + .color(if has_notes_notification { | |
| 2844 | + Color::Default | |
| 2845 | + } else { | |
| 2846 | + Color::Muted | |
| 2847 | + }) | |
| 2848 | + .tooltip(|cx| { | |
| 2849 | + Tooltip::text("Open channel notes", cx) | |
| 2850 | + }), | |
| 2851 | + ), | |
| 3128 | 2852 | ), |
| 3129 | 2853 | ), |
| 3130 | 2854 | ) |
| @@ -3133,7 +2857,18 @@ impl CollabPanel { · 2 unmodified lines | ||
| 3133 | 2857 | } else { |
| 3134 | 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 | 2872 | .on_secondary_mouse_down(cx.listener(|this, _, cx| { |
| 3138 | 2873 | todo!() // open context menu |
| 3139 | 2874 | })), |
| @@ -3445,6 +3180,32 @@ impl CollabPanel { · 305 unmodified lines | ||
| 3445 | 3180 | // .with_cursor_style(CursorStyle::PointingHand) |
| 3446 | 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 | 3211 | // fn render_tree_branch( |
| @@ -3499,6 +3260,8 @@ impl Render for CollabPanel { · 48 unmodified lines | ||
| 3499 | 3260 | div() |
| 3500 | 3261 | .key_context("CollabPanel") |
| 3501 | 3262 | .track_focus(&self.focus_handle) |
| 3263 | + .on_action(cx.listener(Self::confirm)) | |
| 3264 | + .on_action(cx.listener(Self::insert_space)) | |
| 3502 | 3265 | .map(|el| { |
| 3503 | 3266 | if self.user_store.read(cx).current_user().is_none() { |
| 3504 | 3267 | el.child(self.render_signed_out(cx)) |
| @@ -181,7 +181,6 @@ impl PickerDelegate for ContactFinderDelegate { · 180 unmodified lines | ||
| 181 | 181 | ContactRequestStatus::RequestSent => Some("icons/x.svg"), |
| 182 | 182 | ContactRequestStatus::RequestAccepted => None, |
| 183 | 183 | }; |
| 184 | − dbg!(icon_path); | |
| 185 | 184 | Some( |
| 186 | 185 | div() |
| 187 | 186 | .flex_1() |
| @@ -37,7 +37,10 @@ use gpui::{ · 36 unmodified lines | ||
| 37 | 37 | }; |
| 38 | 38 | use project::Project; |
| 39 | 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 | 44 | use util::ResultExt; |
| 42 | 45 | use workspace::{notifications::NotifyResultExt, Workspace}; |
| 43 | 46 | |
| @@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem { · 254 unmodified lines | ||
| 298 | 301 | }) |
| 299 | 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 | 6 | use collections::{CommandPaletteFilter, HashMap}; |
| 2 | 7 | use fuzzy::{StringMatch, StringMatchCandidate}; |
| 3 | 8 | use gpui::{ |
| @@ -5,10 +10,7 @@ use gpui::{ · 1 unmodified line | ||
| 5 | 10 | Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, |
| 6 | 11 | }; |
| 7 | 12 | use picker::{Picker, PickerDelegate}; |
| 8 | −use std::{ | |
| 9 | − cmp::{self, Reverse}, | |
| 10 | − sync::Arc, | |
| 11 | −}; | |
| 13 | + | |
| 12 | 14 | use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; |
| 13 | 15 | use util::{ |
| 14 | 16 | channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, |
| @@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> { · 29 unmodified lines | ||
| 30 | 30 | |
| 31 | 31 | impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V> |
| 32 | 32 | where |
| 33 | − V: 'static + Send + Sync, | |
| 33 | + V: 'static, | |
| 34 | 34 | { |
| 35 | 35 | fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription |
| 36 | 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 | 39 | self.observe_global::<FeatureFlags>(move |v, cx| { |
| 40 | 40 | let feature_flags = cx.global::<FeatureFlags>(); |
| @@ -162,6 +162,7 @@ macro_rules! actions { · 161 unmodified lines | ||
| 162 | 162 | |
| 163 | 163 | ( $name:ident ) => { |
| 164 | 164 | #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)] |
| 165 | + #[serde(crate = "gpui::serde")] | |
| 165 | 166 | pub struct $name; |
| 166 | 167 | }; |
| 167 | 168 | |
| @@ -111,7 +111,7 @@ pub struct Component<C> { · 110 unmodified lines | ||
| 111 | 111 | |
| 112 | 112 | pub struct CompositeElementState<C: RenderOnce> { |
| 113 | 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 | 117 | impl<C> Component<C> { |
| @@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> { · 13 unmodified lines | ||
| 131 | 131 | cx: &mut WindowContext, |
| 132 | 132 | ) -> (LayoutId, Self::State) { |
| 133 | 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); | |
| 135 | − let state = CompositeElementState { | |
| 136 | − rendered_element: Some(element), | |
| 137 | − rendered_element_state: state, | |
| 138 | − }; | |
| 139 | − (layout_id, state) | |
| 134 | + if let Some(element_id) = element.element_id() { | |
| 135 | + let layout_id = | |
| 136 | + cx.with_element_state(element_id, |state, cx| element.layout(state, cx)); | |
| 137 | + let state = CompositeElementState { | |
| 138 | + rendered_element: Some(element), | |
| 139 | + rendered_element_state: None, | |
| 140 | + }; | |
| 141 | + (layout_id, state) | |
| 142 | + } else { | |
| 143 | + let (layout_id, state) = | |
| 144 | + element.layout(state.and_then(|s| s.rendered_element_state), cx); | |
| 145 | + let state = CompositeElementState { | |
| 146 | + rendered_element: Some(element), | |
| 147 | + rendered_element_state: Some(state), | |
| 148 | + }; | |
| 149 | + (layout_id, state) | |
| 150 | + } | |
| 140 | 151 | } |
| 141 | 152 | |
| 142 | 153 | fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { |
| 143 | − state | |
| 144 | − .rendered_element | |
| 145 | − .take() | |
| 146 | − .unwrap() | |
| 147 | − .paint(bounds, &mut state.rendered_element_state, cx); | |
| 154 | + let element = state.rendered_element.take().unwrap(); | |
| 155 | + if let Some(element_id) = element.element_id() { | |
| 156 | + cx.with_element_state(element_id, |element_state, cx| { | |
| 157 | + let mut element_state = element_state.unwrap(); | |
| 158 | + element.paint(bounds, &mut element_state, cx); | |
| 159 | + ((), element_state) | |
| 160 | + }); | |
| 161 | + } else { | |
| 162 | + element.paint( | |
| 163 | + bounds, | |
| 164 | + &mut state.rendered_element_state.as_mut().unwrap(), | |
| 165 | + cx, | |
| 166 | + ); | |
| 167 | + } | |
| 148 | 168 | } |
| 149 | 169 | } |
| 150 | 170 | |
| @@ -173,7 +173,7 @@ impl Element for UniformList { · 172 unmodified lines | ||
| 173 | 173 | let item_size = element_state.item_size; |
| 174 | 174 | let content_size = Size { |
| 175 | 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 | 179 | let shared_scroll_offset = element_state |
| @@ -221,9 +221,7 @@ impl Element for UniformList { · 41 unmodified lines | ||
| 221 | 221 | |
| 222 | 222 | let items = (self.render_items)(visible_range.clone(), cx); |
| 223 | 223 | cx.with_z_index(1, |cx| { |
| 224 | − let content_mask = ContentMask { | |
| 225 | − bounds: padded_bounds, | |
| 226 | − }; | |
| 224 | + let content_mask = ContentMask { bounds }; | |
| 227 | 225 | cx.with_content_mask(Some(content_mask), |cx| { |
| 228 | 226 | for (item, ix) in items.into_iter().zip(visible_range) { |
| 229 | 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 | 1942 | /// Obtain the current content mask. |
| 1960 | 1943 | fn content_mask(&self) -> ContentMask<Pixels> { |
| 1961 | 1944 | self.window() |
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | use editor::Editor; |
| 2 | 2 | use gpui::{ |
| 3 | − div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, | |
| 4 | − MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, | |
| 3 | + div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView, | |
| 4 | + MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, | |
| 5 | + WindowContext, | |
| 5 | 6 | }; |
| 6 | 7 | use std::{cmp, sync::Arc}; |
| 7 | 8 | use ui::{prelude::*, v_stack, Color, Divider, Label}; |
| @@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> { · 8 unmodified lines | ||
| 16 | 17 | |
| 17 | 18 | pub trait PickerDelegate: Sized + 'static { |
| 18 | 19 | type ListItem: IntoElement; |
| 19 | − | |
| 20 | 20 | fn match_count(&self) -> usize; |
| 21 | 21 | fn selected_index(&self) -> usize; |
| 22 | 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 | 205 | .when(self.delegate.match_count() > 0, |el| { |
| 206 | 206 | el.child( |
| 207 | 207 | v_stack() |
| 208 | − .p_1() | |
| 209 | 208 | .grow() |
| 210 | 209 | .child( |
| 211 | 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 | 244 | .max_h_72() |
| 245 | 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 | 13 | use serde::{Deserialize, Serialize}; |
| 14 | 14 | use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; |
| 15 | 15 | |
| 16 | +#[derive(Clone)] | |
| 16 | 17 | pub enum Prettier { |
| 17 | 18 | Real(RealPrettier), |
| 18 | 19 | #[cfg(any(test, feature = "test-support"))] |
| 19 | 20 | Test(TestPrettier), |
| 20 | 21 | } |
| 21 | 22 | |
| 23 | +#[derive(Clone)] | |
| 22 | 24 | pub struct RealPrettier { |
| 23 | 25 | default: bool, |
| 24 | 26 | prettier_dir: PathBuf, |
| @@ -26,11 +28,13 @@ pub struct RealPrettier { · 1 unmodified line | ||
| 26 | 28 | } |
| 27 | 29 | |
| 28 | 30 | #[cfg(any(test, feature = "test-support"))] |
| 31 | +#[derive(Clone)] | |
| 29 | 32 | pub struct TestPrettier { |
| 30 | 33 | prettier_dir: PathBuf, |
| 31 | 34 | default: bool, |
| 32 | 35 | } |
| 33 | 36 | |
| 37 | +pub const FAIL_THRESHOLD: usize = 4; | |
| 34 | 38 | pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; |
| 35 | 39 | pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); |
| 36 | 40 | const PRETTIER_PACKAGE_NAME: &str = "prettier"; |
| @@ -153,7 +153,10 @@ async function handleMessage(message, prettier) { · 152 unmodified lines | ||
| 153 | 153 | const { method, id, params } = message; |
| 154 | 154 | if (method === undefined) { |
| 155 | 155 | throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); |
| 156 | + } else if (method == "initialized") { | |
| 157 | + return; | |
| 156 | 158 | } |
| 159 | + | |
| 157 | 160 | if (id === undefined) { |
| 158 | 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 | 14 | use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR}; |
| 15 | 15 | |
| 16 | +#[derive(Clone)] | |
| 16 | 17 | pub enum Prettier { |
| 17 | 18 | Real(RealPrettier), |
| 18 | 19 | #[cfg(any(test, feature = "test-support"))] |
| 19 | 20 | Test(TestPrettier), |
| 20 | 21 | } |
| 21 | 22 | |
| 23 | +#[derive(Clone)] | |
| 22 | 24 | pub struct RealPrettier { |
| 23 | 25 | default: bool, |
| 24 | 26 | prettier_dir: PathBuf, |
| @@ -26,11 +28,13 @@ pub struct RealPrettier { · 1 unmodified line | ||
| 26 | 28 | } |
| 27 | 29 | |
| 28 | 30 | #[cfg(any(test, feature = "test-support"))] |
| 31 | +#[derive(Clone)] | |
| 29 | 32 | pub struct TestPrettier { |
| 30 | 33 | prettier_dir: PathBuf, |
| 31 | 34 | default: bool, |
| 32 | 35 | } |
| 33 | 36 | |
| 37 | +pub const FAIL_THRESHOLD: usize = 4; | |
| 34 | 38 | pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; |
| 35 | 39 | pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); |
| 36 | 40 | const PRETTIER_PACKAGE_NAME: &str = "prettier"; |
| @@ -153,7 +153,10 @@ async function handleMessage(message, prettier) { · 152 unmodified lines | ||
| 153 | 153 | const { method, id, params } = message; |
| 154 | 154 | if (method === undefined) { |
| 155 | 155 | throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); |
| 156 | + } else if (method == "initialized") { | |
| 157 | + return; | |
| 156 | 158 | } |
| 159 | + | |
| 157 | 160 | if (id === undefined) { |
| 158 | 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 | 1 | mod ignore; |
| 2 | 2 | mod lsp_command; |
| 3 | +mod prettier_support; | |
| 3 | 4 | pub mod project_settings; |
| 4 | 5 | pub mod search; |
| 5 | 6 | pub mod terminals; |
| @@ -20,7 +21,7 @@ use futures::{ · 14 unmodified lines | ||
| 20 | 21 | mpsc::{self, UnboundedReceiver}, |
| 21 | 22 | oneshot, |
| 22 | 23 | }, |
| 23 | − future::{self, try_join_all, Shared}, | |
| 24 | + future::{try_join_all, Shared}, | |
| 24 | 25 | stream::FuturesUnordered, |
| 25 | 26 | AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, |
| 26 | 27 | }; |
| @@ -31,9 +32,7 @@ use gpui::{ · 4 unmodified lines | ||
| 31 | 32 | }; |
| 32 | 33 | use itertools::Itertools; |
| 33 | 34 | use language::{ |
| 34 | − language_settings::{ | |
| 35 | − language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, | |
| 36 | − }, | |
| 35 | + language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, | |
| 37 | 36 | point_to_lsp, |
| 38 | 37 | proto::{ |
| 39 | 38 | deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, |
| @@ -54,7 +53,7 @@ use lsp_command::*; · 14 unmodified lines | ||
| 54 | 53 | use node_runtime::NodeRuntime; |
| 55 | 54 | use parking_lot::Mutex; |
| 56 | 55 | use postage::watch; |
| 57 | −use prettier::Prettier; | |
| 56 | +use prettier_support::{DefaultPrettier, PrettierInstance}; | |
| 58 | 57 | use project_settings::{LspSettings, ProjectSettings}; |
| 59 | 58 | use rand::prelude::*; |
| 60 | 59 | use search::SearchQuery; |
| @@ -72,7 +71,7 @@ use std::{ · 11 unmodified lines | ||
| 72 | 71 | hash::Hash, |
| 73 | 72 | mem, |
| 74 | 73 | num::NonZeroU32, |
| 75 | − ops::{ControlFlow, Range}, | |
| 74 | + ops::Range, | |
| 76 | 75 | path::{self, Component, Path, PathBuf}, |
| 77 | 76 | process::Stdio, |
| 78 | 77 | str, |
| @@ -85,11 +84,8 @@ use std::{ · 6 unmodified lines | ||
| 85 | 84 | use terminals::Terminals; |
| 86 | 85 | use text::Anchor; |
| 87 | 86 | use util::{ |
| 88 | − debug_panic, defer, | |
| 89 | − http::HttpClient, | |
| 90 | − merge_json_value_into, | |
| 91 | − paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, | |
| 92 | − post_inc, ResultExt, TryFutureExt as _, | |
| 87 | + debug_panic, defer, http::HttpClient, merge_json_value_into, | |
| 88 | + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, | |
| 93 | 89 | }; |
| 94 | 90 | |
| 95 | 91 | pub use fs::*; |
| @@ -168,16 +164,9 @@ pub struct Project { · 72 unmodified lines | ||
| 168 | 164 | copilot_log_subscription: Option<lsp::Subscription>, |
| 169 | 165 | current_lsp_settings: HashMap<Arc<str>, LspSettings>, |
| 170 | 166 | node: Option<Arc<dyn NodeRuntime>>, |
| 171 | − default_prettier: Option<DefaultPrettier>, | |
| 167 | + default_prettier: DefaultPrettier, | |
| 172 | 168 | prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>, |
| 173 | − prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>, | |
| 174 | −} | |
| 175 | − | |
| 176 | −struct DefaultPrettier { | |
| 177 | − instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>, | |
| 178 | − installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>, | |
| 179 | − #[cfg(not(any(test, feature = "test-support")))] | |
| 180 | − installed_plugins: HashSet<&'static str>, | |
| 169 | + prettier_instances: HashMap<PathBuf, PrettierInstance>, | |
| 181 | 170 | } |
| 182 | 171 | |
| 183 | 172 | struct DelayedDebounced { |
| @@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate { · 358 unmodified lines | ||
| 542 | 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 | 542 | impl FormatTrigger { |
| 546 | 543 | fn from_proto(value: i32) -> FormatTrigger { |
| 547 | 544 | match value { |
| @@ -690,7 +687,7 @@ impl Project { · 142 unmodified lines | ||
| 690 | 687 | copilot_log_subscription: None, |
| 691 | 688 | current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(), |
| 692 | 689 | node: Some(node), |
| 693 | − default_prettier: None, | |
| 690 | + default_prettier: DefaultPrettier::default(), | |
| 694 | 691 | prettiers_per_worktree: HashMap::default(), |
| 695 | 692 | prettier_instances: HashMap::default(), |
| 696 | 693 | } |
| @@ -791,7 +788,7 @@ impl Project { · 94 unmodified lines | ||
| 791 | 788 | copilot_log_subscription: None, |
| 792 | 789 | current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(), |
| 793 | 790 | node: None, |
| 794 | − default_prettier: None, | |
| 791 | + default_prettier: DefaultPrettier::default(), | |
| 795 | 792 | prettiers_per_worktree: HashMap::default(), |
| 796 | 793 | prettier_instances: HashMap::default(), |
| 797 | 794 | }; |
| @@ -928,8 +925,19 @@ impl Project { · 130 unmodified lines | ||
| 928 | 925 | .detach(); |
| 929 | 926 | } |
| 930 | 927 | |
| 928 | + let mut prettier_plugins_by_worktree = HashMap::default(); | |
| 931 | 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 | 943 | // Start all the newly-enabled language servers. |
| @@ -2685,8 +2693,11 @@ impl Project { · 1749 unmodified lines | ||
| 2685 | 2693 | let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); |
| 2686 | 2694 | let buffer_file = File::from_dyn(buffer_file.as_ref()); |
| 2687 | 2695 | let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); |
| 2688 | − | |
| 2689 | − self.install_default_formatters(worktree, &new_language, &settings, cx); | |
| 2696 | + if let Some(prettier_plugins) = | |
| 2697 | + prettier_support::prettier_plugins_for_language(&new_language, &settings) | |
| 2698 | + { | |
| 2699 | + self.install_default_prettier(worktree, prettier_plugins, cx); | |
| 2700 | + }; | |
| 2690 | 2701 | if let Some(file) = buffer_file { |
| 2691 | 2702 | let worktree = file.worktree.clone(); |
| 2692 | 2703 | if let Some(tree) = worktree.read(cx).as_local() { |
| @@ -4073,8 +4084,6 @@ impl Project { · 1380 unmodified lines | ||
| 4073 | 4084 | |
| 4074 | 4085 | let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; |
| 4075 | 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 | 4087 | let tab_size = settings.tab_size; |
| 4079 | 4088 | |
| 4080 | 4089 | // First, format buffer's whitespace according to the settings. |
| @@ -4099,18 +4108,10 @@ impl Project { · 18 unmodified lines | ||
| 4099 | 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 | 4111 | // Apply language-specific formatting using either a language server |
| 4111 | 4112 | // or external command. |
| 4112 | 4113 | let mut format_operation = None; |
| 4113 | − match (formatter, format_on_save) { | |
| 4114 | + match (&settings.formatter, &settings.format_on_save) { | |
| 4114 | 4115 | (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} |
| 4115 | 4116 | |
| 4116 | 4117 | (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) |
| @@ -4155,46 +4156,11 @@ impl Project { · 38 unmodified lines | ||
| 4155 | 4156 | } |
| 4156 | 4157 | } |
| 4157 | 4158 | (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { |
| 4158 | − if let Some((prettier_path, prettier_task)) = project | |
| 4159 | − .update(&mut cx, |project, cx| { | |
| 4160 | − project.prettier_instance_for_buffer(buffer, cx) | |
| 4161 | − }).await { | |
| 4162 | − match prettier_task.await | |
| 4163 | − { | |
| 4164 | − Ok(prettier) => { | |
| 4165 | − let buffer_path = buffer.update(&mut cx, |buffer, cx| { | |
| 4166 | − File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) | |
| 4167 | − }); | |
| 4168 | − format_operation = Some(FormatOperation::Prettier( | |
| 4169 | − prettier | |
| 4170 | − .format(buffer, buffer_path, &cx) | |
| 4171 | − .await | |
| 4172 | − .context("formatting via prettier")?, | |
| 4173 | − )); | |
| 4174 | − } | |
| 4175 | − Err(e) => { | |
| 4176 | − project.update(&mut cx, |project, _| { | |
| 4177 | − match &prettier_path { | |
| 4178 | − Some(prettier_path) => { | |
| 4179 | − project.prettier_instances.remove(prettier_path); | |
| 4180 | − }, | |
| 4181 | − None => { | |
| 4182 | − if let Some(default_prettier) = project.default_prettier.as_mut() { | |
| 4183 | − default_prettier.instance = None; | |
| 4184 | − } | |
| 4185 | − }, | |
| 4186 | − } | |
| 4187 | − }); | |
| 4188 | − match &prettier_path { | |
| 4189 | − Some(prettier_path) => { | |
| 4190 | − log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); | |
| 4191 | − }, | |
| 4192 | − None => { | |
| 4193 | − log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); | |
| 4194 | − }, | |
| 4195 | − } | |
| 4196 | − } | |
| 4197 | − } | |
| 4159 | + if let Some(new_operation) = | |
| 4160 | + prettier_support::format_with_prettier(&project, buffer, &mut cx) | |
| 4161 | + .await | |
| 4162 | + { | |
| 4163 | + format_operation = Some(new_operation); | |
| 4198 | 4164 | } else if let Some((language_server, buffer_abs_path)) = |
| 4199 | 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) => { | |
| 4216 | − if let Some((prettier_path, prettier_task)) = project | |
| 4217 | − .update(&mut cx, |project, cx| { | |
| 4218 | − project.prettier_instance_for_buffer(buffer, cx) | |
| 4219 | − }).await { | |
| 4220 | − match prettier_task.await | |
| 4221 | − { | |
| 4222 | − Ok(prettier) => { | |
| 4223 | − let buffer_path = buffer.update(&mut cx, |buffer, cx| { | |
| 4224 | − File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) | |
| 4225 | − }); | |
| 4226 | − format_operation = Some(FormatOperation::Prettier( | |
| 4227 | − prettier | |
| 4228 | − .format(buffer, buffer_path, &cx) | |
| 4229 | − .await | |
| 4230 | − .context("formatting via prettier")?, | |
| 4231 | − )); | |
| 4232 | − } | |
| 4233 | − Err(e) => { | |
| 4234 | − project.update(&mut cx, |project, _| { | |
| 4235 | − match &prettier_path { | |
| 4236 | − Some(prettier_path) => { | |
| 4237 | − project.prettier_instances.remove(prettier_path); | |
| 4238 | − }, | |
| 4239 | − None => { | |
| 4240 | − if let Some(default_prettier) = project.default_prettier.as_mut() { | |
| 4241 | − default_prettier.instance = None; | |
| 4242 | − } | |
| 4243 | − }, | |
| 4244 | − } | |
| 4245 | − }); | |
| 4246 | − match &prettier_path { | |
| 4247 | − Some(prettier_path) => { | |
| 4248 | − log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); | |
| 4249 | − }, | |
| 4250 | − None => { | |
| 4251 | − log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); | |
| 4252 | − }, | |
| 4253 | − } | |
| 4254 | − } | |
| 4255 | − } | |
| 4256 | − } | |
| 4181 | + (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => { | |
| 4182 | + if let Some(new_operation) = | |
| 4183 | + prettier_support::format_with_prettier(&project, buffer, &mut cx) | |
| 4184 | + .await | |
| 4185 | + { | |
| 4186 | + format_operation = Some(new_operation); | |
| 4187 | + } | |
| 4257 | 4188 | } |
| 4258 | 4189 | }; |
| 4259 | 4190 | |
| @@ -6566,85 +6497,6 @@ impl Project { · 2306 unmodified lines | ||
| 6566 | 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 | 6500 | pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) { |
| 6649 | 6501 | let new_active_entry = entry.and_then(|project_path| { |
| 6650 | 6502 | let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; |
| @@ -8536,446 +8388,6 @@ impl Project { · 1885 unmodified lines | ||
| 8536 | 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 | 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 | 1 | mod ignore; |
| 2 | 2 | mod lsp_command; |
| 3 | +mod prettier_support; | |
| 3 | 4 | pub mod project_settings; |
| 4 | 5 | pub mod search; |
| 5 | 6 | pub mod terminals; |
| @@ -20,7 +21,7 @@ use futures::{ · 14 unmodified lines | ||
| 20 | 21 | mpsc::{self, UnboundedReceiver}, |
| 21 | 22 | oneshot, |
| 22 | 23 | }, |
| 23 | − future::{self, try_join_all, Shared}, | |
| 24 | + future::{try_join_all, Shared}, | |
| 24 | 25 | stream::FuturesUnordered, |
| 25 | 26 | AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, |
| 26 | 27 | }; |
| @@ -31,9 +32,7 @@ use gpui::{ · 4 unmodified lines | ||
| 31 | 32 | }; |
| 32 | 33 | use itertools::Itertools; |
| 33 | 34 | use language::{ |
| 34 | − language_settings::{ | |
| 35 | − language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, | |
| 36 | − }, | |
| 35 | + language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, | |
| 37 | 36 | point_to_lsp, |
| 38 | 37 | proto::{ |
| 39 | 38 | deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, |
| @@ -54,7 +53,7 @@ use lsp_command::*; · 14 unmodified lines | ||
| 54 | 53 | use node_runtime::NodeRuntime; |
| 55 | 54 | use parking_lot::Mutex; |
| 56 | 55 | use postage::watch; |
| 57 | −use prettier::Prettier; | |
| 56 | +use prettier_support::{DefaultPrettier, PrettierInstance}; | |
| 58 | 57 | use project_settings::{LspSettings, ProjectSettings}; |
| 59 | 58 | use rand::prelude::*; |
| 60 | 59 | use search::SearchQuery; |
| @@ -70,7 +69,7 @@ use std::{ · 9 unmodified lines | ||
| 70 | 69 | hash::Hash, |
| 71 | 70 | mem, |
| 72 | 71 | num::NonZeroU32, |
| 73 | − ops::{ControlFlow, Range}, | |
| 72 | + ops::Range, | |
| 74 | 73 | path::{self, Component, Path, PathBuf}, |
| 75 | 74 | process::Stdio, |
| 76 | 75 | str, |
| @@ -83,11 +82,8 @@ use std::{ · 6 unmodified lines | ||
| 83 | 82 | use terminals::Terminals; |
| 84 | 83 | use text::Anchor; |
| 85 | 84 | use util::{ |
| 86 | − debug_panic, defer, | |
| 87 | − http::HttpClient, | |
| 88 | − merge_json_value_into, | |
| 89 | − paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, | |
| 90 | − post_inc, ResultExt, TryFutureExt as _, | |
| 85 | + debug_panic, defer, http::HttpClient, merge_json_value_into, | |
| 86 | + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, | |
| 91 | 87 | }; |
| 92 | 88 | |
| 93 | 89 | pub use fs::*; |
| @@ -166,16 +162,9 @@ pub struct Project { · 72 unmodified lines | ||
| 166 | 162 | copilot_log_subscription: Option<lsp::Subscription>, |
| 167 | 163 | current_lsp_settings: HashMap<Arc<str>, LspSettings>, |
| 168 | 164 | node: Option<Arc<dyn NodeRuntime>>, |
| 169 | − default_prettier: Option<DefaultPrettier>, | |
| 165 | + default_prettier: DefaultPrettier, | |
| 170 | 166 | prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>, |
| 171 | − prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>, | |
| 172 | −} | |
| 173 | − | |
| 174 | −struct DefaultPrettier { | |
| 175 | − instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>, | |
| 176 | − installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>, | |
| 177 | − #[cfg(not(any(test, feature = "test-support")))] | |
| 178 | − installed_plugins: HashSet<&'static str>, | |
| 167 | + prettier_instances: HashMap<PathBuf, PrettierInstance>, | |
| 179 | 168 | } |
| 180 | 169 | |
| 181 | 170 | struct DelayedDebounced { |
| @@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate { · 358 unmodified lines | ||
| 540 | 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 | 540 | impl FormatTrigger { |
| 544 | 541 | fn from_proto(value: i32) -> FormatTrigger { |
| 545 | 542 | match value { |
| @@ -689,7 +686,7 @@ impl Project { · 143 unmodified lines | ||
| 689 | 686 | copilot_log_subscription: None, |
| 690 | 687 | current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), |
| 691 | 688 | node: Some(node), |
| 692 | − default_prettier: None, | |
| 689 | + default_prettier: DefaultPrettier::default(), | |
| 693 | 690 | prettiers_per_worktree: HashMap::default(), |
| 694 | 691 | prettier_instances: HashMap::default(), |
| 695 | 692 | } |
| @@ -792,7 +789,7 @@ impl Project { · 96 unmodified lines | ||
| 792 | 789 | copilot_log_subscription: None, |
| 793 | 790 | current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), |
| 794 | 791 | node: None, |
| 795 | − default_prettier: None, | |
| 792 | + default_prettier: DefaultPrettier::default(), | |
| 796 | 793 | prettiers_per_worktree: HashMap::default(), |
| 797 | 794 | prettier_instances: HashMap::default(), |
| 798 | 795 | }; |
| @@ -965,8 +962,19 @@ impl Project { · 166 unmodified lines | ||
| 965 | 962 | .detach(); |
| 966 | 963 | } |
| 967 | 964 | |
| 965 | + let mut prettier_plugins_by_worktree = HashMap::default(); | |
| 968 | 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 | 980 | // Start all the newly-enabled language servers. |
| @@ -2722,8 +2730,11 @@ impl Project { · 1749 unmodified lines | ||
| 2722 | 2730 | let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); |
| 2723 | 2731 | let buffer_file = File::from_dyn(buffer_file.as_ref()); |
| 2724 | 2732 | let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); |
| 2725 | − | |
| 2726 | − self.install_default_formatters(worktree, &new_language, &settings, cx); | |
| 2733 | + if let Some(prettier_plugins) = | |
| 2734 | + prettier_support::prettier_plugins_for_language(&new_language, &settings) | |
| 2735 | + { | |
| 2736 | + self.install_default_prettier(worktree, prettier_plugins, cx); | |
| 2737 | + }; | |
| 2727 | 2738 | if let Some(file) = buffer_file { |
| 2728 | 2739 | let worktree = file.worktree.clone(); |
| 2729 | 2740 | if let Some(tree) = worktree.read(cx).as_local() { |
| @@ -4126,7 +4137,8 @@ impl Project { · 1396 unmodified lines | ||
| 4126 | 4137 | this.buffers_being_formatted |
| 4127 | 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 | 4151 | let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; |
| 4140 | 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 | 4153 | let tab_size = settings.tab_size; |
| 4144 | 4154 | |
| 4145 | 4155 | // First, format buffer's whitespace according to the settings. |
| @@ -4164,18 +4174,10 @@ impl Project { · 18 unmodified lines | ||
| 4164 | 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 | 4177 | // Apply language-specific formatting using either a language server |
| 4176 | 4178 | // or external command. |
| 4177 | 4179 | let mut format_operation = None; |
| 4178 | − match (formatter, format_on_save) { | |
| 4180 | + match (&settings.formatter, &settings.format_on_save) { | |
| 4179 | 4181 | (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} |
| 4180 | 4182 | |
| 4181 | 4183 | (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) |
| @@ -4220,46 +4222,11 @@ impl Project { · 38 unmodified lines | ||
| 4220 | 4222 | } |
| 4221 | 4223 | } |
| 4222 | 4224 | (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { |
| 4223 | − if let Some((prettier_path, prettier_task)) = project | |
| 4224 | − .update(&mut cx, |project, cx| { | |
| 4225 | − project.prettier_instance_for_buffer(buffer, cx) | |
| 4226 | − })?.await { | |
| 4227 | − match prettier_task.await | |
| 4228 | − { | |
| 4229 | − Ok(prettier) => { | |
| 4230 | − let buffer_path = buffer.update(&mut cx, |buffer, cx| { | |
| 4231 | − File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) | |
| 4232 | − })?; | |
| 4233 | − format_operation = Some(FormatOperation::Prettier( | |
| 4234 | − prettier | |
| 4235 | − .format(buffer, buffer_path, &mut cx) | |
| 4236 | − .await | |
| 4237 | − .context("formatting via prettier")?, | |
| 4238 | − )); | |
| 4239 | − } | |
| 4240 | − Err(e) => { | |
| 4241 | − project.update(&mut cx, |project, _| { | |
| 4242 | − match &prettier_path { | |
| 4243 | − Some(prettier_path) => { | |
| 4244 | − project.prettier_instances.remove(prettier_path); | |
| 4245 | − }, | |
| 4246 | − None => { | |
| 4247 | − if let Some(default_prettier) = project.default_prettier.as_mut() { | |
| 4248 | − default_prettier.instance = None; | |
| 4249 | − } | |
| 4250 | − }, | |
| 4251 | − } | |
| 4252 | − })?; | |
| 4253 | − match &prettier_path { | |
| 4254 | − Some(prettier_path) => { | |
| 4255 | − log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); | |
| 4256 | − }, | |
| 4257 | − None => { | |
| 4258 | − log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); | |
| 4259 | − }, | |
| 4260 | − } | |
| 4261 | − } | |
| 4262 | − } | |
| 4225 | + if let Some(new_operation) = | |
| 4226 | + prettier_support::format_with_prettier(&project, buffer, &mut cx) | |
| 4227 | + .await | |
| 4228 | + { | |
| 4229 | + format_operation = Some(new_operation); | |
| 4263 | 4230 | } else if let Some((language_server, buffer_abs_path)) = |
| 4264 | 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) => { | |
| 4281 | − if let Some((prettier_path, prettier_task)) = project | |
| 4282 | − .update(&mut cx, |project, cx| { | |
| 4283 | − project.prettier_instance_for_buffer(buffer, cx) | |
| 4284 | − })?.await { | |
| 4285 | − match prettier_task.await | |
| 4286 | − { | |
| 4287 | − Ok(prettier) => { | |
| 4288 | − let buffer_path = buffer.update(&mut cx, |buffer, cx| { | |
| 4289 | − File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) | |
| 4290 | − })?; | |
| 4291 | − format_operation = Some(FormatOperation::Prettier( | |
| 4292 | − prettier | |
| 4293 | − .format(buffer, buffer_path, &mut cx) | |
| 4294 | − .await | |
| 4295 | − .context("formatting via prettier")?, | |
| 4296 | − )); | |
| 4297 | − } | |
| 4298 | − Err(e) => { | |
| 4299 | − project.update(&mut cx, |project, _| { | |
| 4300 | − match &prettier_path { | |
| 4301 | − Some(prettier_path) => { | |
| 4302 | − project.prettier_instances.remove(prettier_path); | |
| 4303 | − }, | |
| 4304 | − None => { | |
| 4305 | − if let Some(default_prettier) = project.default_prettier.as_mut() { | |
| 4306 | − default_prettier.instance = None; | |
| 4307 | − } | |
| 4308 | − }, | |
| 4309 | − } | |
| 4310 | − })?; | |
| 4311 | − match &prettier_path { | |
| 4312 | − Some(prettier_path) => { | |
| 4313 | − log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}"); | |
| 4314 | − }, | |
| 4315 | − None => { | |
| 4316 | − log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}"); | |
| 4317 | − }, | |
| 4318 | − } | |
| 4319 | − } | |
| 4320 | − } | |
| 4321 | − } | |
| 4247 | + (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => { | |
| 4248 | + if let Some(new_operation) = | |
| 4249 | + prettier_support::format_with_prettier(&project, buffer, &mut cx) | |
| 4250 | + .await | |
| 4251 | + { | |
| 4252 | + format_operation = Some(new_operation); | |
| 4253 | + } | |
| 4322 | 4254 | } |
| 4323 | 4255 | }; |
| 4324 | 4256 | |
| @@ -6638,84 +6570,6 @@ impl Project { · 2313 unmodified lines | ||
| 6638 | 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 | 6573 | pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) { |
| 6720 | 6574 | let new_active_entry = entry.and_then(|project_path| { |
| 6721 | 6575 | let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; |
| @@ -8579,486 +8433,6 @@ impl Project { · 1857 unmodified lines | ||
| 8579 | 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 | 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> { | |
| 45 | − maybe!({ | |
| 46 | − let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?; | |
| 44 | + pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> { | |
| 45 | + let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?; | |
| 47 | 46 | |
| 48 | − // FIXME: Associate a type with the languages and have the file's langauge | |
| 49 | − // override these associations | |
| 50 | − maybe!({ | |
| 51 | − let suffix = path.icon_suffix()?; | |
| 47 | + // FIXME: Associate a type with the languages and have the file's langauge | |
| 48 | + // override these associations | |
| 49 | + maybe!({ | |
| 50 | + let suffix = path.icon_suffix()?; | |
| 52 | 51 | |
| 53 | − this.suffixes | |
| 54 | − .get(suffix) | |
| 55 | − .and_then(|type_str| this.types.get(type_str)) | |
| 56 | − .map(|type_config| type_config.icon.clone()) | |
| 57 | − }) | |
| 58 | − .or_else(|| this.types.get("default").map(|config| config.icon.clone())) | |
| 52 | + this.suffixes | |
| 53 | + .get(suffix) | |
| 54 | + .and_then(|type_str| this.types.get(type_str)) | |
| 55 | + .map(|type_config| type_config.icon.clone()) | |
| 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> { | |
| 64 | − maybe!({ | |
| 65 | − let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?; | |
| 60 | + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> { | |
| 61 | + let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?; | |
| 66 | 62 | |
| 67 | − let key = if expanded { | |
| 68 | − EXPANDED_DIRECTORY_TYPE | |
| 69 | − } else { | |
| 70 | − COLLAPSED_DIRECTORY_TYPE | |
| 71 | − }; | |
| 63 | + let key = if expanded { | |
| 64 | + EXPANDED_DIRECTORY_TYPE | |
| 65 | + } else { | |
| 66 | + COLLAPSED_DIRECTORY_TYPE | |
| 67 | + }; | |
| 72 | 68 | |
| 73 | − this.types | |
| 74 | − .get(key) | |
| 75 | − .map(|type_config| type_config.icon.clone()) | |
| 76 | − }) | |
| 77 | − .unwrap_or_else(|| Arc::from("".to_string())) | |
| 69 | + this.types | |
| 70 | + .get(key) | |
| 71 | + .map(|type_config| type_config.icon.clone()) | |
| 78 | 72 | } |
| 79 | 73 | |
| 80 | − pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> { | |
| 81 | − maybe!({ | |
| 82 | − let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?; | |
| 74 | + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> { | |
| 75 | + let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?; | |
| 83 | 76 | |
| 84 | − let key = if expanded { | |
| 85 | − EXPANDED_CHEVRON_TYPE | |
| 86 | − } else { | |
| 87 | − COLLAPSED_CHEVRON_TYPE | |
| 88 | − }; | |
| 77 | + let key = if expanded { | |
| 78 | + EXPANDED_CHEVRON_TYPE | |
| 79 | + } else { | |
| 80 | + COLLAPSED_CHEVRON_TYPE | |
| 81 | + }; | |
| 89 | 82 | |
| 90 | − this.types | |
| 91 | − .get(key) | |
| 92 | − .map(|type_config| type_config.icon.clone()) | |
| 93 | − }) | |
| 94 | − .unwrap_or_else(|| Arc::from("".to_string())) | |
| 83 | + this.types | |
| 84 | + .get(key) | |
| 85 | + .map(|type_config| type_config.icon.clone()) | |
| 95 | 86 | } |
| 96 | 87 | } |
| @@ -1283,16 +1283,16 @@ impl ProjectPanel { · 1282 unmodified lines | ||
| 1283 | 1283 | let icon = match entry.kind { |
| 1284 | 1284 | EntryKind::File(_) => { |
| 1285 | 1285 | if show_file_icons { |
| 1286 | − Some(FileAssociations::get_icon(&entry.path, cx)) | |
| 1286 | + FileAssociations::get_icon(&entry.path, cx) | |
| 1287 | 1287 | } else { |
| 1288 | 1288 | None |
| 1289 | 1289 | } |
| 1290 | 1290 | } |
| 1291 | 1291 | _ => { |
| 1292 | 1292 | if show_folder_icons { |
| 1293 | − Some(FileAssociations::get_folder_icon(is_expanded, cx)) | |
| 1293 | + FileAssociations::get_folder_icon(is_expanded, cx) | |
| 1294 | 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 | 2 | use ui::{Button, ButtonVariant, IconButton}; |
| 3 | 3 | |
| 4 | 4 | use crate::mode::SearchMode; |
| @@ -6,7 +6,7 @@ use crate::mode::SearchMode; · 1 unmodified line | ||
| 6 | 6 | pub(super) fn render_nav_button( |
| 7 | 7 | icon: ui::Icon, |
| 8 | 8 | _active: bool, |
| 9 | − on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, | |
| 9 | + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, | |
| 10 | 10 | ) -> impl IntoElement { |
| 11 | 11 | // let tooltip_style = cx.theme().tooltip.clone(); |
| 12 | 12 | // let cursor_style = if active { |
| @@ -21,7 +21,7 @@ pub(super) fn render_nav_button( · 8 unmodified lines | ||
| 21 | 21 | pub(crate) fn render_search_mode_button( |
| 22 | 22 | mode: SearchMode, |
| 23 | 23 | is_active: bool, |
| 24 | − on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, | |
| 24 | + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, | |
| 25 | 25 | ) -> Button { |
| 26 | 26 | let button_variant = if is_active { |
| 27 | 27 | ButtonVariant::Filled |
| @@ -2,7 +2,7 @@ use gpui::{ · 1 unmodified line | ||
| 2 | 2 | actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View, |
| 3 | 3 | WindowContext, |
| 4 | 4 | }; |
| 5 | −use theme2::ActiveTheme; | |
| 5 | +use ui::prelude::*; | |
| 6 | 6 | |
| 7 | 7 | actions!(ActionA, ActionB, ActionC); |
| 8 | 8 | |
| @@ -4,7 +4,7 @@ use gpui::{ · 3 unmodified lines | ||
| 4 | 4 | }; |
| 5 | 5 | use picker::{Picker, PickerDelegate}; |
| 6 | 6 | use std::sync::Arc; |
| 7 | −use theme2::ActiveTheme; | |
| 7 | +use ui::prelude::*; | |
| 8 | 8 | use ui::{Label, ListItem}; |
| 9 | 9 | |
| 10 | 10 | pub struct PickerStory { |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; |
| 2 | −use theme2::ActiveTheme; | |
| 2 | +use ui::prelude::*; | |
| 3 | 3 | use ui::Tooltip; |
| 4 | 4 | |
| 5 | 5 | pub struct ScrollStory; |
| @@ -19,7 +19,6 @@ pub enum ComponentStory { · 18 unmodified lines | ||
| 19 | 19 | Focus, |
| 20 | 20 | Icon, |
| 21 | 21 | IconButton, |
| 22 | − Input, | |
| 23 | 22 | Keybinding, |
| 24 | 23 | Label, |
| 25 | 24 | ListItem, |
| @@ -39,7 +38,6 @@ impl ComponentStory { · 13 unmodified lines | ||
| 39 | 38 | Self::Focus => FocusStory::view(cx).into(), |
| 40 | 39 | Self::Icon => cx.build_view(|_| ui::IconStory).into(), |
| 41 | 40 | Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(), |
| 42 | − Self::Input => cx.build_view(|_| ui::InputStory).into(), | |
| 43 | 41 | Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), |
| 44 | 42 | Self::Label => cx.build_view(|_| ui::LabelStory).into(), |
| 45 | 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 | 93 | pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ { |
| 90 | 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 | 1 | mod avatar; |
| 2 | 2 | mod button; |
| 3 | +mod button2; | |
| 3 | 4 | mod checkbox; |
| 4 | 5 | mod context_menu; |
| 5 | 6 | mod disclosure; |
| 6 | 7 | mod divider; |
| 7 | 8 | mod icon; |
| 8 | 9 | mod icon_button; |
| 9 | −mod input; | |
| 10 | 10 | mod keybinding; |
| 11 | 11 | mod label; |
| 12 | 12 | mod list; |
| @@ -21,13 +21,13 @@ mod stories; · 8 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | pub use avatar::*; |
| 23 | 23 | pub use button::*; |
| 24 | +pub use button2::*; | |
| 24 | 25 | pub use checkbox::*; |
| 25 | 26 | pub use context_menu::*; |
| 26 | 27 | pub use disclosure::*; |
| 27 | 28 | pub use divider::*; |
| 28 | 29 | pub use icon::*; |
| 29 | 30 | pub use icon_button::*; |
| 30 | −pub use input::*; | |
| 31 | 31 | pub use keybinding::*; |
| 32 | 32 | pub use label::*; |
| 33 | 33 | pub use list::*; |
| @@ -1,9 +1,7 @@ | ||
| 1 | −use std::rc::Rc; | |
| 2 | − | |
| 3 | 1 | use gpui::{ |
| 4 | − DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent, | |
| 5 | − StatefulInteractiveElement, WindowContext, | |
| 2 | + ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext, | |
| 6 | 3 | }; |
| 4 | +use std::rc::Rc; | |
| 7 | 5 | |
| 8 | 6 | use crate::prelude::*; |
| 9 | 7 | use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; |
| @@ -67,7 +65,7 @@ impl ButtonVariant { · 57 unmodified lines | ||
| 67 | 65 | #[derive(IntoElement)] |
| 68 | 66 | pub struct Button { |
| 69 | 67 | disabled: bool, |
| 70 | − click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>, | |
| 68 | + click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>, | |
| 71 | 69 | icon: Option<Icon>, |
| 72 | 70 | icon_position: Option<IconPosition>, |
| 73 | 71 | label: SharedString, |
| @@ -118,7 +116,7 @@ impl RenderOnce for Button { · 44 unmodified lines | ||
| 118 | 116 | } |
| 119 | 117 | |
| 120 | 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 | 120 | click_handler(event, cx); |
| 123 | 121 | }); |
| 124 | 122 | } |
| @@ -168,10 +166,7 @@ impl Button { · 43 unmodified lines | ||
| 168 | 166 | self |
| 169 | 167 | } |
| 170 | 168 | |
| 171 | − pub fn on_click( | |
| 172 | − mut self, | |
| 173 | − handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, | |
| 174 | − ) -> Self { | |
| 169 | + pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { | |
| 175 | 170 | self.click_handler = Some(Rc::new(handler)); |
| 176 | 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 | 11 | match (toggle.is_toggleable(), toggle.is_toggled()) { |
| 7 | 12 | (false, _) => div(), |
| 8 | 13 | (_, true) => div().child( |
| 9 | − IconElement::new(Icon::ChevronDown) | |
| 14 | + IconButton::new("toggle", Icon::ChevronDown) | |
| 10 | 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 | 21 | (_, false) => div().child( |
| 14 | − IconElement::new(Icon::ChevronRight) | |
| 22 | + IconButton::new("toggle", Icon::ChevronRight) | |
| 15 | 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}; | |
| 2 | −use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful}; | |
| 1 | +use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; | |
| 2 | +use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful}; | |
| 3 | 3 | |
| 4 | 4 | #[derive(IntoElement)] |
| 5 | 5 | pub struct IconButton { |
| 6 | 6 | id: ElementId, |
| 7 | 7 | icon: Icon, |
| 8 | 8 | color: Color, |
| 9 | + size: IconSize, | |
| 9 | 10 | variant: ButtonVariant, |
| 10 | − state: InteractionState, | |
| 11 | + disabled: bool, | |
| 11 | 12 | selected: bool, |
| 12 | 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 | 17 | impl RenderOnce for IconButton { |
| 17 | 18 | type Rendered = Stateful<Div>; |
| 18 | 19 | |
| 19 | 20 | fn render(self, cx: &mut WindowContext) -> Self::Rendered { |
| 20 | − let icon_color = match (self.state, self.color) { | |
| 21 | − (InteractionState::Disabled, _) => Color::Disabled, | |
| 22 | − (InteractionState::Active, _) => Color::Selected, | |
| 21 | + let icon_color = match (self.disabled, self.selected, self.color) { | |
| 22 | + (true, _, _) => Color::Disabled, | |
| 23 | + (false, true, _) => Color::Selected, | |
| 23 | 24 | _ => self.color, |
| 24 | 25 | }; |
| 25 | 26 | |
| @@ -50,10 +51,14 @@ impl RenderOnce for IconButton { · 24 unmodified lines | ||
| 50 | 51 | // place we use an icon button. |
| 51 | 52 | // .hover(|style| style.bg(bg_hover_color)) |
| 52 | 53 | .active(|style| style.bg(bg_active_color)) |
| 53 | − .child(IconElement::new(self.icon).color(icon_color)); | |
| 54 | − | |
| 55 | − if let Some(click_handler) = self.on_mouse_down { | |
| 56 | − button = button.on_mouse_down(MouseButton::Left, move |event, cx| { | |
| 54 | + .child( | |
| 55 | + IconElement::new(self.icon) | |
| 56 | + .size(self.size) | |
| 57 | + .color(icon_color), | |
| 58 | + ); | |
| 59 | + | |
| 60 | + if let Some(click_handler) = self.on_click { | |
| 61 | + button = button.on_click(move |event, cx| { | |
| 57 | 62 | cx.stop_propagation(); |
| 58 | 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. | |
| 69 | − div().id(self.id.clone()).child(button) | |
| 73 | + button | |
| 70 | 74 | } |
| 71 | 75 | } |
| 72 | 76 | |
| @@ -76,11 +80,12 @@ impl IconButton { · 3 unmodified lines | ||
| 76 | 80 | id: id.into(), |
| 77 | 81 | icon, |
| 78 | 82 | color: Color::default(), |
| 83 | + size: Default::default(), | |
| 79 | 84 | variant: ButtonVariant::default(), |
| 80 | − state: InteractionState::default(), | |
| 81 | 85 | selected: false, |
| 86 | + disabled: false, | |
| 82 | 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 | 99 | self |
| 95 | 100 | } |
| 96 | 101 | |
| 97 | − pub fn variant(mut self, variant: ButtonVariant) -> Self { | |
| 98 | − self.variant = variant; | |
| 102 | + pub fn size(mut self, size: IconSize) -> Self { | |
| 103 | + self.size = size; | |
| 99 | 104 | self |
| 100 | 105 | } |
| 101 | 106 | |
| 102 | − pub fn state(mut self, state: InteractionState) -> Self { | |
| 103 | − self.state = state; | |
| 107 | + pub fn variant(mut self, variant: ButtonVariant) -> Self { | |
| 108 | + self.variant = variant; | |
| 104 | 109 | self |
| 105 | 110 | } |
| 106 | 111 | |
| @@ -109,16 +114,18 @@ impl IconButton { · 2 unmodified lines | ||
| 109 | 114 | self |
| 110 | 115 | } |
| 111 | 116 | |
| 117 | + pub fn disabled(mut self, disabled: bool) -> Self { | |
| 118 | + self.disabled = disabled; | |
| 119 | + self | |
| 120 | + } | |
| 121 | + | |
| 112 | 122 | pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { |
| 113 | 123 | self.tooltip = Some(Box::new(tooltip)); |
| 114 | 124 | self |
| 115 | 125 | } |
| 116 | 126 | |
| 117 | − pub fn on_click( | |
| 118 | − mut self, | |
| 119 | − handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext), | |
| 120 | − ) -> Self { | |
| 121 | − self.on_mouse_down = Some(Box::new(handler)); | |
| 127 | + pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self { | |
| 128 | + self.on_click = Some(Box::new(handler)); | |
| 122 | 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 | 25 | left_icon: Option<Icon>, |
| 26 | 26 | meta: Option<ListHeaderMeta>, |
| 27 | 27 | toggle: Toggle, |
| 28 | + on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, | |
| 28 | 29 | inset: bool, |
| 30 | + selected: bool, | |
| 29 | 31 | } |
| 30 | 32 | |
| 31 | 33 | impl ListHeader { |
| @@ -36,6 +38,8 @@ impl ListHeader { · 4 unmodified lines | ||
| 36 | 38 | meta: None, |
| 37 | 39 | inset: false, |
| 38 | 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 | 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 | 59 | pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self { |
| 48 | 60 | self.left_icon = left_icon; |
| 49 | 61 | self |
| @@ -57,13 +69,18 @@ impl ListHeader { · 7 unmodified lines | ||
| 57 | 69 | self.meta = meta; |
| 58 | 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 | 79 | impl RenderOnce for ListHeader { |
| 63 | 80 | type Rendered = Div; |
| 64 | 81 | |
| 65 | 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 | 85 | let meta = match self.meta { |
| 69 | 86 | Some(ListHeaderMeta::Tools(icons)) => div().child( |
| @@ -85,6 +102,9 @@ impl RenderOnce for ListHeader { · 15 unmodified lines | ||
| 85 | 102 | div() |
| 86 | 103 | .h_5() |
| 87 | 104 | .when(self.inset, |this| this.px_2()) |
| 105 | + .when(self.selected, |this| { | |
| 106 | + this.bg(cx.theme().colors().ghost_element_selected) | |
| 107 | + }) | |
| 88 | 108 | .flex() |
| 89 | 109 | .flex_1() |
| 90 | 110 | .items_center() |
| @@ -177,6 +197,7 @@ pub struct ListItem { · 86 unmodified lines | ||
| 177 | 197 | toggle: Toggle, |
| 178 | 198 | inset: bool, |
| 179 | 199 | on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, |
| 200 | + on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, | |
| 180 | 201 | on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>, |
| 181 | 202 | children: SmallVec<[AnyElement; 2]>, |
| 182 | 203 | } |
| @@ -193,6 +214,7 @@ impl ListItem { · 10 unmodified lines | ||
| 193 | 214 | inset: false, |
| 194 | 215 | on_click: None, |
| 195 | 216 | on_secondary_mouse_down: None, |
| 217 | + on_toggle: None, | |
| 196 | 218 | children: SmallVec::new(), |
| 197 | 219 | } |
| 198 | 220 | } |
| @@ -230,6 +252,14 @@ impl ListItem { · 31 unmodified lines | ||
| 230 | 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 | 263 | pub fn selected(mut self, selected: bool) -> Self { |
| 234 | 264 | self.selected = selected; |
| 235 | 265 | self |
| @@ -255,19 +285,6 @@ impl RenderOnce for ListItem { · 19 unmodified lines | ||
| 255 | 285 | type Rendered = Stateful<Div>; |
| 256 | 286 | |
| 257 | 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 | 288 | div() |
| 272 | 289 | .id(self.id) |
| 273 | 290 | .relative() |
| @@ -282,8 +299,8 @@ impl RenderOnce for ListItem { · 8 unmodified lines | ||
| 282 | 299 | .when(self.selected, |this| { |
| 283 | 300 | this.bg(cx.theme().colors().ghost_element_selected) |
| 284 | 301 | }) |
| 285 | − .when_some(self.on_click.clone(), |this, on_click| { | |
| 286 | − this.on_click(move |event, cx| { | |
| 302 | + .when_some(self.on_click, |this, on_click| { | |
| 303 | + this.cursor_pointer().on_click(move |event, cx| { | |
| 287 | 304 | // HACK: GPUI currently fires `on_click` with any mouse button, |
| 288 | 305 | // but we only care about the left button. |
| 289 | 306 | if event.down.button == MouseButton::Left { |
| @@ -304,23 +321,18 @@ impl RenderOnce for ListItem { · 14 unmodified lines | ||
| 304 | 321 | .gap_1() |
| 305 | 322 | .items_center() |
| 306 | 323 | .relative() |
| 307 | − .child(disclosure_control(self.toggle)) | |
| 308 | − .children(left_content) | |
| 309 | − .children(self.children) | |
| 310 | − // HACK: We need to attach the `on_click` handler to the child element in order to have the click | |
| 311 | − // event actually fire. | |
| 312 | − // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the | |
| 313 | − // outer `div`. | |
| 314 | − .id("on_click_hack") | |
| 315 | − .when_some(self.on_click, |this, on_click| { | |
| 316 | − this.on_click(move |event, cx| { | |
| 317 | − // HACK: GPUI currently fires `on_click` with any mouse button, | |
| 318 | − // but we only care about the left button. | |
| 319 | − if event.down.button == MouseButton::Left { | |
| 320 | − (on_click)(event, cx) | |
| 321 | − } | |
| 322 | − }) | |
| 323 | − }), | |
| 324 | + .child(disclosure_control(self.toggle, self.on_toggle)) | |
| 325 | + .map(|this| match self.left_slot { | |
| 326 | + Some(GraphicSlot::Icon(i)) => this.child( | |
| 327 | + IconElement::new(i) | |
| 328 | + .size(IconSize::Small) | |
| 329 | + .color(Color::Muted), | |
| 330 | + ), | |
| 331 | + Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)), | |
| 332 | + Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)), | |
| 333 | + None => this, | |
| 334 | + }) | |
| 335 | + .children(self.children), | |
| 324 | 336 | ) |
| 325 | 337 | } |
| 326 | 338 | } |
| @@ -4,18 +4,15 @@ mod checkbox; · 3 unmodified lines | ||
| 4 | 4 | mod context_menu; |
| 5 | 5 | mod icon; |
| 6 | 6 | mod icon_button; |
| 7 | −mod input; | |
| 8 | 7 | mod keybinding; |
| 9 | 8 | mod label; |
| 10 | 9 | mod list_item; |
| 11 | − | |
| 12 | 10 | pub use avatar::*; |
| 13 | 11 | pub use button::*; |
| 14 | 12 | pub use checkbox::*; |
| 15 | 13 | pub use context_menu::*; |
| 16 | 14 | pub use icon::*; |
| 17 | 15 | pub use icon_button::*; |
| 18 | −pub use input::*; | |
| 19 | 16 | pub use keybinding::*; |
| 20 | 17 | pub use label::*; |
| 21 | 18 | pub use list_item::*; |
| @@ -1,9 +1,8 @@ | ||
| 1 | −use gpui::{rems, Div, Render}; | |
| 1 | +use gpui::{Div, Render}; | |
| 2 | 2 | use story::Story; |
| 3 | −use strum::IntoEnumIterator; | |
| 4 | 3 | |
| 5 | 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 | 7 | pub struct ButtonStory; |
| 9 | 8 | |
| @@ -11,8 +10,6 @@ impl Render for ButtonStory { · 1 unmodified line | ||
| 11 | 10 | type Element = Div; |
| 12 | 11 | |
| 13 | 12 | fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { |
| 14 | − let states = InteractionState::iter(); | |
| 15 | − | |
| 16 | 13 | Story::container() |
| 17 | 14 | .child(Story::title_for::<Button>()) |
| 18 | 15 | .child( |
| @@ -20,121 +17,56 @@ impl Render for ButtonStory { · 1 unmodified line | ||
| 20 | 17 | .flex() |
| 21 | 18 | .gap_8() |
| 22 | 19 | .child( |
| 23 | − div() | |
| 24 | − .child(Story::label("Ghost (Default)")) | |
| 25 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 26 | − v_stack() | |
| 27 | − .gap_1() | |
| 28 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 29 | − .child( | |
| 30 | − Button::new("Label").variant(ButtonVariant::Ghost), // .state(state), | |
| 31 | − ) | |
| 32 | − }))) | |
| 33 | − .child(Story::label("Ghost – Left Icon")) | |
| 34 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 35 | − v_stack() | |
| 36 | − .gap_1() | |
| 37 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 38 | − .child( | |
| 39 | − Button::new("Label") | |
| 40 | − .variant(ButtonVariant::Ghost) | |
| 41 | − .icon(Icon::Plus) | |
| 42 | − .icon_position(IconPosition::Left), // .state(state), | |
| 43 | − ) | |
| 44 | − }))) | |
| 45 | − .child(Story::label("Ghost – Right Icon")) | |
| 46 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 47 | − v_stack() | |
| 48 | − .gap_1() | |
| 49 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 50 | − .child( | |
| 51 | − Button::new("Label") | |
| 52 | − .variant(ButtonVariant::Ghost) | |
| 53 | − .icon(Icon::Plus) | |
| 54 | − .icon_position(IconPosition::Right), // .state(state), | |
| 55 | − ) | |
| 56 | − }))), | |
| 57 | − ) | |
| 58 | − .child( | |
| 59 | − div() | |
| 60 | − .child(Story::label("Filled")) | |
| 61 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 62 | − v_stack() | |
| 63 | − .gap_1() | |
| 64 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 65 | − .child( | |
| 66 | − Button::new("Label").variant(ButtonVariant::Filled), // .state(state), | |
| 67 | − ) | |
| 68 | − }))) | |
| 69 | − .child(Story::label("Filled – Left Button")) | |
| 70 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 71 | − v_stack() | |
| 72 | − .gap_1() | |
| 73 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 74 | − .child( | |
| 75 | − Button::new("Label") | |
| 76 | − .variant(ButtonVariant::Filled) | |
| 77 | − .icon(Icon::Plus) | |
| 78 | − .icon_position(IconPosition::Left), // .state(state), | |
| 79 | − ) | |
| 80 | − }))) | |
| 81 | − .child(Story::label("Filled – Right Button")) | |
| 82 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 83 | − v_stack() | |
| 84 | − .gap_1() | |
| 85 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 86 | − .child( | |
| 87 | − Button::new("Label") | |
| 88 | − .variant(ButtonVariant::Filled) | |
| 89 | − .icon(Icon::Plus) | |
| 90 | − .icon_position(IconPosition::Right), // .state(state), | |
| 91 | − ) | |
| 92 | − }))), | |
| 20 | + div().child(Story::label("Ghost (Default)")).child( | |
| 21 | + h_stack() | |
| 22 | + .gap_2() | |
| 23 | + .child(Button::new("Label").variant(ButtonVariant::Ghost)), | |
| 24 | + ), | |
| 93 | 25 | ) |
| 26 | + .child(Story::label("Ghost – Left Icon")) | |
| 94 | 27 | .child( |
| 95 | − div() | |
| 96 | − .child(Story::label("Fixed With")) | |
| 97 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 98 | − v_stack() | |
| 99 | − .gap_1() | |
| 100 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 101 | − .child( | |
| 102 | − Button::new("Label") | |
| 103 | − .variant(ButtonVariant::Filled) | |
| 104 | − // .state(state) | |
| 105 | − .width(Some(rems(6.).into())), | |
| 106 | − ) | |
| 107 | − }))) | |
| 108 | − .child(Story::label("Fixed With – Left Icon")) | |
| 109 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 110 | − v_stack() | |
| 111 | − .gap_1() | |
| 112 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 113 | − .child( | |
| 114 | − Button::new("Label") | |
| 115 | − .variant(ButtonVariant::Filled) | |
| 116 | − // .state(state) | |
| 117 | − .icon(Icon::Plus) | |
| 118 | − .icon_position(IconPosition::Left) | |
| 119 | − .width(Some(rems(6.).into())), | |
| 120 | − ) | |
| 121 | − }))) | |
| 122 | − .child(Story::label("Fixed With – Right Icon")) | |
| 123 | − .child(h_stack().gap_2().children(states.clone().map(|state| { | |
| 124 | − v_stack() | |
| 125 | − .gap_1() | |
| 126 | − .child(Label::new(state.to_string()).color(Color::Muted)) | |
| 127 | − .child( | |
| 128 | − Button::new("Label") | |
| 129 | − .variant(ButtonVariant::Filled) | |
| 130 | − // .state(state) | |
| 131 | − .icon(Icon::Plus) | |
| 132 | − .icon_position(IconPosition::Right) | |
| 133 | − .width(Some(rems(6.).into())), | |
| 134 | − ) | |
| 135 | − }))), | |
| 28 | + h_stack().gap_2().child( | |
| 29 | + Button::new("Label") | |
| 30 | + .variant(ButtonVariant::Ghost) | |
| 31 | + .icon(Icon::Plus) | |
| 32 | + .icon_position(IconPosition::Left), | |
| 33 | + ), | |
| 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 | 70 | .child(Story::label("Button with `on_click`")) |
| 139 | 71 | .child( |
| 140 | 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 | 2 | use story::Story; |
| 3 | 3 | |
| 4 | 4 | use crate::prelude::*; |
| 5 | −use crate::ListItem; | |
| 5 | +use crate::{Icon, ListItem}; | |
| 6 | 6 | |
| 7 | 7 | pub struct ListItemStory; |
| 8 | 8 | |
| @@ -14,6 +14,20 @@ impl Render for ListItemStory { · 5 unmodified lines | ||
| 14 | 14 | .child(Story::title_for::<ListItem>()) |
| 15 | 15 | .child(Story::label("Default")) |
| 16 | 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 | 31 | .child(Story::label("With `on_click`")) |
| 18 | 32 | .child( |
| 19 | 33 | ListItem::new("with_on_click") |
| @@ -24,11 +38,11 @@ impl Render for ListItemStory { · 4 unmodified lines | ||
| 24 | 38 | ) |
| 25 | 39 | .child(Story::label("With `on_secondary_mouse_down`")) |
| 26 | 40 | .child( |
| 27 | − ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down( | |
| 28 | − |_event, _cx| { | |
| 41 | + ListItem::new("with_on_secondary_mouse_down") | |
| 42 | + .child("Right click me") | |
| 43 | + .on_secondary_mouse_down(|_event, _cx| { | |
| 29 | 44 | println!("Right mouse down!"); |
| 30 | − }, | |
| 31 | − ), | |
| 45 | + }), | |
| 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 | 3 | ViewContext, WindowContext, |
| 4 | 4 | }; |
| 5 | 5 | |
| 6 | +pub use crate::clickable::*; | |
| 7 | +pub use crate::fixed::*; | |
| 8 | +pub use crate::selectable::*; | |
| 6 | 9 | pub use crate::StyledExt; |
| 7 | 10 | pub use crate::{ButtonVariant, Color}; |
| 8 | 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 | 1 | use gpui::{Hsla, WindowContext}; |
| 2 | 2 | use theme::ActiveTheme; |
| 3 | 3 | |
| 4 | −#[derive(Default, PartialEq, Copy, Clone)] | |
| 4 | +#[derive(Debug, Default, PartialEq, Copy, Clone)] | |
| 5 | 5 | pub enum Color { |
| 6 | 6 | #[default] |
| 7 | 7 | Default, |
| @@ -12,13 +12,19 @@ · 11 unmodified lines | ||
| 12 | 12 | #![doc = include_str!("../docs/building-ui.md")] |
| 13 | 13 | #![doc = include_str!("../docs/todo.md")] |
| 14 | 14 | |
| 15 | +mod clickable; | |
| 15 | 16 | mod components; |
| 17 | +mod fixed; | |
| 16 | 18 | pub mod prelude; |
| 19 | +mod selectable; | |
| 17 | 20 | mod styled_ext; |
| 18 | 21 | mod styles; |
| 19 | 22 | pub mod utils; |
| 20 | 23 | |
| 24 | +pub use clickable::*; | |
| 21 | 25 | pub use components::*; |
| 26 | +pub use fixed::*; | |
| 22 | 27 | pub use prelude::*; |
| 28 | +pub use selectable::*; | |
| 23 | 29 | pub use styled_ext::*; |
| 24 | 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 | 7 | use schemars::JsonSchema; |
| 8 | 8 | use serde::{Deserialize, Serialize}; |
| 9 | 9 | use std::sync::Arc; |
| 10 | −use theme2::ActiveTheme; | |
| 11 | −use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip}; | |
| 10 | +use ui::prelude::*; | |
| 11 | +use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip}; | |
| 12 | 12 | |
| 13 | 13 | pub enum PanelEvent { |
| 14 | 14 | ChangePosition, |
| @@ -686,22 +686,26 @@ impl Render for PanelButtons { · 671 unmodified lines | ||
| 686 | 686 | let name = entry.panel.persistent_name(); |
| 687 | 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 | 692 | let action = dock.toggle_action(); |
| 693 | + | |
| 691 | 694 | let tooltip: SharedString = |
| 692 | 695 | format!("Close {} dock", dock.position.to_label()).into(); |
| 693 | − IconButton::new(name, icon) | |
| 694 | − .state(InteractionState::Active) | |
| 695 | − .action(action.boxed_clone()) | |
| 696 | − .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) | |
| 696 | + | |
| 697 | + (action, tooltip) | |
| 697 | 698 | } else { |
| 698 | 699 | let action = entry.panel.toggle_action(cx); |
| 699 | 700 | |
| 700 | − IconButton::new(name, icon) | |
| 701 | − .action(action.boxed_clone()) | |
| 702 | − .tooltip(move |cx| Tooltip::for_action(name, &*action, cx)) | |
| 701 | + (action, name.into()) | |
| 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 | 709 | Some( |
| 706 | 710 | menu_handle(name) |
| 707 | 711 | .menu(move |cx| { |
| @@ -1482,18 +1482,14 @@ impl Pane { · 1481 unmodified lines | ||
| 1482 | 1482 | .gap_px() |
| 1483 | 1483 | .child( |
| 1484 | 1484 | div().border().border_color(gpui::red()).child( |
| 1485 | − IconButton::new("navigate_backward", Icon::ArrowLeft).state( | |
| 1486 | − InteractionState::Enabled | |
| 1487 | − .if_enabled(self.can_navigate_backward()), | |
| 1488 | − ), | |
| 1485 | + IconButton::new("navigate_backward", Icon::ArrowLeft) | |
| 1486 | + .disabled(!self.can_navigate_backward()), | |
| 1489 | 1487 | ), |
| 1490 | 1488 | ) |
| 1491 | 1489 | .child( |
| 1492 | 1490 | div().border().border_color(gpui::red()).child( |
| 1493 | − IconButton::new("navigate_forward", Icon::ArrowRight).state( | |
| 1494 | − InteractionState::Enabled | |
| 1495 | − .if_enabled(self.can_navigate_forward()), | |
| 1496 | − ), | |
| 1491 | + IconButton::new("navigate_forward", Icon::ArrowRight) | |
| 1492 | + .disabled(!self.can_navigate_forward()), | |
| 1497 | 1493 | ), |
| 1498 | 1494 | ), |
| 1499 | 1495 | ), |
| @@ -5,7 +5,7 @@ use gpui::{ · 4 unmodified lines | ||
| 5 | 5 | div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, |
| 6 | 6 | WindowContext, |
| 7 | 7 | }; |
| 8 | −use theme2::ActiveTheme; | |
| 8 | +use ui::prelude::*; | |
| 9 | 9 | use ui::{h_stack, Button, Icon, IconButton}; |
| 10 | 10 | use util::ResultExt; |
| 11 | 11 | |
| @@ -3,7 +3,7 @@ use gpui::{ · 2 unmodified lines | ||
| 3 | 3 | div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, |
| 4 | 4 | ViewContext, WindowContext, |
| 5 | 5 | }; |
| 6 | −use theme2::ActiveTheme; | |
| 6 | +use ui::prelude::*; | |
| 7 | 7 | use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label}; |
| 8 | 8 | |
| 9 | 9 | pub enum ToolbarItemEvent { |
| @@ -1808,22 +1808,22 @@ impl Workspace { · 1807 unmodified lines | ||
| 1808 | 1808 | pane |
| 1809 | 1809 | } |
| 1810 | 1810 | |
| 1811 | − // pub fn add_item_to_center( | |
| 1812 | − // &mut self, | |
| 1813 | − // item: Box<dyn ItemHandle>, | |
| 1814 | − // cx: &mut ViewContext<Self>, | |
| 1815 | − // ) -> bool { | |
| 1816 | − // if let Some(center_pane) = self.last_active_center_pane.clone() { | |
| 1817 | − // if let Some(center_pane) = center_pane.upgrade(cx) { | |
| 1818 | − // center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); | |
| 1819 | − // true | |
| 1820 | − // } else { | |
| 1821 | − // false | |
| 1822 | − // } | |
| 1823 | − // } else { | |
| 1824 | − // false | |
| 1825 | − // } | |
| 1826 | − // } | |
| 1811 | + pub fn add_item_to_center( | |
| 1812 | + &mut self, | |
| 1813 | + item: Box<dyn ItemHandle>, | |
| 1814 | + cx: &mut ViewContext<Self>, | |
| 1815 | + ) -> bool { | |
| 1816 | + if let Some(center_pane) = self.last_active_center_pane.clone() { | |
| 1817 | + if let Some(center_pane) = center_pane.upgrade() { | |
| 1818 | + center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); | |
| 1819 | + true | |
| 1820 | + } else { | |
| 1821 | + false | |
| 1822 | + } | |
| 1823 | + } else { | |
| 1824 | + false | |
| 1825 | + } | |
| 1826 | + } | |
| 1827 | 1827 | |
| 1828 | 1828 | pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) { |
| 1829 | 1829 | self.active_pane |
| @@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"] · 2 unmodified lines | ||
| 3 | 3 | description = "The fast, collaborative code editor." |
| 4 | 4 | edition = "2021" |
| 5 | 5 | name = "zed" |
| 6 | −version = "0.115.0" | |
| 6 | +version = "0.116.0" | |
| 7 | 7 | publish = false |
| 8 | 8 | |
| 9 | 9 | [lib] |
| @@ -66,12 +66,12 @@ shellexpand = "2.1.0" · 65 unmodified lines | ||
| 66 | 66 | text = { package = "text2", path = "../text2" } |
| 67 | 67 | terminal_view = { package = "terminal_view2", path = "../terminal_view2" } |
| 68 | 68 | theme = { package = "theme2", path = "../theme2" } |
| 69 | −# theme_selector = { path = "../theme_selector" } | |
| 69 | +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } | |
| 70 | 70 | util = { path = "../util" } |
| 71 | 71 | # semantic_index = { path = "../semantic_index" } |
| 72 | 72 | # vim = { path = "../vim" } |
| 73 | 73 | workspace = { package = "workspace2", path = "../workspace2" } |
| 74 | −# welcome = { path = "../welcome" } | |
| 74 | +welcome = { package = "welcome2", path = "../welcome2" } | |
| 75 | 75 | zed_actions = {package = "zed_actions2", path = "../zed_actions2"} |
| 76 | 76 | anyhow.workspace = true |
| 77 | 77 | async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } |
| @@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE; · 12 unmodified lines | ||
| 13 | 13 | use editor::Editor; |
| 14 | 14 | use fs::RealFs; |
| 15 | 15 | use futures::StreamExt; |
| 16 | −use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; | |
| 16 | +use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; | |
| 17 | 17 | use isahc::{prelude::Configurable, Request}; |
| 18 | 18 | use language::LanguageRegistry; |
| 19 | 19 | use log::LevelFilter; |
| @@ -36,7 +36,7 @@ use std::{ · 16 unmodified lines | ||
| 36 | 36 | path::{Path, PathBuf}, |
| 37 | 37 | sync::{ |
| 38 | 38 | atomic::{AtomicU32, Ordering}, |
| 39 | − Arc, | |
| 39 | + Arc, Weak, | |
| 40 | 40 | }, |
| 41 | 41 | thread, |
| 42 | 42 | }; |
| @@ -48,6 +48,7 @@ use util::{ · 5 unmodified lines | ||
| 48 | 48 | paths, ResultExt, |
| 49 | 49 | }; |
| 50 | 50 | use uuid::Uuid; |
| 51 | +use welcome::{show_welcome_experience, FIRST_OPEN}; | |
| 51 | 52 | use workspace::{AppState, WorkspaceStore}; |
| 52 | 53 | use zed2::{ |
| 53 | 54 | build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, |
| @@ -103,16 +104,15 @@ fn main() { · 49 unmodified lines | ||
| 103 | 104 | let listener = Arc::new(listener); |
| 104 | 105 | let open_listener = listener.clone(); |
| 105 | 106 | app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); |
| 106 | − app.on_reopen(move |_cx| { | |
| 107 | − // todo!("workspace") | |
| 108 | − // if cx.has_global::<Weak<AppState>>() { | |
| 109 | − // if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() { | |
| 110 | − // workspace::open_new(&app_state, cx, |workspace, cx| { | |
| 111 | − // Editor::new_file(workspace, &Default::default(), cx) | |
| 112 | − // }) | |
| 113 | − // .detach(); | |
| 114 | − // } | |
| 115 | − // } | |
| 107 | + app.on_reopen(move |cx| { | |
| 108 | + if cx.has_global::<Weak<AppState>>() { | |
| 109 | + if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() { | |
| 110 | + workspace::open_new(&app_state, cx, |workspace, cx| { | |
| 111 | + Editor::new_file(workspace, &Default::default(), cx) | |
| 112 | + }) | |
| 113 | + .detach(); | |
| 114 | + } | |
| 115 | + } | |
| 116 | 116 | }); |
| 117 | 117 | |
| 118 | 118 | app.run(move |cx| { |
| @@ -164,17 +164,16 @@ fn main() { · 45 unmodified lines | ||
| 164 | 164 | // assistant::init(cx); |
| 165 | 165 | // component_test::init(cx); |
| 166 | 166 | |
| 167 | − // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); | |
| 168 | 167 | // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) |
| 169 | 168 | // .detach(); |
| 170 | − // watch_file_types(fs.clone(), cx); | |
| 169 | + watch_file_types(fs.clone(), cx); | |
| 171 | 170 | |
| 172 | 171 | languages.set_theme(cx.theme().clone()); |
| 173 | − // cx.observe_global::<SettingsStore, _>({ | |
| 174 | − // let languages = languages.clone(); | |
| 175 | − // move |cx| languages.set_theme(theme::current(cx).clone()) | |
| 176 | − // }) | |
| 177 | − // .detach(); | |
| 172 | + cx.observe_global::<SettingsStore>({ | |
| 173 | + let languages = languages.clone(); | |
| 174 | + move |cx| languages.set_theme(cx.theme().clone()) | |
| 175 | + }) | |
| 176 | + .detach(); | |
| 178 | 177 | |
| 179 | 178 | client.telemetry().start(installation_id, session_id, cx); |
| 180 | 179 | let telemetry_settings = *client::TelemetrySettings::get_global(cx); |
| @@ -193,7 +192,6 @@ fn main() { · 12 unmodified lines | ||
| 193 | 192 | fs, |
| 194 | 193 | build_window_options, |
| 195 | 194 | call_factory: call::Call::new, |
| 196 | − // background_actions: todo!("ask Mikayla"), | |
| 197 | 195 | workspace_store, |
| 198 | 196 | node_runtime, |
| 199 | 197 | }); |
| @@ -219,14 +217,13 @@ fn main() { · 19 unmodified lines | ||
| 219 | 217 | |
| 220 | 218 | // journal2::init(app_state.clone(), cx); |
| 221 | 219 | // language_selector::init(cx); |
| 222 | − // theme_selector::init(cx); | |
| 220 | + theme_selector::init(cx); | |
| 223 | 221 | // activity_indicator::init(cx); |
| 224 | 222 | // language_tools::init(cx); |
| 225 | 223 | call::init(app_state.client.clone(), app_state.user_store.clone(), cx); |
| 226 | 224 | collab_ui::init(&app_state, cx); |
| 227 | 225 | // feedback::init(cx); |
| 228 | − // welcome::init(cx); | |
| 229 | − // zed::init(&app_state, cx); | |
| 226 | + welcome::init(cx); | |
| 230 | 227 | |
| 231 | 228 | // cx.set_menus(menus::menus()); |
| 232 | 229 | initialize_workspace(app_state.clone(), cx); |
| @@ -279,17 +276,18 @@ fn main() { · 46 unmodified lines | ||
| 279 | 276 | .detach(); |
| 280 | 277 | } |
| 281 | 278 | Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => { |
| 282 | − todo!() | |
| 283 | − // triggered_authentication = true; | |
| 284 | − // let app_state = app_state.clone(); | |
| 285 | − // let client = client.clone(); | |
| 286 | − // cx.spawn(|mut cx| async move { | |
| 287 | − // // ignore errors here, we'll show a generic "not signed in" | |
| 288 | − // let _ = authenticate(client, &cx).await; | |
| 289 | − // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) | |
| 290 | − // .await | |
| 291 | − // }) | |
| 292 | − // .detach_and_log_err(cx) | |
| 279 | + triggered_authentication = true; | |
| 280 | + let app_state = app_state.clone(); | |
| 281 | + let client = client.clone(); | |
| 282 | + cx.spawn(|mut cx| async move { | |
| 283 | + // ignore errors here, we'll show a generic "not signed in" | |
| 284 | + let _ = authenticate(client, &cx).await; | |
| 285 | + //todo!() | |
| 286 | + // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) | |
| 287 | + // .await | |
| 288 | + anyhow::Ok(()) | |
| 289 | + }) | |
| 290 | + .detach_and_log_err(cx) | |
| 293 | 291 | } |
| 294 | 292 | Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => { |
| 295 | 293 | todo!() |
| @@ -340,7 +338,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> { · 44 unmodified lines | ||
| 340 | 338 | if client::IMPERSONATE_LOGIN.is_some() { |
| 341 | 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 | 342 | client.authenticate_and_connect(true, &cx).await?; |
| 345 | 343 | } |
| 346 | 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 | 366 | cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))? |
| 369 | 367 | .await |
| 370 | 368 | .log_err(); |
| 371 | − // todo!(welcome) | |
| 372 | − //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { | |
| 373 | − //todo!() | |
| 374 | − // cx.update(|cx| show_welcome_experience(app_state, cx)); | |
| 369 | + } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { | |
| 370 | + cx.update(|cx| show_welcome_experience(app_state, cx)) | |
| 371 | + .log_err(); | |
| 375 | 372 | } else { |
| 376 | 373 | cx.update(|cx| { |
| 377 | 374 | workspace::open_new(app_state, cx, |workspace, cx| { |
| @@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) { · 331 unmodified lines | ||
| 709 | 706 | .unwrap(); |
| 710 | 707 | } |
| 711 | 708 | |
| 712 | −// #[cfg(debug_assertions)] | |
| 713 | −// async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> { | |
| 714 | −// let mut events = fs | |
| 715 | −// .watch("styles/src".as_ref(), Duration::from_millis(100)) | |
| 716 | −// .await; | |
| 717 | −// while (events.next().await).is_some() { | |
| 718 | −// let output = Command::new("npm") | |
| 719 | −// .current_dir("styles") | |
| 720 | −// .args(["run", "build"]) | |
| 721 | −// .output() | |
| 722 | −// .await | |
| 723 | −// .log_err()?; | |
| 724 | −// if output.status.success() { | |
| 725 | −// cx.update(|cx| theme_selector::reload(cx)) | |
| 726 | −// } else { | |
| 727 | −// eprintln!( | |
| 728 | −// "build script failed {}", | |
| 729 | −// String::from_utf8_lossy(&output.stderr) | |
| 730 | −// ); | |
| 731 | −// } | |
| 732 | −// } | |
| 733 | −// Some(()) | |
| 734 | −// } | |
| 735 | − | |
| 736 | −// #[cfg(debug_assertions)] | |
| 737 | −// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> { | |
| 738 | −// let mut events = fs | |
| 739 | −// .watch( | |
| 740 | −// "crates/zed/src/languages".as_ref(), | |
| 741 | −// Duration::from_millis(100), | |
| 742 | −// ) | |
| 743 | −// .await; | |
| 744 | −// while (events.next().await).is_some() { | |
| 745 | −// languages.reload(); | |
| 746 | −// } | |
| 747 | −// Some(()) | |
| 748 | −// } | |
| 749 | − | |
| 750 | −// #[cfg(debug_assertions)] | |
| 751 | −// fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) { | |
| 752 | −// cx.spawn(|mut cx| async move { | |
| 753 | −// let mut events = fs | |
| 754 | −// .watch( | |
| 755 | −// "assets/icons/file_icons/file_types.json".as_ref(), | |
| 756 | −// Duration::from_millis(100), | |
| 757 | −// ) | |
| 758 | −// .await; | |
| 759 | −// while (events.next().await).is_some() { | |
| 760 | −// cx.update(|cx| { | |
| 761 | −// cx.update_global(|file_types, _| { | |
| 762 | −// *file_types = project_panel::file_associations::FileAssociations::new(Assets); | |
| 763 | −// }); | |
| 764 | −// }) | |
| 765 | −// } | |
| 766 | −// }) | |
| 767 | −// .detach() | |
| 768 | −// } | |
| 769 | − | |
| 770 | −// #[cfg(not(debug_assertions))] | |
| 771 | −// async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> { | |
| 772 | −// None | |
| 773 | −// } | |
| 774 | − | |
| 775 | −// #[cfg(not(debug_assertions))] | |
| 776 | −// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> { | |
| 777 | −// None | |
| 778 | −// | |
| 779 | − | |
| 780 | −// #[cfg(not(debug_assertions))] | |
| 781 | −// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {} | |
| 782 | − | |
| 783 | −pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { | |
| 784 | − // &[ | |
| 785 | − // ("Go to file", &file_finder::Toggle), | |
| 786 | − // ("Open command palette", &command_palette::Toggle), | |
| 787 | − // ("Open recent projects", &recent_projects::OpenRecent), | |
| 788 | − // ("Change your settings", &zed_actions::OpenSettings), | |
| 789 | − // ] | |
| 790 | − // todo!() | |
| 791 | − &[] | |
| 709 | +#[cfg(debug_assertions)] | |
| 710 | +async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> { | |
| 711 | + use std::time::Duration; | |
| 712 | + | |
| 713 | + let mut events = fs | |
| 714 | + .watch( | |
| 715 | + "crates/zed2/src/languages".as_ref(), | |
| 716 | + Duration::from_millis(100), | |
| 717 | + ) | |
| 718 | + .await; | |
| 719 | + while (events.next().await).is_some() { | |
| 720 | + languages.reload(); | |
| 721 | + } | |
| 722 | + Some(()) | |
| 723 | +} | |
| 724 | + | |
| 725 | +#[cfg(debug_assertions)] | |
| 726 | +fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) { | |
| 727 | + use std::time::Duration; | |
| 728 | + | |
| 729 | + cx.spawn(|mut cx| async move { | |
| 730 | + let mut events = fs | |
| 731 | + .watch( | |
| 732 | + "assets/icons/file_icons/file_types.json".as_ref(), | |
| 733 | + Duration::from_millis(100), | |
| 734 | + ) | |
| 735 | + .await; | |
| 736 | + while (events.next().await).is_some() { | |
| 737 | + cx.update(|cx| { | |
| 738 | + cx.update_global(|file_types, _| { | |
| 739 | + *file_types = project_panel::file_associations::FileAssociations::new(Assets); | |
| 740 | + }); | |
| 741 | + }) | |
| 742 | + .ok(); | |
| 743 | + } | |
| 744 | + }) | |
| 745 | + .detach() | |
| 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 | 11 | cargo depgraph \ |
| 12 | 12 | --workspace-only \ |
| 13 | 13 | --offline \ |
| 14 | − --root=zed,cli,collab \ | |
| 14 | + --root=zed2,cli,collab2 \ | |
| 15 | 15 | --dedup-transitive-deps \ |
| 16 | 16 | | dot -Tsvg > $graph_file |
| 17 | 17 | |